cmdx 0.4.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 +42 -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 +212 -19
- data/docs/outcomes/statuses.md +284 -18
- 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 +399 -20
- 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 +409 -34
- 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 -59
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -34
- data/lib/cmdx/run.rb +0 -38
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -16
- 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,570 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Custom RSpec matchers for CMDx task behavior testing
|
4
|
+
#
|
5
|
+
# This module provides specialized matchers for testing task classes and their
|
6
|
+
# behavior rather than execution results. These matchers focus on task structure,
|
7
|
+
# parameter validation, middleware composition, callback registration, and lifecycle
|
8
|
+
# management following RSpec Style Guide conventions.
|
9
|
+
#
|
10
|
+
# The matchers are designed to test task classes before execution, validating
|
11
|
+
# their configuration, behavior patterns, and architectural compliance.
|
12
|
+
#
|
13
|
+
# @example Parameter validation testing
|
14
|
+
# expect(TaskClass).to validate_required_parameter(:user_id)
|
15
|
+
# expect(TaskClass).to validate_parameter_type(:count, :integer)
|
16
|
+
# expect(TaskClass).to use_default_value(:timeout, 30)
|
17
|
+
#
|
18
|
+
# @example Middleware and callback testing
|
19
|
+
# expect(TaskClass).to have_middleware(LoggingMiddleware)
|
20
|
+
# expect(TaskClass).to have_callback(:before_validation)
|
21
|
+
# expect(TaskClass).to execute_callbacks(:before_validation, :on_success)
|
22
|
+
#
|
23
|
+
# @example Task lifecycle and behavior testing
|
24
|
+
# expect(TaskClass).to be_well_formed_task
|
25
|
+
# expect(TaskClass).to handle_exceptions_gracefully
|
26
|
+
#
|
27
|
+
# @see https://rspec.rubystyle.guide/ RSpec Style Guide
|
28
|
+
# @since 1.0.0
|
29
|
+
|
30
|
+
# Tests that a task class validates a required parameter
|
31
|
+
#
|
32
|
+
# This matcher verifies that a task properly validates the presence of
|
33
|
+
# a required parameter by calling the task without the parameter and
|
34
|
+
# ensuring it fails with an appropriate validation message.
|
35
|
+
#
|
36
|
+
# @param [Symbol] parameter_name The name of the required parameter to test
|
37
|
+
#
|
38
|
+
# @example Basic required parameter validation
|
39
|
+
# expect(CreateUserTask).to validate_required_parameter(:email)
|
40
|
+
# expect(ProcessOrderTask).to validate_required_parameter(:order_id)
|
41
|
+
#
|
42
|
+
# @example Negated usage
|
43
|
+
# expect(OptionalTask).not_to validate_required_parameter(:optional_field)
|
44
|
+
#
|
45
|
+
# @return [Boolean] true if task fails validation when parameter is missing
|
46
|
+
#
|
47
|
+
# @since 1.0.0
|
48
|
+
RSpec::Matchers.define :validate_required_parameter do |parameter_name|
|
49
|
+
match do |task_class|
|
50
|
+
result = task_class.call
|
51
|
+
result.failed? &&
|
52
|
+
result.metadata[:reason]&.include?("#{parameter_name} is a required parameter")
|
53
|
+
end
|
54
|
+
|
55
|
+
failure_message do |task_class|
|
56
|
+
result = task_class.call
|
57
|
+
if result.success?
|
58
|
+
"expected task to fail validation for required parameter #{parameter_name}, but it succeeded"
|
59
|
+
elsif result.failed?
|
60
|
+
"expected task to fail with message about required parameter #{parameter_name}, but failed with: #{result.metadata[:reason]}"
|
61
|
+
else
|
62
|
+
"expected task to fail validation for required parameter #{parameter_name}, but was #{result.status}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
failure_message_when_negated do |_task_class|
|
67
|
+
"expected task not to validate required parameter #{parameter_name}, but it did"
|
68
|
+
end
|
69
|
+
|
70
|
+
description do
|
71
|
+
"validate required parameter #{parameter_name}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Tests that a task class validates parameter type coercion
|
76
|
+
#
|
77
|
+
# This matcher verifies that a task properly validates parameter types by
|
78
|
+
# passing an invalid type value and ensuring the task fails with an
|
79
|
+
# appropriate type validation message.
|
80
|
+
#
|
81
|
+
# @param [Symbol] parameter_name The name of the parameter to test
|
82
|
+
# @param [Symbol] expected_type The expected parameter type (:integer, :string, :boolean, etc.)
|
83
|
+
#
|
84
|
+
# @example Basic type validation testing
|
85
|
+
# expect(CreateUserTask).to validate_parameter_type(:age, :integer)
|
86
|
+
# expect(UpdateSettingsTask).to validate_parameter_type(:enabled, :boolean)
|
87
|
+
# expect(SearchTask).to validate_parameter_type(:filters, :hash)
|
88
|
+
#
|
89
|
+
# @example Negated usage
|
90
|
+
# expect(FlexibleTask).not_to validate_parameter_type(:flexible_param, :string)
|
91
|
+
#
|
92
|
+
# @return [Boolean] true if task fails validation when invalid type is provided
|
93
|
+
#
|
94
|
+
# @since 1.0.0
|
95
|
+
RSpec::Matchers.define :validate_parameter_type do |parameter_name, expected_type|
|
96
|
+
match do |task_class|
|
97
|
+
# Test with invalid type - use string when expecting integer, etc.
|
98
|
+
invalid_value = case expected_type
|
99
|
+
when :integer then "not_an_integer"
|
100
|
+
when :string then 123
|
101
|
+
when :boolean then "not_a_boolean"
|
102
|
+
when :hash then "not_a_hash"
|
103
|
+
when :array then "not_an_array"
|
104
|
+
else "invalid_value"
|
105
|
+
end
|
106
|
+
|
107
|
+
result = task_class.call(parameter_name => invalid_value)
|
108
|
+
result.failed? &&
|
109
|
+
result.metadata[:reason]&.include?("#{parameter_name} must be a #{expected_type}")
|
110
|
+
end
|
111
|
+
|
112
|
+
failure_message do |task_class|
|
113
|
+
invalid_value = case expected_type
|
114
|
+
when :integer then "not_an_integer"
|
115
|
+
when :string then 123
|
116
|
+
when :boolean then "not_a_boolean"
|
117
|
+
when :hash then "not_a_hash"
|
118
|
+
when :array then "not_an_array"
|
119
|
+
else "invalid_value"
|
120
|
+
end
|
121
|
+
|
122
|
+
result = task_class.call(parameter_name => invalid_value)
|
123
|
+
if result.success?
|
124
|
+
"expected task to fail type validation for parameter #{parameter_name} (#{expected_type}), but it succeeded"
|
125
|
+
elsif result.failed?
|
126
|
+
"expected task to fail with type validation message for #{parameter_name} (#{expected_type}), but failed with: #{result.metadata[:reason]}"
|
127
|
+
else
|
128
|
+
"expected task to fail type validation for parameter #{parameter_name} (#{expected_type}), but was #{result.status}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
failure_message_when_negated do |_task_class|
|
133
|
+
"expected task not to validate parameter type #{parameter_name} (#{expected_type}), but it did"
|
134
|
+
end
|
135
|
+
|
136
|
+
description do
|
137
|
+
"validate parameter type #{parameter_name} (#{expected_type})"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Tests that a task class uses a specific default value for a parameter
|
142
|
+
#
|
143
|
+
# This matcher verifies that when a parameter is not provided, the task
|
144
|
+
# uses the expected default value by calling the task without the parameter
|
145
|
+
# and checking the context contains the default value.
|
146
|
+
#
|
147
|
+
# @param [Symbol] parameter_name The name of the parameter to test
|
148
|
+
# @param [Object] default_value The expected default value
|
149
|
+
#
|
150
|
+
# @example Basic default value testing
|
151
|
+
# expect(ProcessTask).to use_default_value(:timeout, 30)
|
152
|
+
# expect(EmailTask).to use_default_value(:priority, "normal")
|
153
|
+
# expect(ConfigTask).to use_default_value(:enabled, true)
|
154
|
+
#
|
155
|
+
# @example Negated usage
|
156
|
+
# expect(RequiredParamTask).not_to use_default_value(:required_field, nil)
|
157
|
+
#
|
158
|
+
# @return [Boolean] true if task uses the expected default value
|
159
|
+
#
|
160
|
+
# @since 1.0.0
|
161
|
+
RSpec::Matchers.define :use_default_value do |parameter_name, default_value|
|
162
|
+
match do |task_class|
|
163
|
+
result = task_class.call
|
164
|
+
result.success? &&
|
165
|
+
result.context.public_send(parameter_name) == default_value
|
166
|
+
end
|
167
|
+
|
168
|
+
failure_message do |task_class|
|
169
|
+
result = task_class.call
|
170
|
+
if result.failed?
|
171
|
+
"expected task to use default value #{default_value} for #{parameter_name}, but task failed: #{result.metadata[:reason]}"
|
172
|
+
else
|
173
|
+
actual_value = result.context.public_send(parameter_name)
|
174
|
+
"expected task to use default value #{default_value} for #{parameter_name}, but was #{actual_value}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
failure_message_when_negated do |_task_class|
|
179
|
+
"expected task not to use default value #{default_value} for #{parameter_name}, but it did"
|
180
|
+
end
|
181
|
+
|
182
|
+
description do
|
183
|
+
"use default value #{default_value} for parameter #{parameter_name}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Tests that a task class has a specific middleware registered
|
188
|
+
#
|
189
|
+
# This matcher verifies that a task has registered a specific middleware
|
190
|
+
# class in its middleware registry, ensuring proper middleware composition.
|
191
|
+
#
|
192
|
+
# @param [Class] middleware_class The middleware class to check for
|
193
|
+
#
|
194
|
+
# @example Basic middleware testing
|
195
|
+
# expect(AuthenticatedTask).to have_middleware(AuthenticationMiddleware)
|
196
|
+
# expect(LoggedTask).to have_middleware(LoggingMiddleware)
|
197
|
+
# expect(TimedTask).to have_middleware(TimeoutMiddleware)
|
198
|
+
#
|
199
|
+
# @example Negated usage
|
200
|
+
# expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
|
201
|
+
#
|
202
|
+
# @return [Boolean] true if task has the specified middleware registered
|
203
|
+
#
|
204
|
+
# @since 1.0.0
|
205
|
+
RSpec::Matchers.define :have_middleware do |middleware_class|
|
206
|
+
match do |task_class|
|
207
|
+
task_class.cmd_middlewares.any? do |middleware|
|
208
|
+
middleware.is_a?(middleware_class) || middleware.instance_of?(middleware_class)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
failure_message do |task_class|
|
213
|
+
middleware_classes = task_class.cmd_middlewares.map(&:class)
|
214
|
+
"expected task to have middleware #{middleware_class}, but had #{middleware_classes}"
|
215
|
+
end
|
216
|
+
|
217
|
+
failure_message_when_negated do |_task_class|
|
218
|
+
"expected task not to have middleware #{middleware_class}, but it did"
|
219
|
+
end
|
220
|
+
|
221
|
+
description do
|
222
|
+
"have middleware #{middleware_class}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Tests that a task class has a specific callback registered
|
227
|
+
#
|
228
|
+
# This matcher verifies that a task has registered a callback for a specific
|
229
|
+
# lifecycle event. Optionally validates the callback uses a specific callable.
|
230
|
+
#
|
231
|
+
# @param [Symbol] callback_name The name of the callback to check for
|
232
|
+
#
|
233
|
+
# @example Basic callback testing
|
234
|
+
# expect(ValidatedTask).to have_callback(:before_validation)
|
235
|
+
# expect(NotifiedTask).to have_callback(:on_success)
|
236
|
+
# expect(CleanupTask).to have_callback(:after_execution)
|
237
|
+
#
|
238
|
+
# @example Callback with specific callable
|
239
|
+
# expect(CustomTask).to have_callback(:on_failure).with_callable(my_proc)
|
240
|
+
#
|
241
|
+
# @example Negated usage
|
242
|
+
# expect(SimpleTask).not_to have_callback(:complex_callback)
|
243
|
+
#
|
244
|
+
# @return [Boolean] true if task has the specified callback registered
|
245
|
+
#
|
246
|
+
# @since 1.0.0
|
247
|
+
RSpec::Matchers.define :have_callback do |callback_name|
|
248
|
+
match do |task_class|
|
249
|
+
task_class.cmd_callbacks.registered?(callback_name)
|
250
|
+
end
|
251
|
+
|
252
|
+
chain :with_callable do |callable|
|
253
|
+
@expected_callable = callable
|
254
|
+
end
|
255
|
+
|
256
|
+
match do |task_class|
|
257
|
+
callbacks_registered = task_class.cmd_callbacks.registered?(callback_name)
|
258
|
+
return false unless callbacks_registered
|
259
|
+
|
260
|
+
if @expected_callable
|
261
|
+
task_class.cmd_callbacks.find(callback_name).any? do |callback|
|
262
|
+
callback.callable == @expected_callable
|
263
|
+
end
|
264
|
+
else
|
265
|
+
true
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
failure_message do |task_class|
|
270
|
+
if @expected_callable
|
271
|
+
"expected task to have callback #{callback_name} with callable #{@expected_callable}, but it didn't"
|
272
|
+
else
|
273
|
+
registered_callbacks = task_class.cmd_callbacks.registered_callbacks
|
274
|
+
"expected task to have callback #{callback_name}, but had #{registered_callbacks}"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
failure_message_when_negated do |_task_class|
|
279
|
+
if @expected_callable
|
280
|
+
"expected task not to have callback #{callback_name} with callable #{@expected_callable}, but it did"
|
281
|
+
else
|
282
|
+
"expected task not to have callback #{callback_name}, but it did"
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
description do
|
287
|
+
desc = "have callback #{callback_name}"
|
288
|
+
desc += " with callable #{@expected_callable}" if @expected_callable
|
289
|
+
desc
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Tests that a task executes specific callbacks during its lifecycle
|
294
|
+
#
|
295
|
+
# This matcher verifies that when a task is executed, it triggers the
|
296
|
+
# expected callbacks in the proper sequence. Works by mocking callback execution
|
297
|
+
# and tracking which callbacks are called.
|
298
|
+
#
|
299
|
+
# @param [Array<Symbol>] callback_names The names of callbacks that should be executed
|
300
|
+
#
|
301
|
+
# @example Basic callback execution testing
|
302
|
+
# expect(task).to execute_callbacks(:before_validation, :after_validation)
|
303
|
+
# expect(failed_task).to execute_callbacks(:before_execution, :on_failure)
|
304
|
+
#
|
305
|
+
# @example Single callback execution
|
306
|
+
# expect(simple_task).to execute_callbacks(:on_success)
|
307
|
+
#
|
308
|
+
# @example Negated usage
|
309
|
+
# expect(task).not_to execute_callbacks(:unused_callback)
|
310
|
+
#
|
311
|
+
# @note This matcher requires the task to be executed and may mock internal
|
312
|
+
# callback execution mechanisms for testing purposes.
|
313
|
+
#
|
314
|
+
# @return [Boolean] true if task executes all specified callbacks
|
315
|
+
#
|
316
|
+
# @since 1.0.0
|
317
|
+
RSpec::Matchers.define :execute_callbacks do |*callback_names|
|
318
|
+
match do |task_or_result|
|
319
|
+
@executed_callbacks = []
|
320
|
+
|
321
|
+
# Mock the callback execution to track what gets called
|
322
|
+
if task_or_result.is_a?(CMDx::Task)
|
323
|
+
task = task_or_result
|
324
|
+
original_callback_call = task.cmd_callbacks.method(:call)
|
325
|
+
|
326
|
+
allow(task.cmd_callbacks).to receive(:call) do |task_instance, callback_name|
|
327
|
+
@executed_callbacks << callback_name
|
328
|
+
original_callback_call.call(task_instance, callback_name)
|
329
|
+
end
|
330
|
+
|
331
|
+
task.perform
|
332
|
+
else
|
333
|
+
# If it's a result, check if callbacks were executed during task execution
|
334
|
+
result = task_or_result
|
335
|
+
# This would require the callbacks to be tracked during execution
|
336
|
+
# For now, assume callbacks were executed based on result state
|
337
|
+
@executed_callbacks = infer_executed_callbacks(result)
|
338
|
+
end
|
339
|
+
|
340
|
+
callback_names.all? { |callback_name| @executed_callbacks.include?(callback_name) }
|
341
|
+
end
|
342
|
+
|
343
|
+
failure_message do |_task_or_result|
|
344
|
+
missing_callbacks = callback_names - @executed_callbacks
|
345
|
+
"expected to execute callbacks #{callback_names}, but missing #{missing_callbacks}. Executed: #{@executed_callbacks}"
|
346
|
+
end
|
347
|
+
|
348
|
+
failure_message_when_negated do |_task_or_result|
|
349
|
+
"expected not to execute callbacks #{callback_names}, but executed #{@executed_callbacks & callback_names}"
|
350
|
+
end
|
351
|
+
|
352
|
+
description do
|
353
|
+
"execute callbacks #{callback_names}"
|
354
|
+
end
|
355
|
+
|
356
|
+
private
|
357
|
+
|
358
|
+
def infer_executed_callbacks(result)
|
359
|
+
callbacks = []
|
360
|
+
callbacks << :before_validation if result.executed?
|
361
|
+
callbacks << :after_validation if result.executed?
|
362
|
+
callbacks << :before_execution if result.executed?
|
363
|
+
callbacks << :after_execution if result.executed?
|
364
|
+
callbacks << :on_executed if result.executed?
|
365
|
+
callbacks << :"on_#{result.status}" if result.executed?
|
366
|
+
callbacks << :on_good if result.good?
|
367
|
+
callbacks << :on_bad if result.bad?
|
368
|
+
callbacks << :"on_#{result.state}" if result.executed?
|
369
|
+
callbacks
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Tests that a task handles exceptions gracefully by converting them to failed results
|
374
|
+
#
|
375
|
+
# This matcher verifies that when a task raises an exception during execution,
|
376
|
+
# it catches the exception and converts it to a failed result with appropriate
|
377
|
+
# metadata rather than allowing the exception to propagate.
|
378
|
+
#
|
379
|
+
# @example Basic exception handling testing
|
380
|
+
# expect(RobustTask).to handle_exceptions_gracefully
|
381
|
+
#
|
382
|
+
# @example Negated usage (for exception-propagating tasks)
|
383
|
+
# expect(StrictTask).not_to handle_exceptions_gracefully
|
384
|
+
#
|
385
|
+
# @return [Boolean] true if task converts exceptions to failed results
|
386
|
+
#
|
387
|
+
# @since 1.0.0
|
388
|
+
RSpec::Matchers.define :handle_exceptions_gracefully do
|
389
|
+
match do |task_class|
|
390
|
+
# Test that exceptions are caught and converted to failed results
|
391
|
+
erroring_task = Class.new(task_class) do
|
392
|
+
def call
|
393
|
+
raise StandardError, "Test error"
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
task = erroring_task.new
|
398
|
+
task.perform
|
399
|
+
|
400
|
+
task.result.failed? &&
|
401
|
+
task.result.metadata[:reason]&.include?("Test error") &&
|
402
|
+
task.result.metadata[:original_exception].is_a?(StandardError)
|
403
|
+
end
|
404
|
+
|
405
|
+
failure_message do |_task_class|
|
406
|
+
"expected task to handle exceptions gracefully by converting to failed results, but it didn't"
|
407
|
+
end
|
408
|
+
|
409
|
+
failure_message_when_negated do |_task_class|
|
410
|
+
"expected task not to handle exceptions gracefully, but it did"
|
411
|
+
end
|
412
|
+
|
413
|
+
description do
|
414
|
+
"handle exceptions gracefully"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Tests that a task propagates exceptions when called with the bang method
|
419
|
+
#
|
420
|
+
# This matcher verifies that when using the call! method instead of call,
|
421
|
+
# exceptions are allowed to propagate rather than being converted to failed
|
422
|
+
# results, enabling fail-fast behavior when desired.
|
423
|
+
#
|
424
|
+
# @example Basic exception propagation testing
|
425
|
+
# expect(MyTask).to propagate_exceptions_with_bang
|
426
|
+
#
|
427
|
+
# @example Negated usage (for always-graceful tasks)
|
428
|
+
# expect(AlwaysGracefulTask).not_to propagate_exceptions_with_bang
|
429
|
+
#
|
430
|
+
# @return [Boolean] true if task propagates exceptions with call!
|
431
|
+
#
|
432
|
+
# @since 1.0.0
|
433
|
+
RSpec::Matchers.define :propagate_exceptions_with_bang do
|
434
|
+
match do |task_class|
|
435
|
+
# Test that call! propagates exceptions instead of handling them
|
436
|
+
erroring_task = Class.new(task_class) do
|
437
|
+
def call
|
438
|
+
raise StandardError, "Test error"
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
begin
|
443
|
+
erroring_task.call!
|
444
|
+
false # Should not reach here
|
445
|
+
rescue StandardError => e
|
446
|
+
e.message == "Test error"
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
failure_message do |_task_class|
|
451
|
+
"expected task to propagate exceptions with call!, but it didn't"
|
452
|
+
end
|
453
|
+
|
454
|
+
failure_message_when_negated do |_task_class|
|
455
|
+
"expected task not to propagate exceptions with call!, but it did"
|
456
|
+
end
|
457
|
+
|
458
|
+
description do
|
459
|
+
"propagate exceptions with call!"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# Tests that a task class has a specific configuration setting
|
464
|
+
#
|
465
|
+
# This matcher verifies that a task has a particular configuration setting
|
466
|
+
# defined, optionally validating the setting's value. Task settings control
|
467
|
+
# various aspects of task behavior and execution.
|
468
|
+
#
|
469
|
+
# @param [Symbol] setting_name The name of the setting to check for
|
470
|
+
# @param [Object, nil] expected_value Optional expected value for the setting
|
471
|
+
#
|
472
|
+
# @example Basic setting presence testing
|
473
|
+
# expect(ConfiguredTask).to have_task_setting(:timeout)
|
474
|
+
# expect(CustomTask).to have_task_setting(:priority)
|
475
|
+
#
|
476
|
+
# @example Setting with specific value
|
477
|
+
# expect(TimedTask).to have_task_setting(:timeout, 30)
|
478
|
+
# expect(PriorityTask).to have_task_setting(:priority, "high")
|
479
|
+
#
|
480
|
+
# @example Negated usage
|
481
|
+
# expect(SimpleTask).not_to have_task_setting(:complex_setting)
|
482
|
+
#
|
483
|
+
# @return [Boolean] true if task has the setting (with expected value if provided)
|
484
|
+
#
|
485
|
+
# @since 1.0.0
|
486
|
+
RSpec::Matchers.define :have_task_setting do |setting_name, expected_value = nil|
|
487
|
+
match do |task_class|
|
488
|
+
return false unless task_class.task_setting?(setting_name)
|
489
|
+
|
490
|
+
if expected_value
|
491
|
+
task_class.task_setting(setting_name) == expected_value
|
492
|
+
else
|
493
|
+
true
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
failure_message do |task_class|
|
498
|
+
if expected_value
|
499
|
+
actual_value = task_class.task_setting(setting_name)
|
500
|
+
"expected task to have setting #{setting_name} with value #{expected_value}, but was #{actual_value}"
|
501
|
+
else
|
502
|
+
available_settings = task_class.task_settings.keys
|
503
|
+
"expected task to have setting #{setting_name}, but had #{available_settings}"
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
failure_message_when_negated do |_task_class|
|
508
|
+
if expected_value
|
509
|
+
"expected task not to have setting #{setting_name} with value #{expected_value}, but it did"
|
510
|
+
else
|
511
|
+
"expected task not to have setting #{setting_name}, but it did"
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
description do
|
516
|
+
desc = "have task setting #{setting_name}"
|
517
|
+
desc += " with value #{expected_value}" if expected_value
|
518
|
+
desc
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# Tests that a task class is well-formed and follows CMDx conventions
|
523
|
+
#
|
524
|
+
# This composite matcher verifies that a task class meets all the basic
|
525
|
+
# structural requirements for a valid CMDx task, including proper inheritance,
|
526
|
+
# required method implementation, and registry initialization.
|
527
|
+
#
|
528
|
+
# Validates that the task:
|
529
|
+
# - Inherits from CMDx::Task
|
530
|
+
# - Implements the required call method
|
531
|
+
# - Has properly initialized parameter, callback, and middleware registries
|
532
|
+
#
|
533
|
+
# @example Basic well-formed task validation
|
534
|
+
# expect(MyTask).to be_well_formed_task
|
535
|
+
# expect(UserCreationTask).to be_well_formed_task
|
536
|
+
#
|
537
|
+
# @example Negated usage (for malformed tasks)
|
538
|
+
# expect(BrokenTask).not_to be_well_formed_task
|
539
|
+
#
|
540
|
+
# @return [Boolean] true if task meets all structural requirements
|
541
|
+
#
|
542
|
+
# @since 1.0.0
|
543
|
+
RSpec::Matchers.define :be_well_formed_task do
|
544
|
+
match do |task_class|
|
545
|
+
task_class < CMDx::Task &&
|
546
|
+
task_class.instance_methods.include?(:call) &&
|
547
|
+
task_class.cmd_parameters.is_a?(CMDx::ParameterRegistry) &&
|
548
|
+
task_class.cmd_callbacks.is_a?(CMDx::CallbackRegistry) &&
|
549
|
+
task_class.cmd_middlewares.is_a?(CMDx::MiddlewareRegistry)
|
550
|
+
end
|
551
|
+
|
552
|
+
failure_message do |task_class|
|
553
|
+
issues = []
|
554
|
+
issues << "does not inherit from CMDx::Task" unless task_class < CMDx::Task
|
555
|
+
issues << "does not implement call method" unless task_class.instance_methods.include?(:call)
|
556
|
+
issues << "does not have parameter registry" unless task_class.cmd_parameters.is_a?(CMDx::ParameterRegistry)
|
557
|
+
issues << "does not have callback registry" unless task_class.cmd_callbacks.is_a?(CMDx::CallbackRegistry)
|
558
|
+
issues << "does not have middleware registry" unless task_class.cmd_middlewares.is_a?(CMDx::MiddlewareRegistry)
|
559
|
+
|
560
|
+
"expected task to be well-formed, but #{issues.join(', ')}"
|
561
|
+
end
|
562
|
+
|
563
|
+
failure_message_when_negated do |_task_class|
|
564
|
+
"expected task not to be well-formed, but it was"
|
565
|
+
end
|
566
|
+
|
567
|
+
description do
|
568
|
+
"be a well-formed task"
|
569
|
+
end
|
570
|
+
end
|