summaryrefslogtreecommitdiff
path: root/qa/qa/support/matchers/eventually_matcher.rb
blob: 2fb5249d9af3265b5089e3991c2d6abeb06ab903 (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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# 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(max_duration: 10, max_attempts: 5)
# )

module QA
  module Support
    module Matchers
      module EventuallyMatcher
        %w[
          eq
          be
          include
          be_truthy
          be_falsey
          be_empty
        ].each do |op|
          RSpec::Matchers.define(:"eventually_#{op}") do |*expected|
            chain(:within) do |kwargs = {}|
              @retry_args = kwargs
              @retry_args[:sleep_interval] = 0.5 unless kwargs[:sleep_interval]
            end

            description { "eventually #{operator_msg}: #{expected_formatted}" }

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

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

            failure_message { fail_message }

            failure_message_when_negated { fail_message(negate: true) }

            def supports_block_expectations?
              true
            end

            # Execute rspec expectation within retrier
            #
            # @param [Proc] actual
            # @param [Symbol] expectation_name
            # @return [Boolean]
            def wait_and_check(actual, expectation_name)
              attempt = 0

              QA::Runtime::Logger.debug(
                "Running eventually matcher with '#{operator_msg}' operator with: '#{retry_args}' arguments"
              )
              QA::Support::Retrier.retry_until(**retry_args, log: false) do
                QA::Runtime::Logger.debug("evaluating expectation, attempt: #{attempt += 1}")

                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
              operator == 'eq' ? 'equal' : operator
            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

            # Custom retry arguments
            #
            # @return [Hash]
            def retry_args
              @retry_args ||= { sleep_interval: 0.5 }
            end

            # Custom failure message
            #
            # @param [Boolean] negate
            # @return [String]
            def fail_message(negate: false)
              "#{e}:\n\nexpected #{negate ? 'not ' : ''}to #{description}\n\n"\
              "last attempt was: #{@result.nil? ? 'nil' : actual_formatted}\n\n"\
              "Diff:#{diff}"
            end

            # Formatted expect
            #
            # @return [String]
            def expected_formatted
              RSpec::Support::ObjectFormatter.format(expected)
            end

            # Formatted actual result
            #
            # @return [String]
            def actual_formatted
              RSpec::Support::ObjectFormatter.format(@result)
            end

            # Object diff
            #
            # @return [String]
            def diff
              RSpec::Support::Differ.new(color: true).diff(@result, expected)
            end
          end
        end
      end
    end
  end
end