cmdx 0.5.0 → 1.0.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/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +31 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +203 -31
- data/docs/outcomes/statuses.md +275 -30
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +367 -25
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +405 -37
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -0,0 +1,917 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Custom RSpec matchers for CMDx result testing
|
4
|
+
#
|
5
|
+
# This module provides a comprehensive set of custom RSpec matchers specifically
|
6
|
+
# designed for testing CMDx task execution results. These matchers follow the
|
7
|
+
# RSpec Style Guide conventions and provide expressive, readable test assertions
|
8
|
+
# for task outcomes, side effects, and execution state.
|
9
|
+
#
|
10
|
+
# The matchers are automatically loaded when the spec_helper is required and are
|
11
|
+
# available in all RSpec test contexts.
|
12
|
+
#
|
13
|
+
# @example Basic result outcome testing
|
14
|
+
# expect(result).to be_successful_task
|
15
|
+
# expect(result).to be_failed_task.with_reason("Validation failed")
|
16
|
+
# expect(result).to be_skipped_task("Already processed")
|
17
|
+
#
|
18
|
+
# @example Side effects and context testing
|
19
|
+
# expect(result).to have_context(user_id: 123, processed: true)
|
20
|
+
# expect(result).to preserve_context(original_data)
|
21
|
+
#
|
22
|
+
# @example Composable matcher usage
|
23
|
+
# expect(result).to be_successful_task(user_id: 123)
|
24
|
+
# .and have_context(processed_at: be_a(Time))
|
25
|
+
# .and have_runtime(be > 0)
|
26
|
+
#
|
27
|
+
# @see https://rspec.rubystyle.guide/ RSpec Style Guide
|
28
|
+
# @since 1.0.0
|
29
|
+
|
30
|
+
# Tests that a task result represents a successful execution
|
31
|
+
#
|
32
|
+
# This matcher verifies that a result has a success status, complete state,
|
33
|
+
# and was executed. Optionally validates specific context attributes.
|
34
|
+
#
|
35
|
+
# @param [Hash] expected_context Optional hash of context attributes to validate
|
36
|
+
#
|
37
|
+
# @example Basic successful task validation
|
38
|
+
# expect(result).to be_successful_task
|
39
|
+
#
|
40
|
+
# @example Successful task with context validation
|
41
|
+
# expect(result).to be_successful_task(user_id: 123, processed: true)
|
42
|
+
#
|
43
|
+
# @example Negated usage
|
44
|
+
# expect(result).not_to be_successful_task
|
45
|
+
#
|
46
|
+
# @return [Boolean] true if result is successful, complete, and executed
|
47
|
+
#
|
48
|
+
# @since 1.0.0
|
49
|
+
RSpec::Matchers.define :be_successful_task do |expected_context = {}|
|
50
|
+
match do |result|
|
51
|
+
result.success? &&
|
52
|
+
result.complete? &&
|
53
|
+
result.executed? &&
|
54
|
+
(expected_context.empty? || context_matches?(result, expected_context))
|
55
|
+
end
|
56
|
+
|
57
|
+
failure_message do |result|
|
58
|
+
messages = []
|
59
|
+
messages << "expected result to be successful, but was #{result.status}" unless result.success?
|
60
|
+
messages << "expected result to be complete, but was #{result.state}" unless result.complete?
|
61
|
+
messages << "expected result to be executed, but was not" unless result.executed?
|
62
|
+
|
63
|
+
unless expected_context.empty?
|
64
|
+
mismatches = context_mismatches(result, expected_context)
|
65
|
+
messages << "expected context to match #{expected_context}, but #{mismatches}" if mismatches.any?
|
66
|
+
end
|
67
|
+
|
68
|
+
messages.join(", ")
|
69
|
+
end
|
70
|
+
|
71
|
+
failure_message_when_negated do |_result|
|
72
|
+
"expected result not to be successful, but it was"
|
73
|
+
end
|
74
|
+
|
75
|
+
description do
|
76
|
+
desc = "be a successful task"
|
77
|
+
desc += " with context #{expected_context}" unless expected_context.empty?
|
78
|
+
desc
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def context_matches?(result, expected_context)
|
84
|
+
expected_context.all? do |key, value|
|
85
|
+
result.context.public_send(key) == value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def context_mismatches(result, expected_context)
|
90
|
+
expected_context.filter_map do |key, expected_value|
|
91
|
+
actual_value = result.context.public_send(key)
|
92
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" if actual_value != expected_value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Tests that a task result represents a failed execution
|
98
|
+
#
|
99
|
+
# This matcher verifies that a result has a failed status, interrupted state,
|
100
|
+
# and was executed. Supports optional reason validation and chainable metadata checks.
|
101
|
+
#
|
102
|
+
# @param [String, nil] expected_reason Optional failure reason to validate
|
103
|
+
#
|
104
|
+
# @example Basic failed task validation
|
105
|
+
# expect(result).to be_failed_task
|
106
|
+
#
|
107
|
+
# @example Failed task with specific reason
|
108
|
+
# expect(result).to be_failed_task("Validation failed")
|
109
|
+
#
|
110
|
+
# @example Chainable reason and metadata validation
|
111
|
+
# expect(result).to be_failed_task
|
112
|
+
# .with_reason("Invalid data")
|
113
|
+
# .with_metadata(error_code: "ERR001", retryable: false)
|
114
|
+
#
|
115
|
+
# @example Negated usage
|
116
|
+
# expect(result).not_to be_failed_task
|
117
|
+
#
|
118
|
+
# @return [Boolean] true if result is failed, interrupted, and executed
|
119
|
+
#
|
120
|
+
# @since 1.0.0
|
121
|
+
RSpec::Matchers.define :be_failed_task do |expected_reason = nil|
|
122
|
+
match do |result|
|
123
|
+
result.failed? &&
|
124
|
+
result.interrupted? &&
|
125
|
+
result.executed? &&
|
126
|
+
(expected_reason.nil? || result.metadata[:reason] == expected_reason)
|
127
|
+
end
|
128
|
+
|
129
|
+
chain :with_reason do |reason|
|
130
|
+
@expected_reason = reason
|
131
|
+
end
|
132
|
+
|
133
|
+
chain :with_metadata do |metadata|
|
134
|
+
@expected_metadata = metadata
|
135
|
+
end
|
136
|
+
|
137
|
+
match do |result|
|
138
|
+
reason = @expected_reason || expected_reason
|
139
|
+
metadata = @expected_metadata || {}
|
140
|
+
|
141
|
+
result.failed? &&
|
142
|
+
result.interrupted? &&
|
143
|
+
result.executed? &&
|
144
|
+
(reason.nil? || result.metadata[:reason] == reason) &&
|
145
|
+
(metadata.empty? || metadata.all? { |k, v| result.metadata[k] == v })
|
146
|
+
end
|
147
|
+
|
148
|
+
failure_message do |result|
|
149
|
+
messages = []
|
150
|
+
messages << "expected result to be failed, but was #{result.status}" unless result.failed?
|
151
|
+
messages << "expected result to be interrupted, but was #{result.state}" unless result.interrupted?
|
152
|
+
messages << "expected result to be executed, but was not" unless result.executed?
|
153
|
+
|
154
|
+
reason = @expected_reason || expected_reason
|
155
|
+
messages << "expected failure reason to be '#{reason}', but was '#{result.metadata[:reason]}'" if reason && result.metadata[:reason] != reason
|
156
|
+
|
157
|
+
if @expected_metadata&.any?
|
158
|
+
mismatches = @expected_metadata.filter_map do |k, v|
|
159
|
+
"#{k}: expected #{v}, got #{result.metadata[k]}" if result.metadata[k] != v
|
160
|
+
end
|
161
|
+
messages.concat(mismatches)
|
162
|
+
end
|
163
|
+
|
164
|
+
messages.join(", ")
|
165
|
+
end
|
166
|
+
|
167
|
+
failure_message_when_negated do |_result|
|
168
|
+
"expected result not to be failed, but it was"
|
169
|
+
end
|
170
|
+
|
171
|
+
description do
|
172
|
+
desc = "be a failed task"
|
173
|
+
reason = @expected_reason || expected_reason
|
174
|
+
desc += " with reason '#{reason}'" if reason
|
175
|
+
desc += " with metadata #{@expected_metadata}" if @expected_metadata&.any?
|
176
|
+
desc
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Tests that a task result represents a skipped execution
|
181
|
+
#
|
182
|
+
# This matcher verifies that a result has a skipped status, interrupted state,
|
183
|
+
# and was executed. Supports optional reason validation and chainable metadata checks.
|
184
|
+
#
|
185
|
+
# @param [String, nil] expected_reason Optional skip reason to validate
|
186
|
+
#
|
187
|
+
# @example Basic skipped task validation
|
188
|
+
# expect(result).to be_skipped_task
|
189
|
+
#
|
190
|
+
# @example Skipped task with specific reason
|
191
|
+
# expect(result).to be_skipped_task("Already processed")
|
192
|
+
#
|
193
|
+
# @example Chainable reason and metadata validation
|
194
|
+
# expect(result).to be_skipped_task
|
195
|
+
# .with_reason("Order already processed")
|
196
|
+
# .with_metadata(processed_at: be_a(Time), skip_code: "DUPLICATE")
|
197
|
+
#
|
198
|
+
# @example Negated usage
|
199
|
+
# expect(result).not_to be_skipped_task
|
200
|
+
#
|
201
|
+
# @return [Boolean] true if result is skipped, interrupted, and executed
|
202
|
+
#
|
203
|
+
# @since 1.0.0
|
204
|
+
RSpec::Matchers.define :be_skipped_task do |expected_reason = nil|
|
205
|
+
match do |result|
|
206
|
+
result.skipped? &&
|
207
|
+
result.interrupted? &&
|
208
|
+
result.executed? &&
|
209
|
+
(expected_reason.nil? || result.metadata[:reason] == expected_reason)
|
210
|
+
end
|
211
|
+
|
212
|
+
chain :with_reason do |reason|
|
213
|
+
@expected_reason = reason
|
214
|
+
end
|
215
|
+
|
216
|
+
chain :with_metadata do |metadata|
|
217
|
+
@expected_metadata = metadata
|
218
|
+
end
|
219
|
+
|
220
|
+
match do |result|
|
221
|
+
reason = @expected_reason || expected_reason
|
222
|
+
metadata = @expected_metadata || {}
|
223
|
+
|
224
|
+
result.skipped? &&
|
225
|
+
result.interrupted? &&
|
226
|
+
result.executed? &&
|
227
|
+
(reason.nil? || result.metadata[:reason] == reason) &&
|
228
|
+
(metadata.empty? || metadata.all? { |k, v| result.metadata[k] == v })
|
229
|
+
end
|
230
|
+
|
231
|
+
failure_message do |result|
|
232
|
+
messages = []
|
233
|
+
messages << "expected result to be skipped, but was #{result.status}" unless result.skipped?
|
234
|
+
messages << "expected result to be interrupted, but was #{result.state}" unless result.interrupted?
|
235
|
+
messages << "expected result to be executed, but was not" unless result.executed?
|
236
|
+
|
237
|
+
reason = @expected_reason || expected_reason
|
238
|
+
messages << "expected skip reason to be '#{reason}', but was '#{result.metadata[:reason]}'" if reason && result.metadata[:reason] != reason
|
239
|
+
|
240
|
+
if @expected_metadata&.any?
|
241
|
+
mismatches = @expected_metadata.filter_map do |k, v|
|
242
|
+
"#{k}: expected #{v}, got #{result.metadata[k]}" if result.metadata[k] != v
|
243
|
+
end
|
244
|
+
messages.concat(mismatches)
|
245
|
+
end
|
246
|
+
|
247
|
+
messages.join(", ")
|
248
|
+
end
|
249
|
+
|
250
|
+
failure_message_when_negated do |_result|
|
251
|
+
"expected result not to be skipped, but it was"
|
252
|
+
end
|
253
|
+
|
254
|
+
description do
|
255
|
+
desc = "be a skipped task"
|
256
|
+
reason = @expected_reason || expected_reason
|
257
|
+
desc += " with reason '#{reason}'" if reason
|
258
|
+
desc += " with metadata #{@expected_metadata}" if @expected_metadata&.any?
|
259
|
+
desc
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Tests that a task result has a good outcome (success or skipped)
|
264
|
+
#
|
265
|
+
# This matcher verifies that a result has either a success or skipped status,
|
266
|
+
# representing a positive outcome where the task completed its intended purpose
|
267
|
+
# or was appropriately bypassed.
|
268
|
+
#
|
269
|
+
# @example Basic good outcome validation
|
270
|
+
# expect(result).to have_good_outcome
|
271
|
+
#
|
272
|
+
# @example Negated usage
|
273
|
+
# expect(result).not_to have_good_outcome
|
274
|
+
#
|
275
|
+
# @return [Boolean] true if result is success or skipped
|
276
|
+
#
|
277
|
+
# @since 1.0.0
|
278
|
+
RSpec::Matchers.define :have_good_outcome do
|
279
|
+
match(&:good?)
|
280
|
+
|
281
|
+
failure_message do |result|
|
282
|
+
"expected result to have good outcome (success or skipped), but was #{result.status}"
|
283
|
+
end
|
284
|
+
|
285
|
+
failure_message_when_negated do |result|
|
286
|
+
"expected result not to have good outcome, but it did (status: #{result.status})"
|
287
|
+
end
|
288
|
+
|
289
|
+
description do
|
290
|
+
"have good outcome"
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Tests that a task result has a bad outcome (not success)
|
295
|
+
#
|
296
|
+
# This matcher verifies that a result does not have a success status,
|
297
|
+
# representing a negative outcome where the task did not complete successfully.
|
298
|
+
#
|
299
|
+
# @example Basic bad outcome validation
|
300
|
+
# expect(result).to have_bad_outcome
|
301
|
+
#
|
302
|
+
# @example Negated usage
|
303
|
+
# expect(result).not_to have_bad_outcome
|
304
|
+
#
|
305
|
+
# @return [Boolean] true if result is not success
|
306
|
+
#
|
307
|
+
# @since 1.0.0
|
308
|
+
RSpec::Matchers.define :have_bad_outcome do
|
309
|
+
match(&:bad?)
|
310
|
+
|
311
|
+
failure_message do |result|
|
312
|
+
"expected result to have bad outcome (not success), but was #{result.status}"
|
313
|
+
end
|
314
|
+
|
315
|
+
failure_message_when_negated do |result|
|
316
|
+
"expected result not to have bad outcome, but it did (status: #{result.status})"
|
317
|
+
end
|
318
|
+
|
319
|
+
description do
|
320
|
+
"have bad outcome"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Tests that a task result indicates the task was executed
|
325
|
+
#
|
326
|
+
# This matcher verifies that a result shows the task has moved beyond
|
327
|
+
# the initialized state and has been processed by the execution engine.
|
328
|
+
#
|
329
|
+
# @example Basic execution validation
|
330
|
+
# expect(result).to be_executed
|
331
|
+
#
|
332
|
+
# @example Negated usage
|
333
|
+
# expect(result).not_to be_executed
|
334
|
+
#
|
335
|
+
# @return [Boolean] true if result indicates execution occurred
|
336
|
+
#
|
337
|
+
# @since 1.0.0
|
338
|
+
RSpec::Matchers.define :be_executed do
|
339
|
+
match(&:executed?)
|
340
|
+
|
341
|
+
failure_message do |result|
|
342
|
+
"expected result to be executed, but was in #{result.state} state"
|
343
|
+
end
|
344
|
+
|
345
|
+
failure_message_when_negated do |result|
|
346
|
+
"expected result not to be executed, but it was (state: #{result.state})"
|
347
|
+
end
|
348
|
+
|
349
|
+
description do
|
350
|
+
"be executed"
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
# Tests that a task result has runtime information
|
355
|
+
#
|
356
|
+
# This matcher verifies that a result contains execution timing data.
|
357
|
+
# Optionally validates the runtime against a specific value or matcher.
|
358
|
+
#
|
359
|
+
# @param [Numeric, RSpec::Matchers::BuiltIn::BaseMatcher, nil] expected_runtime
|
360
|
+
# Optional runtime value or matcher to validate against
|
361
|
+
#
|
362
|
+
# @example Basic runtime presence validation
|
363
|
+
# expect(result).to have_runtime
|
364
|
+
#
|
365
|
+
# @example Runtime with specific value
|
366
|
+
# expect(result).to have_runtime(0.5)
|
367
|
+
#
|
368
|
+
# @example Runtime with matcher
|
369
|
+
# expect(result).to have_runtime(be > 0)
|
370
|
+
# expect(result).to have_runtime(be_within(0.1).of(0.5))
|
371
|
+
#
|
372
|
+
# @example Negated usage
|
373
|
+
# expect(result).not_to have_runtime
|
374
|
+
#
|
375
|
+
# @return [Boolean] true if result has runtime (and matches expectation if provided)
|
376
|
+
#
|
377
|
+
# @since 1.0.0
|
378
|
+
RSpec::Matchers.define :have_runtime do |expected_runtime = nil|
|
379
|
+
match do |result|
|
380
|
+
return false if result.runtime.nil?
|
381
|
+
return true if expected_runtime.nil?
|
382
|
+
|
383
|
+
if expected_runtime.respond_to?(:matches?)
|
384
|
+
expected_runtime.matches?(result.runtime)
|
385
|
+
else
|
386
|
+
result.runtime == expected_runtime
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
failure_message do |result|
|
391
|
+
if result.runtime.nil?
|
392
|
+
"expected result to have runtime, but it was nil"
|
393
|
+
elsif expected_runtime
|
394
|
+
"expected result runtime to #{expected_runtime}, but was #{result.runtime}"
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
failure_message_when_negated do |result|
|
399
|
+
if expected_runtime
|
400
|
+
"expected result runtime not to #{expected_runtime}, but it was #{result.runtime}"
|
401
|
+
else
|
402
|
+
"expected result not to have runtime, but it was #{result.runtime}"
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
description do
|
407
|
+
if expected_runtime
|
408
|
+
"have runtime #{expected_runtime}"
|
409
|
+
else
|
410
|
+
"have runtime"
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Tests that a task result contains specific metadata
|
416
|
+
#
|
417
|
+
# This matcher verifies that a result's metadata hash contains the expected
|
418
|
+
# key-value pairs. Supports chainable inclusion for complex metadata validation.
|
419
|
+
#
|
420
|
+
# @param [Hash] expected_metadata Hash of metadata keys and values to validate
|
421
|
+
#
|
422
|
+
# @example Basic metadata validation
|
423
|
+
# expect(result).to have_metadata(reason: "Error", code: "001")
|
424
|
+
#
|
425
|
+
# @example Chainable metadata inclusion
|
426
|
+
# expect(result).to have_metadata(reason: "Error")
|
427
|
+
# .including(code: "001", retryable: false)
|
428
|
+
#
|
429
|
+
# @example Empty metadata validation
|
430
|
+
# expect(result).to have_metadata({})
|
431
|
+
#
|
432
|
+
# @example Negated usage
|
433
|
+
# expect(result).not_to have_metadata(reason: "Different error")
|
434
|
+
#
|
435
|
+
# @return [Boolean] true if result metadata contains all expected key-value pairs
|
436
|
+
#
|
437
|
+
# @since 1.0.0
|
438
|
+
RSpec::Matchers.define :have_metadata do |expected_metadata = {}|
|
439
|
+
match do |result|
|
440
|
+
expected_metadata.all? do |key, value|
|
441
|
+
actual_value = result.metadata[key]
|
442
|
+
if value.respond_to?(:matches?)
|
443
|
+
value.matches?(actual_value)
|
444
|
+
else
|
445
|
+
actual_value == value
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
chain :including do |metadata|
|
451
|
+
@additional_metadata = metadata
|
452
|
+
end
|
453
|
+
|
454
|
+
match do |result|
|
455
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
456
|
+
all_metadata.all? do |key, value|
|
457
|
+
actual_value = result.metadata[key]
|
458
|
+
if value.respond_to?(:matches?)
|
459
|
+
value.matches?(actual_value)
|
460
|
+
else
|
461
|
+
actual_value == value
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
failure_message do |result|
|
467
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
468
|
+
mismatches = all_metadata.filter_map do |key, expected_value|
|
469
|
+
actual_value = result.metadata[key]
|
470
|
+
match_result = if expected_value.respond_to?(:matches?)
|
471
|
+
expected_value.matches?(actual_value)
|
472
|
+
else
|
473
|
+
actual_value == expected_value
|
474
|
+
end
|
475
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" unless match_result
|
476
|
+
end
|
477
|
+
"expected metadata to include #{all_metadata}, but #{mismatches.join(', ')}"
|
478
|
+
end
|
479
|
+
|
480
|
+
failure_message_when_negated do |_result|
|
481
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
482
|
+
"expected metadata not to include #{all_metadata}, but it did"
|
483
|
+
end
|
484
|
+
|
485
|
+
description do
|
486
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
487
|
+
"have metadata #{all_metadata}"
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
# Tests that a task result has no metadata
|
492
|
+
#
|
493
|
+
# This matcher verifies that a result's metadata hash is empty,
|
494
|
+
# indicating no additional execution information was recorded.
|
495
|
+
#
|
496
|
+
# @example Basic empty metadata validation
|
497
|
+
# expect(result).to have_empty_metadata
|
498
|
+
#
|
499
|
+
# @example Negated usage
|
500
|
+
# expect(result).not_to have_empty_metadata
|
501
|
+
#
|
502
|
+
# @return [Boolean] true if result metadata is empty
|
503
|
+
#
|
504
|
+
# @since 1.0.0
|
505
|
+
RSpec::Matchers.define :have_empty_metadata do
|
506
|
+
match do |result|
|
507
|
+
result.metadata.empty?
|
508
|
+
end
|
509
|
+
|
510
|
+
failure_message do |result|
|
511
|
+
"expected metadata to be empty, but was #{result.metadata}"
|
512
|
+
end
|
513
|
+
|
514
|
+
failure_message_when_negated do |_result|
|
515
|
+
"expected metadata not to be empty, but it was"
|
516
|
+
end
|
517
|
+
|
518
|
+
description do
|
519
|
+
"have empty metadata"
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
# Tests that a task result has specific side effects in the context
|
524
|
+
#
|
525
|
+
# This matcher verifies that the result's context contains expected
|
526
|
+
# attribute changes or additions that represent the task's side effects.
|
527
|
+
# Supports both exact value matching and RSpec matcher integration.
|
528
|
+
#
|
529
|
+
# @param [Hash] expected_effects Hash of context attributes and expected values
|
530
|
+
#
|
531
|
+
# @example Basic side effects validation
|
532
|
+
# expect(result).to have_context(processed: true, user_id: 123)
|
533
|
+
#
|
534
|
+
# @example Side effects with RSpec matchers
|
535
|
+
# expect(result).to have_context(
|
536
|
+
# processed_at: be_a(Time),
|
537
|
+
# errors: be_empty,
|
538
|
+
# count: be > 0
|
539
|
+
# )
|
540
|
+
#
|
541
|
+
# @example Complex side effects validation
|
542
|
+
# expect(result).to have_context(
|
543
|
+
# user: have_attributes(id: 123, name: "John"),
|
544
|
+
# notifications: contain_exactly("email", "sms")
|
545
|
+
# )
|
546
|
+
#
|
547
|
+
# @example Negated usage
|
548
|
+
# expect(result).not_to have_context(deleted: true)
|
549
|
+
#
|
550
|
+
# @return [Boolean] true if context contains all expected side effects
|
551
|
+
#
|
552
|
+
# @since 1.0.0
|
553
|
+
RSpec::Matchers.define :have_context do |expected_effects|
|
554
|
+
match do |result|
|
555
|
+
expected_effects.all? do |key, expected_value|
|
556
|
+
actual_value = result.context.public_send(key)
|
557
|
+
if expected_value.respond_to?(:matches?)
|
558
|
+
expected_value.matches?(actual_value)
|
559
|
+
else
|
560
|
+
actual_value == expected_value
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
failure_message do |result|
|
566
|
+
mismatches = expected_effects.filter_map do |key, expected_value|
|
567
|
+
actual_value = result.context.public_send(key)
|
568
|
+
match_result = if expected_value.respond_to?(:matches?)
|
569
|
+
expected_value.matches?(actual_value)
|
570
|
+
else
|
571
|
+
actual_value == expected_value
|
572
|
+
end
|
573
|
+
|
574
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" unless match_result
|
575
|
+
end
|
576
|
+
"expected context to have side effects #{expected_effects}, but #{mismatches.join(', ')}"
|
577
|
+
end
|
578
|
+
|
579
|
+
failure_message_when_negated do |_result|
|
580
|
+
"expected context not to have side effects #{expected_effects}, but it did"
|
581
|
+
end
|
582
|
+
|
583
|
+
description do
|
584
|
+
"have side effects #{expected_effects}"
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# Tests that a task result preserves specific context attributes
|
589
|
+
#
|
590
|
+
# This matcher verifies that certain context attributes retain their
|
591
|
+
# original values after task execution, ensuring data integrity for
|
592
|
+
# attributes that should not be modified.
|
593
|
+
#
|
594
|
+
# @param [Hash] preserved_attributes Hash of attributes and their expected preserved values
|
595
|
+
#
|
596
|
+
# @example Basic context preservation validation
|
597
|
+
# expect(result).to preserve_context(user_id: 123, session_id: "abc")
|
598
|
+
#
|
599
|
+
# @example Preserving complex data structures
|
600
|
+
# expect(result).to preserve_context(
|
601
|
+
# original_request: original_data,
|
602
|
+
# user_permissions: ["read", "write"]
|
603
|
+
# )
|
604
|
+
#
|
605
|
+
# @example Negated usage
|
606
|
+
# expect(result).not_to preserve_context(temporary_flag: true)
|
607
|
+
#
|
608
|
+
# @return [Boolean] true if context preserves all specified attributes
|
609
|
+
#
|
610
|
+
# @since 1.0.0
|
611
|
+
RSpec::Matchers.define :preserve_context do |preserved_attributes|
|
612
|
+
match do |result|
|
613
|
+
preserved_attributes.all? do |key, expected_value|
|
614
|
+
result.context.public_send(key) == expected_value
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
failure_message do |result|
|
619
|
+
mismatches = preserved_attributes.filter_map do |key, expected_value|
|
620
|
+
actual_value = result.context.public_send(key)
|
621
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" if actual_value != expected_value
|
622
|
+
end
|
623
|
+
"expected context to preserve #{preserved_attributes}, but #{mismatches.join(', ')}"
|
624
|
+
end
|
625
|
+
|
626
|
+
failure_message_when_negated do |_result|
|
627
|
+
"expected context not to preserve #{preserved_attributes}, but it did"
|
628
|
+
end
|
629
|
+
|
630
|
+
description do
|
631
|
+
"preserve context #{preserved_attributes}"
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
# Tests that a task result represents a failure that was caused (not thrown)
|
636
|
+
#
|
637
|
+
# This matcher verifies that a failed result originated from the current task
|
638
|
+
# rather than being propagated from another task. Used to distinguish between
|
639
|
+
# original failures and failure propagation in task chains.
|
640
|
+
#
|
641
|
+
# @example Basic caused failure validation
|
642
|
+
# expect(result).to have_caused_failure
|
643
|
+
#
|
644
|
+
# @example Negated usage (for thrown failures)
|
645
|
+
# expect(result).not_to have_caused_failure
|
646
|
+
#
|
647
|
+
# @return [Boolean] true if result failed and caused the failure
|
648
|
+
#
|
649
|
+
# @since 1.0.0
|
650
|
+
RSpec::Matchers.define :have_caused_failure do
|
651
|
+
match do |result|
|
652
|
+
result.failed? && result.caused_failure?
|
653
|
+
end
|
654
|
+
|
655
|
+
failure_message do |result|
|
656
|
+
if result.failed?
|
657
|
+
"expected result to have caused failure, but it threw/received a failure instead"
|
658
|
+
else
|
659
|
+
"expected result to have caused failure, but it was not failed (status: #{result.status})"
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
failure_message_when_negated do |_result|
|
664
|
+
"expected result not to have caused failure, but it did"
|
665
|
+
end
|
666
|
+
|
667
|
+
description do
|
668
|
+
"have caused failure"
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
# Tests that a task result represents a failure that was thrown from another task
|
673
|
+
#
|
674
|
+
# This matcher verifies that a failed result was propagated from another task
|
675
|
+
# using CMDx's failure throwing mechanism. Optionally validates the original
|
676
|
+
# result that was thrown.
|
677
|
+
#
|
678
|
+
# @param [CMDx::Result, nil] expected_original_result
|
679
|
+
# Optional original result that should have been thrown
|
680
|
+
#
|
681
|
+
# @example Basic thrown failure validation
|
682
|
+
# expect(result).to have_thrown_failure
|
683
|
+
#
|
684
|
+
# @example Thrown failure with specific original result
|
685
|
+
# expect(result).to have_thrown_failure(original_failed_result)
|
686
|
+
#
|
687
|
+
# @example Negated usage (for caused failures)
|
688
|
+
# expect(result).not_to have_thrown_failure
|
689
|
+
#
|
690
|
+
# @return [Boolean] true if result failed and threw a failure from another task
|
691
|
+
#
|
692
|
+
# @since 1.0.0
|
693
|
+
RSpec::Matchers.define :have_thrown_failure do |expected_original_result = nil|
|
694
|
+
match do |result|
|
695
|
+
result.failed? &&
|
696
|
+
result.threw_failure? &&
|
697
|
+
(expected_original_result.nil? || result.threw_failure == expected_original_result)
|
698
|
+
end
|
699
|
+
|
700
|
+
failure_message do |result|
|
701
|
+
messages = []
|
702
|
+
messages << "expected result to be failed, but was #{result.status}" unless result.failed?
|
703
|
+
messages << "expected result to have thrown failure, but it #{result.caused_failure? ? 'caused' : 'received'} failure instead" unless result.threw_failure?
|
704
|
+
|
705
|
+
messages << "expected to throw failure from #{expected_original_result}, but threw from #{result.threw_failure}" if expected_original_result && result.threw_failure != expected_original_result
|
706
|
+
|
707
|
+
messages.join(", ")
|
708
|
+
end
|
709
|
+
|
710
|
+
failure_message_when_negated do |_result|
|
711
|
+
"expected result not to have thrown failure, but it did"
|
712
|
+
end
|
713
|
+
|
714
|
+
description do
|
715
|
+
desc = "have thrown failure"
|
716
|
+
desc += " from #{expected_original_result}" if expected_original_result
|
717
|
+
desc
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
# Tests that a task result represents a failure that was received from a thrown failure
|
722
|
+
#
|
723
|
+
# This matcher verifies that a failed result received a failure that was thrown
|
724
|
+
# from another task in the execution chain. This is the receiving side of the
|
725
|
+
# failure propagation mechanism.
|
726
|
+
#
|
727
|
+
# @example Basic received thrown failure validation
|
728
|
+
# expect(result).to have_received_thrown_failure
|
729
|
+
#
|
730
|
+
# @example Negated usage
|
731
|
+
# expect(result).not_to have_received_thrown_failure
|
732
|
+
#
|
733
|
+
# @return [Boolean] true if result failed and received a thrown failure
|
734
|
+
#
|
735
|
+
# @since 1.0.0
|
736
|
+
RSpec::Matchers.define :have_received_thrown_failure do
|
737
|
+
match do |result|
|
738
|
+
result.failed? && result.thrown_failure?
|
739
|
+
end
|
740
|
+
|
741
|
+
failure_message do |result|
|
742
|
+
if result.failed?
|
743
|
+
"expected result to have received thrown failure, but it #{result.caused_failure? ? 'caused' : 'threw'} failure instead"
|
744
|
+
else
|
745
|
+
"expected result to have received thrown failure, but it was not failed (status: #{result.status})"
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
749
|
+
failure_message_when_negated do |_result|
|
750
|
+
"expected result not to have received thrown failure, but it did"
|
751
|
+
end
|
752
|
+
|
753
|
+
description do
|
754
|
+
"have received thrown failure"
|
755
|
+
end
|
756
|
+
end
|
757
|
+
|
758
|
+
# Tests that a task result belongs to a specific chain
|
759
|
+
#
|
760
|
+
# This matcher verifies that a result is associated with a CMDx::Chain
|
761
|
+
# instance, optionally validating it's a specific chain object.
|
762
|
+
#
|
763
|
+
# @param [CMDx::Chain, nil] expected_chain
|
764
|
+
# Optional specific chain instance to validate against
|
765
|
+
#
|
766
|
+
# @example Basic chain membership validation
|
767
|
+
# expect(result).to belong_to_chain
|
768
|
+
#
|
769
|
+
# @example Specific chain validation
|
770
|
+
# expect(result).to belong_to_chain(my_chain)
|
771
|
+
#
|
772
|
+
# @example Negated usage
|
773
|
+
# expect(result).not_to belong_to_chain
|
774
|
+
#
|
775
|
+
# @return [Boolean] true if result belongs to a chain (optionally specific one)
|
776
|
+
#
|
777
|
+
# @since 1.0.0
|
778
|
+
RSpec::Matchers.define :belong_to_chain do |expected_chain = nil|
|
779
|
+
match do |result|
|
780
|
+
result.chain.is_a?(CMDx::Chain) &&
|
781
|
+
(expected_chain.nil? || result.chain == expected_chain)
|
782
|
+
end
|
783
|
+
|
784
|
+
failure_message do |result|
|
785
|
+
if result.chain.is_a?(CMDx::Chain)
|
786
|
+
"expected result to belong to chain #{expected_chain}, but belonged to #{result.chain}"
|
787
|
+
else
|
788
|
+
"expected result to belong to a chain, but chain was #{result.chain.class}"
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
792
|
+
failure_message_when_negated do |_result|
|
793
|
+
if expected_chain
|
794
|
+
"expected result not to belong to chain #{expected_chain}, but it did"
|
795
|
+
else
|
796
|
+
"expected result not to belong to a chain, but it did"
|
797
|
+
end
|
798
|
+
end
|
799
|
+
|
800
|
+
description do
|
801
|
+
desc = "belong to chain"
|
802
|
+
desc += " #{expected_chain}" if expected_chain
|
803
|
+
desc
|
804
|
+
end
|
805
|
+
end
|
806
|
+
|
807
|
+
# Tests that a task result has a specific chain index
|
808
|
+
#
|
809
|
+
# This matcher verifies that a result has the expected position index
|
810
|
+
# within its chain, useful for testing chain execution order and position.
|
811
|
+
#
|
812
|
+
# @param [Integer] expected_index The expected chain index (0-based)
|
813
|
+
#
|
814
|
+
# @example Basic chain index validation
|
815
|
+
# expect(result).to have_chain_index(0) # First task in chain
|
816
|
+
# expect(result).to have_chain_index(2) # Third task in chain
|
817
|
+
#
|
818
|
+
# @example Negated usage
|
819
|
+
# expect(result).not_to have_chain_index(1)
|
820
|
+
#
|
821
|
+
# @return [Boolean] true if result has the expected chain index
|
822
|
+
#
|
823
|
+
# @since 1.0.0
|
824
|
+
RSpec::Matchers.define :have_chain_index do |expected_index|
|
825
|
+
match do |result|
|
826
|
+
result.index == expected_index
|
827
|
+
end
|
828
|
+
|
829
|
+
failure_message do |result|
|
830
|
+
"expected result to have chain index #{expected_index}, but was #{result.index}"
|
831
|
+
end
|
832
|
+
|
833
|
+
failure_message_when_negated do |_result|
|
834
|
+
"expected result not to have chain index #{expected_index}, but it did"
|
835
|
+
end
|
836
|
+
|
837
|
+
description do
|
838
|
+
"have chain index #{expected_index}"
|
839
|
+
end
|
840
|
+
end
|
841
|
+
|
842
|
+
# Auto-generated predicate matchers for common result states
|
843
|
+
#
|
844
|
+
# These matchers are dynamically created for each result state and provide
|
845
|
+
# convenient boolean testing without requiring the full state name.
|
846
|
+
#
|
847
|
+
# Generated matchers:
|
848
|
+
# - be_initialized: Tests if result is initialized
|
849
|
+
# - be_executing: Tests if result is executing
|
850
|
+
# - be_complete: Tests if result is complete
|
851
|
+
# - be_interrupted: Tests if result is interrupted
|
852
|
+
#
|
853
|
+
# @example State predicate usage
|
854
|
+
# expect(result).to be_complete
|
855
|
+
# expect(result).to be_interrupted
|
856
|
+
# expect(result).not_to be_initialized
|
857
|
+
#
|
858
|
+
# @since 1.0.0
|
859
|
+
CMDx::Result::STATES.each do |state|
|
860
|
+
RSpec::Matchers.define :"be_#{state}" do
|
861
|
+
match do |result|
|
862
|
+
result.public_send(:"#{state}?")
|
863
|
+
end
|
864
|
+
|
865
|
+
failure_message do |result|
|
866
|
+
"expected result to be #{state}, but was #{result.state}"
|
867
|
+
end
|
868
|
+
|
869
|
+
failure_message_when_negated do |_result|
|
870
|
+
"expected result not to be #{state}, but it was"
|
871
|
+
end
|
872
|
+
|
873
|
+
description do
|
874
|
+
"be #{state}"
|
875
|
+
end
|
876
|
+
end
|
877
|
+
end
|
878
|
+
|
879
|
+
# Auto-generated predicate matchers for common result statuses
|
880
|
+
#
|
881
|
+
# These matchers are dynamically created for each result status and provide
|
882
|
+
# convenient boolean testing without requiring status-specific matchers.
|
883
|
+
#
|
884
|
+
# Generated matchers:
|
885
|
+
# - be_success: Tests if result has success status
|
886
|
+
# - be_skipped: Tests if result has skipped status
|
887
|
+
# - be_failed: Tests if result has failed status
|
888
|
+
#
|
889
|
+
# @example Status predicate usage
|
890
|
+
# expect(result).to be_success
|
891
|
+
# expect(result).to be_failed
|
892
|
+
# expect(result).not_to be_skipped
|
893
|
+
#
|
894
|
+
# @note These are simpler alternatives to the full outcome matchers
|
895
|
+
# (be_successful_task, be_failed_task, be_skipped_task) when you only
|
896
|
+
# need to test the status without additional validation.
|
897
|
+
#
|
898
|
+
# @since 1.0.0
|
899
|
+
CMDx::Result::STATUSES.each do |status|
|
900
|
+
RSpec::Matchers.define :"be_#{status}" do
|
901
|
+
match do |result|
|
902
|
+
result.public_send(:"#{status}?")
|
903
|
+
end
|
904
|
+
|
905
|
+
failure_message do |result|
|
906
|
+
"expected result to be #{status}, but was #{result.status}"
|
907
|
+
end
|
908
|
+
|
909
|
+
failure_message_when_negated do |_result|
|
910
|
+
"expected result not to be #{status}, but it was"
|
911
|
+
end
|
912
|
+
|
913
|
+
description do
|
914
|
+
"be #{status}"
|
915
|
+
end
|
916
|
+
end
|
917
|
+
end
|