summaryrefslogtreecommitdiff
path: root/qa/spec/support/matchers/eventually_matcher.rb
blob: 3f0afd6fb54d71fd8c0a70ee3fdbbf21491d3e68 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# frozen_string_literal: true

# Rspec matcher with build in retry logic
#
# USAGE:
#
# Basic
# expect { Something.that.takes.time.to_appear }.to eventually_eq(expected_result)
# expect { Something.that.takes.time.to_appear }.not_to eventually_eq(expected_result)
#
# With duration and attempts override
# expect { Something.that.takes.time.to_appear }.to eventually_eq(expected_result).within(duration: 10, attempts: 5)

module Matchers
  %w[
    eq
    be
    include
    be_truthy
    be_falsey
    be_empty
  ].each do |op|
    RSpec::Matchers.define(:"eventually_#{op}") do |*expected|
      chain(:within) do |options = {}|
        @duration = options[:duration]
        @attempts = options[:attempts]
      end

      def supports_block_expectations?
        true
      end

      match { |actual| wait_and_check(actual, :default_expectation) }

      match_when_negated { |actual| wait_and_check(actual, :when_negated_expectation) }

      description do
        "eventually #{operator_msg} #{expected.inspect}"
      end

      failure_message do
        "#{e}:\nexpected to #{description}, last attempt was #{@result.nil? ? 'nil' : @result}"
      end

      failure_message_when_negated do
        "#{e}:\nexpected not to #{description}, last attempt was #{@result.nil? ? 'nil' : @result}"
      end

      # Execute rspec expectation within retrier
      #
      # @param [Proc] actual
      # @param [Symbol] expectation_name
      # @return [Boolean]
      def wait_and_check(actual, expectation_name)
        QA::Support::Retrier.retry_until(
          max_attempts: @attempts,
          max_duration: @duration,
          sleep_interval: 0.5
        ) do
          public_send(expectation_name, actual)
        rescue RSpec::Expectations::ExpectationNotMetError, QA::Resource::ApiFabricator::ResourceNotFoundError
          false
        end
      rescue QA::Support::Repeater::RetriesExceededError, QA::Support::Repeater::WaitExceededError => e
        @e = e
        false
      end

      # Execute rspec expectation
      #
      # @param [Proc] actual
      # @return [void]
      def default_expectation(actual)
        expect(result(&actual)).to public_send(*expectation_args)
      end

      # Execute negated rspec expectation
      #
      # @param [Proc] actual
      # @return [void]
      def when_negated_expectation(actual)
        expect(result(&actual)).not_to public_send(*expectation_args)
      end

      # Result of actual block
      #
      # @return [Object]
      def result
        @result = yield
      end

      # Error message placeholder to indicate waiter did not fail properly
      # This message should not appear under normal circumstances since it should
      # always be assigned from repeater
      #
      # @return [String]
      def e
        @e ||= 'Waiter did not fail!'
      end

      # Operator message
      #
      # @return [String]
      def operator_msg
        case operator
        when 'eq' then 'equal'
        else operator
        end
      end

      # Expect operator
      #
      # @return [String]
      def operator
        @operator ||= name.to_s.match(/eventually_(.+?)$/).to_a[1].to_s
      end

      # Expectation args
      #
      # @return [String, Array]
      def expectation_args
        if operator.include?('truthy') || operator.include?('falsey') || operator.include?('empty')
          operator
        elsif operator == "include" && expected.is_a?(Array)
          [operator, *expected]
        else
          [operator, expected]
        end
      end
    end
  end
end