dexkit 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +50 -18
- data/gemfiles/mongoid_no_ar.gemfile +10 -0
- data/gemfiles/mongoid_no_ar.gemfile.lock +232 -0
- data/guides/llm/EVENT.md +41 -23
- data/guides/llm/FORM.md +202 -61
- data/guides/llm/OPERATION.md +49 -20
- data/guides/llm/QUERY.md +52 -2
- data/lib/dex/context_dsl.rb +56 -0
- data/lib/dex/context_setup.rb +2 -33
- data/lib/dex/event/bus.rb +85 -8
- data/lib/dex/event/handler.rb +18 -0
- data/lib/dex/event/metadata.rb +16 -9
- data/lib/dex/event/processor.rb +1 -1
- data/lib/dex/event/test_helpers.rb +88 -0
- data/lib/dex/event/trace.rb +14 -27
- data/lib/dex/event.rb +2 -7
- data/lib/dex/event_test_helpers.rb +1 -86
- data/lib/dex/form/context.rb +27 -0
- data/lib/dex/form/export.rb +128 -0
- data/lib/dex/form/nesting.rb +2 -0
- data/lib/dex/form/uniqueness_validator.rb +17 -1
- data/lib/dex/form.rb +119 -3
- data/lib/dex/id.rb +38 -0
- data/lib/dex/operation/async_proxy.rb +13 -2
- data/lib/dex/operation/explain.rb +11 -7
- data/lib/dex/operation/jobs.rb +5 -4
- data/lib/dex/operation/lock_wrapper.rb +15 -2
- data/lib/dex/operation/once_wrapper.rb +24 -15
- data/lib/dex/operation/record_backend.rb +15 -1
- data/lib/dex/operation/record_wrapper.rb +43 -8
- data/lib/dex/operation/test_helpers/assertions.rb +359 -0
- data/lib/dex/operation/test_helpers/execution.rb +30 -0
- data/lib/dex/operation/test_helpers/stubbing.rb +61 -0
- data/lib/dex/operation/test_helpers.rb +160 -0
- data/lib/dex/operation/trace_wrapper.rb +20 -0
- data/lib/dex/operation/transaction_adapter.rb +29 -68
- data/lib/dex/operation/transaction_wrapper.rb +10 -16
- data/lib/dex/operation.rb +2 -0
- data/lib/dex/query/backend.rb +13 -0
- data/lib/dex/query/export.rb +64 -0
- data/lib/dex/query.rb +50 -5
- data/lib/dex/ref_type.rb +4 -0
- data/lib/dex/test_helpers.rb +4 -139
- data/lib/dex/test_log.rb +62 -4
- data/lib/dex/trace.rb +291 -0
- data/lib/dex/type_coercion.rb +4 -1
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +9 -5
- metadata +16 -5
- data/lib/dex/test_helpers/assertions.rb +0 -333
- data/lib/dex/test_helpers/execution.rb +0 -28
- data/lib/dex/test_helpers/stubbing.rb +0 -59
- /data/lib/dex/{event_test_helpers → event/test_helpers}/assertions.rb +0 -0
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Dex
|
|
4
|
-
module TestHelpers
|
|
5
|
-
# --- Result assertions ---
|
|
6
|
-
|
|
7
|
-
def assert_ok(result, expected = :_not_given, msg = nil, &block)
|
|
8
|
-
assert result.ok?, msg || "Expected Ok, got Err:\n#{_dex_format_err(result)}"
|
|
9
|
-
if expected != :_not_given
|
|
10
|
-
assert_equal expected, result.value, msg || "Ok value mismatch"
|
|
11
|
-
end
|
|
12
|
-
yield result.value if block
|
|
13
|
-
result
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def refute_ok(result, msg = nil)
|
|
17
|
-
refute result.ok?, msg || "Expected Err, got Ok:\n#{_dex_format_ok(result)}"
|
|
18
|
-
result
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def assert_err(result, code = nil, message: nil, details: nil, msg: nil, &block)
|
|
22
|
-
assert result.error?, msg || "Expected Err, got Ok:\n#{_dex_format_ok(result)}"
|
|
23
|
-
if code
|
|
24
|
-
assert_equal code, result.code, msg || "Error code mismatch.\n#{_dex_format_err(result)}"
|
|
25
|
-
end
|
|
26
|
-
if message
|
|
27
|
-
case message
|
|
28
|
-
when Regexp
|
|
29
|
-
assert_match message, result.message, msg || "Error message mismatch.\n#{_dex_format_err(result)}"
|
|
30
|
-
else
|
|
31
|
-
assert_equal message, result.message, msg || "Error message mismatch.\n#{_dex_format_err(result)}"
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
details&.each do |key, val|
|
|
35
|
-
assert_equal val, result.details&.dig(key),
|
|
36
|
-
msg || "Error details[:#{key}] mismatch.\n#{_dex_format_err(result)}"
|
|
37
|
-
end
|
|
38
|
-
yield result.error if block
|
|
39
|
-
result
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def refute_err(result, code = nil, msg: nil)
|
|
43
|
-
if code
|
|
44
|
-
if result.error?
|
|
45
|
-
refute_equal code, result.code,
|
|
46
|
-
msg || "Expected result to not have error code #{code.inspect}, but it does.\n#{_dex_format_err(result)}"
|
|
47
|
-
end
|
|
48
|
-
else
|
|
49
|
-
refute result.error?, msg || "Expected Ok, got Err:\n#{_dex_format_err(result)}"
|
|
50
|
-
end
|
|
51
|
-
result
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# --- One-liner assertions ---
|
|
55
|
-
|
|
56
|
-
def assert_operation(*args, returns: :_not_given, **params)
|
|
57
|
-
klass = _dex_resolve_subject(args)
|
|
58
|
-
result = klass.new(**params).safe.call
|
|
59
|
-
assert result.ok?, "Expected operation to succeed, got Err:\n#{_dex_format_err(result)}"
|
|
60
|
-
if returns != :_not_given
|
|
61
|
-
assert_equal returns, result.value, "Return value mismatch"
|
|
62
|
-
end
|
|
63
|
-
result
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def assert_operation_error(*args, message: nil, details: nil, **params)
|
|
67
|
-
klass, code = _dex_resolve_subject_and_code(args)
|
|
68
|
-
result = klass.new(**params).safe.call
|
|
69
|
-
assert result.error?, "Expected operation to fail, got Ok:\n#{_dex_format_ok(result)}"
|
|
70
|
-
if code
|
|
71
|
-
assert_equal code, result.code, "Error code mismatch.\n#{_dex_format_err(result)}"
|
|
72
|
-
end
|
|
73
|
-
if message
|
|
74
|
-
case message
|
|
75
|
-
when Regexp
|
|
76
|
-
assert_match message, result.message
|
|
77
|
-
else
|
|
78
|
-
assert_equal message, result.message
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
details&.each do |key, val|
|
|
82
|
-
assert_equal val, result.details&.dig(key)
|
|
83
|
-
end
|
|
84
|
-
result
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# --- Contract assertions ---
|
|
88
|
-
|
|
89
|
-
def assert_params(*args)
|
|
90
|
-
if args.last.is_a?(Hash)
|
|
91
|
-
klass_args, type_hash = _dex_split_class_and_hash(args)
|
|
92
|
-
klass = _dex_resolve_subject(klass_args)
|
|
93
|
-
contract = klass.contract
|
|
94
|
-
type_hash.each do |name, type|
|
|
95
|
-
assert contract.params.key?(name),
|
|
96
|
-
"Expected param #{name.inspect} to be declared on #{klass.name || klass}"
|
|
97
|
-
assert_equal type, contract.params[name],
|
|
98
|
-
"Type mismatch for param #{name.inspect}"
|
|
99
|
-
end
|
|
100
|
-
else
|
|
101
|
-
klass_args, names = _dex_split_class_and_symbols(args)
|
|
102
|
-
klass = _dex_resolve_subject(klass_args)
|
|
103
|
-
contract = klass.contract
|
|
104
|
-
assert_equal names.sort, contract.params.keys.sort,
|
|
105
|
-
"Params mismatch on #{klass.name || klass}.\n Expected: #{names.sort.inspect}\n Actual: #{contract.params.keys.sort.inspect}"
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def assert_accepts_param(*args)
|
|
110
|
-
klass_args, names = _dex_split_class_and_symbols(args)
|
|
111
|
-
klass = _dex_resolve_subject(klass_args)
|
|
112
|
-
contract = klass.contract
|
|
113
|
-
names.each do |name|
|
|
114
|
-
assert contract.params.key?(name),
|
|
115
|
-
"Expected #{klass.name || klass} to accept param #{name.inspect}, but it doesn't.\n Declared params: #{contract.params.keys.inspect}"
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def assert_success_type(*args)
|
|
120
|
-
klass = if args.first.is_a?(Class) && args.first < Dex::Operation
|
|
121
|
-
args.shift
|
|
122
|
-
else
|
|
123
|
-
_dex_resolve_subject([])
|
|
124
|
-
end
|
|
125
|
-
expected = args.first
|
|
126
|
-
contract = klass.contract
|
|
127
|
-
assert_equal expected, contract.success,
|
|
128
|
-
"Success type mismatch on #{klass.name || klass}"
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def assert_error_codes(*args)
|
|
132
|
-
klass_args, codes = _dex_split_class_and_symbols(args)
|
|
133
|
-
klass = _dex_resolve_subject(klass_args)
|
|
134
|
-
contract = klass.contract
|
|
135
|
-
assert_equal codes.sort, contract.errors.sort,
|
|
136
|
-
"Error codes mismatch on #{klass.name || klass}.\n Expected: #{codes.sort.inspect}\n Actual: #{contract.errors.sort.inspect}"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def assert_contract(*args, params: nil, success: :_not_given, errors: nil)
|
|
140
|
-
klass = _dex_resolve_subject(args)
|
|
141
|
-
contract = klass.contract
|
|
142
|
-
|
|
143
|
-
if params
|
|
144
|
-
case params
|
|
145
|
-
when Array
|
|
146
|
-
assert_equal params.sort, contract.params.keys.sort, "Contract params mismatch"
|
|
147
|
-
when Hash
|
|
148
|
-
params.each do |name, type|
|
|
149
|
-
assert contract.params.key?(name), "Expected param #{name.inspect}"
|
|
150
|
-
assert_equal type, contract.params[name], "Type mismatch for param #{name.inspect}"
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
if success != :_not_given
|
|
156
|
-
assert_equal success, contract.success, "Contract success type mismatch"
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
if errors
|
|
160
|
-
assert_equal errors.sort, contract.errors.sort, "Contract error codes mismatch"
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# --- Param validation assertions ---
|
|
165
|
-
|
|
166
|
-
def assert_invalid_params(*args, **params)
|
|
167
|
-
klass = _dex_resolve_subject(args)
|
|
168
|
-
assert_raises(Literal::TypeError) { klass.new(**params) }
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def assert_valid_params(*args, **params)
|
|
172
|
-
klass = _dex_resolve_subject(args)
|
|
173
|
-
klass.new(**params)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# --- Async assertions ---
|
|
177
|
-
|
|
178
|
-
def assert_enqueues_operation(*args, queue: nil, **params)
|
|
179
|
-
_dex_ensure_active_job_test_helper!
|
|
180
|
-
klass = _dex_resolve_subject(args)
|
|
181
|
-
async_opts = queue ? { queue: queue } : {}
|
|
182
|
-
before_count = enqueued_jobs.size
|
|
183
|
-
klass.new(**params).async(**async_opts).call
|
|
184
|
-
new_jobs = enqueued_jobs[before_count..]
|
|
185
|
-
dex_job = new_jobs.find { |j|
|
|
186
|
-
j[:job] == Dex::Operation::DirectJob || j[:job] == Dex::Operation::RecordJob
|
|
187
|
-
}
|
|
188
|
-
assert dex_job,
|
|
189
|
-
"Expected #{klass.name || klass} to enqueue an async job, but none were enqueued"
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def refute_enqueues_operation(&block)
|
|
193
|
-
_dex_ensure_active_job_test_helper!
|
|
194
|
-
before_count = enqueued_jobs.size
|
|
195
|
-
yield
|
|
196
|
-
after_count = enqueued_jobs.size
|
|
197
|
-
assert_equal before_count, after_count,
|
|
198
|
-
"Expected no operations to be enqueued, but #{after_count - before_count} were"
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# --- Transaction assertions ---
|
|
202
|
-
|
|
203
|
-
def assert_rolls_back(model_class, &block)
|
|
204
|
-
count_before = model_class.count
|
|
205
|
-
assert_raises(Dex::Error) { yield }
|
|
206
|
-
assert_equal count_before, model_class.count,
|
|
207
|
-
"Expected transaction to roll back, but #{model_class.name} count changed from #{count_before} to #{model_class.count}"
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def assert_commits(model_class, &block)
|
|
211
|
-
count_before = model_class.count
|
|
212
|
-
yield
|
|
213
|
-
assert count_before < model_class.count,
|
|
214
|
-
"Expected #{model_class.name} count to increase, but it stayed at #{count_before}"
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# --- Guard assertions ---
|
|
218
|
-
|
|
219
|
-
def assert_callable(*args, **params)
|
|
220
|
-
klass = _dex_resolve_subject(args)
|
|
221
|
-
result = klass.callable(**params)
|
|
222
|
-
assert result.ok?, "Expected operation to be callable, but guards failed:\n#{_dex_format_err(result)}"
|
|
223
|
-
result
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def refute_callable(*args, **params)
|
|
227
|
-
klass_args, codes = _dex_split_class_and_symbols(args)
|
|
228
|
-
klass = _dex_resolve_subject(klass_args)
|
|
229
|
-
code = codes.first
|
|
230
|
-
result = klass.callable(**params)
|
|
231
|
-
refute result.ok?, "Expected operation to NOT be callable, but all guards passed"
|
|
232
|
-
if code
|
|
233
|
-
failed_codes = result.details.map { |f| f[:guard] }
|
|
234
|
-
assert_includes failed_codes, code,
|
|
235
|
-
"Expected guard :#{code} to fail, but it didn't.\n Failed guards: #{failed_codes.inspect}"
|
|
236
|
-
end
|
|
237
|
-
result
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# --- Batch assertions ---
|
|
241
|
-
|
|
242
|
-
def assert_all_succeed(*args, params_list:)
|
|
243
|
-
klass = _dex_resolve_subject(args)
|
|
244
|
-
results = params_list.map { |p| klass.new(**p).safe.call }
|
|
245
|
-
failures = results.each_with_index.reject { |r, _| r.ok? }
|
|
246
|
-
if failures.any?
|
|
247
|
-
msgs = failures.map { |r, i| " [#{i}] #{params_list[i].inspect} => #{_dex_format_err(r)}" }
|
|
248
|
-
flunk "Expected all #{results.size} calls to succeed, but #{failures.size} failed:\n#{msgs.join("\n")}"
|
|
249
|
-
end
|
|
250
|
-
results
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def assert_all_fail(*args, code:, params_list:, message: nil, details: nil)
|
|
254
|
-
klass = _dex_resolve_subject(args)
|
|
255
|
-
results = params_list.map { |p| klass.new(**p).safe.call }
|
|
256
|
-
failures = results.each_with_index.reject { |r, _| r.error? && r.code == code }
|
|
257
|
-
if failures.any?
|
|
258
|
-
msgs = failures.map { |r, i|
|
|
259
|
-
status = r.ok? ? "Ok(#{r.value.inspect})" : "Err(#{r.code})"
|
|
260
|
-
" [#{i}] #{params_list[i].inspect} => #{status}"
|
|
261
|
-
}
|
|
262
|
-
flunk "Expected all #{results.size} calls to fail with #{code.inspect}, but #{failures.size} didn't:\n#{msgs.join("\n")}"
|
|
263
|
-
end
|
|
264
|
-
results.each_with_index do |r, i|
|
|
265
|
-
if message
|
|
266
|
-
case message
|
|
267
|
-
when Regexp
|
|
268
|
-
assert_match message, r.message,
|
|
269
|
-
"Error message mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
|
|
270
|
-
else
|
|
271
|
-
assert_equal message, r.message,
|
|
272
|
-
"Error message mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
details&.each do |key, val|
|
|
276
|
-
assert_equal val, r.details&.dig(key),
|
|
277
|
-
"Error details[:#{key}] mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
results
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
private
|
|
284
|
-
|
|
285
|
-
def _dex_format_err(result)
|
|
286
|
-
return "(not an error)" unless result.respond_to?(:error?) && result.error?
|
|
287
|
-
|
|
288
|
-
lines = [" code: #{result.code.inspect}"]
|
|
289
|
-
lines << " message: #{result.message.inspect}" if result.message && result.message != result.code.to_s
|
|
290
|
-
lines << " details: #{result.details.inspect}" if result.details
|
|
291
|
-
lines.join("\n")
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def _dex_format_ok(result)
|
|
295
|
-
return "(not ok)" unless result.respond_to?(:ok?) && result.ok?
|
|
296
|
-
|
|
297
|
-
" value: #{result.value.inspect}"
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def _dex_resolve_subject_and_code(args)
|
|
301
|
-
if args.first.is_a?(Class) && args.first < Dex::Operation
|
|
302
|
-
klass = args.shift
|
|
303
|
-
code = args.shift
|
|
304
|
-
[klass, code]
|
|
305
|
-
elsif args.first.is_a?(Symbol)
|
|
306
|
-
[_dex_resolve_subject([]), args.shift]
|
|
307
|
-
else
|
|
308
|
-
[_dex_resolve_subject([]), nil]
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def _dex_split_class_and_symbols(args)
|
|
313
|
-
if args.first.is_a?(Class) && args.first < Dex::Operation
|
|
314
|
-
[args[0..0], args[1..]]
|
|
315
|
-
else
|
|
316
|
-
[[], args]
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
def _dex_split_class_and_hash(args)
|
|
321
|
-
hash = args.pop
|
|
322
|
-
klass_args = args.select { |a| a.is_a?(Class) && a < Dex::Operation }
|
|
323
|
-
[klass_args, hash]
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def _dex_ensure_active_job_test_helper!
|
|
327
|
-
return if respond_to?(:assert_enqueued_with)
|
|
328
|
-
|
|
329
|
-
raise "assert_enqueues_operation requires ActiveJob::TestHelper. " \
|
|
330
|
-
"Include it in your test class: `include ActiveJob::TestHelper`"
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Dex
|
|
4
|
-
module TestHelpers
|
|
5
|
-
def call_operation(*args, **params)
|
|
6
|
-
klass = _dex_resolve_subject(args)
|
|
7
|
-
klass.new(**params).safe.call
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def call_operation!(*args, **params)
|
|
11
|
-
klass = _dex_resolve_subject(args)
|
|
12
|
-
klass.new(**params).call
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def _dex_resolve_subject(args)
|
|
18
|
-
if args.first.is_a?(Class) && args.first < Dex::Operation
|
|
19
|
-
args.first
|
|
20
|
-
elsif _dex_test_subject
|
|
21
|
-
_dex_test_subject
|
|
22
|
-
else
|
|
23
|
-
raise ArgumentError,
|
|
24
|
-
"No operation class specified. Pass it as the first argument or use `testing MyOperation` in your test class."
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Dex
|
|
4
|
-
module TestHelpers
|
|
5
|
-
def stub_operation(klass, returns: nil, error: nil, &block)
|
|
6
|
-
raise ArgumentError, "stub_operation requires a block" unless block
|
|
7
|
-
|
|
8
|
-
opts = if error
|
|
9
|
-
{ error: error }
|
|
10
|
-
else
|
|
11
|
-
{ returns: returns }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
Dex::TestWrapper.register_stub(klass, **opts)
|
|
15
|
-
yield
|
|
16
|
-
ensure
|
|
17
|
-
Dex::TestWrapper.clear_stub(klass)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def spy_on_operation(klass, &block)
|
|
21
|
-
spy = Spy.new(klass)
|
|
22
|
-
yield spy
|
|
23
|
-
spy
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
class Spy
|
|
27
|
-
def initialize(klass)
|
|
28
|
-
@klass = klass
|
|
29
|
-
@started_at = Dex::TestLog.size
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def calls
|
|
33
|
-
Dex::TestLog.calls[@started_at..].select { |e| e.operation_class == @klass }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def called?
|
|
37
|
-
calls.any?
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def called_once?
|
|
41
|
-
calls.size == 1
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def call_count
|
|
45
|
-
calls.size
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def last_result
|
|
49
|
-
calls.last&.result
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def called_with?(**params)
|
|
53
|
-
calls.any? do |entry|
|
|
54
|
-
params.all? { |k, v| entry.params[k] == v }
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
File without changes
|