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
173
|
# 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
match
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.info(
"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
|