summaryrefslogtreecommitdiff
path: root/qa/qa/support/matchers/eventually_matcher.rb
blob: dedef8e6b98e6e1938257b77f7fd7ce46c7ee3dc (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
# 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 @retry_args[:sleep_interval]
            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)
              attempt = 0

              QA::Runtime::Logger.debug(
                "Running eventually matcher with '#{operator_msg}' operator with: #{@retry_args}"
              )
              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
          end
        end
      end
    end
  end
end