cmdx-rspec 2.0.0 → 2.1.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/CHANGELOG.md +18 -1
- data/README.md +85 -18
- data/lib/cmdx/rspec/helpers.rb +241 -47
- data/lib/cmdx/rspec/matchers/be_ko.rb +1 -1
- data/lib/cmdx/rspec/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c236290ec6d51edc6b2f65d8f69cb8ba7c516f74436f3f6f58947ce90ddcf545
|
|
4
|
+
data.tar.gz: fe7699ab785d5554a7317ed1963b16c975e57464c780b2c8e515ea3c198b45bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 441052016d2e78837cf7f541d67a6b240464441ee78f7fc45450e702e85a3c5e0b6f3a66667b1f852133728f777b23bc8d3c3ce59f6ebfe2981348dc54c8def2
|
|
7
|
+
data.tar.gz: 9d3970eeee44f807f0b507880ca569f0e4b4c676749a5b4604a7e93a4657075b81a6d5c0ef0e2a32f66191d12fa8dd420d119ecfb1f55ad55cdbd964f8bd8d53
|
data/.DS_Store
ADDED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [2.
|
|
9
|
+
## [2.1.0] - 2026-10-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Added `expected_task_execution`, `expected_task_execution!`, `expected_no_task_execution`, and `expected_no_task_execution!` helpers for asserting on `execute` / `execute!` via `have_received` after the action
|
|
13
|
+
- Added `build_result`, `build_successful_result`, `build_skipped_result`, `build_failed_result`, and `build_echoed_result` helpers for constructing real `CMDx::Result` objects without stubbing task execution
|
|
14
|
+
- Added `stub_task` and `stub_task!` alias of `stub_task_success` and `stub_task_success!`
|
|
15
|
+
- Task stub helpers — raise `ArgumentError` when `command` is not a `CMDx::Task` subclass
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- README — document RuboCop `RSpec/NoExpectationExample` configuration for task execution mock helpers
|
|
19
|
+
- Task execution expectation helpers (`unstub_task`, `expect_task_execution`, `expected_task_execution`, and their `!` variants) — optional positional arguments are forwarded to RSpec's `.with` matcher alongside keyword context
|
|
20
|
+
- `build_echoed_result` and `stub_task_throw` — echo upstream validation now raises with message `upstream result must be a failed CMDx::Result`
|
|
21
|
+
- `stub_task_fail!` — stubbed `execute!` now raises `CMDx::Fault` (or re-raises `cause`) instead of returning a failed result, matching CMDx strict execution
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Task stub helpers — stubbed results now use the supplied command class for `result.task`
|
|
25
|
+
|
|
26
|
+
## [2.0.0] - 2026-05-05
|
|
10
27
|
|
|
11
28
|
### Added
|
|
12
29
|
- Support for CMDx v2
|
data/README.md
CHANGED
|
@@ -38,6 +38,14 @@ require "cmdx/rspec"
|
|
|
38
38
|
|
|
39
39
|
This loads every matcher under `RSpec::Matchers` and exposes the helpers under `CMDx::RSpec::Helpers`. See [Helpers](#helpers) for how to mix the helpers into your example groups.
|
|
40
40
|
|
|
41
|
+
### Relationship to the CMDx testing guide
|
|
42
|
+
|
|
43
|
+
The [CMDx testing docs](https://drexed.github.io/cmdx/testing/) use RSpec’s built-in predicate matchers on `CMDx::Result` — for example `be_success` calls `success?`, `be_failed` calls `failed?`, and `be_skipped` calls `skipped?`.
|
|
44
|
+
|
|
45
|
+
This gem adds matchers that pin both **state** and **status** via `result.to_h` (`be_successful`, `have_skipped`, `have_failed`), plus declaration matchers, chain helpers, and stub/build utilities. You can use both styles in the same suite.
|
|
46
|
+
|
|
47
|
+
For `execute!`, CMDx only raises when the outcome is **failed** (a `CMDx::Fault`, or the original exception when `result.cause` is a non-fault). Skips and successes return a result even in strict mode.
|
|
48
|
+
|
|
41
49
|
## Matchers
|
|
42
50
|
|
|
43
51
|
All result-oriented matchers raise `ArgumentError` when given a subject that isn't a `CMDx::Result`. Class-oriented matchers accept either the Task class or an instance.
|
|
@@ -46,7 +54,7 @@ All result-oriented matchers raise `ArgumentError` when given a subject that isn
|
|
|
46
54
|
|
|
47
55
|
#### `be_successful`
|
|
48
56
|
|
|
49
|
-
Asserts a `CMDx::Result` completed with `state: complete` and `status: success`. Extra keyword args are forwarded to a `result.to_h` inclusion check, so any field can be constrained inline.
|
|
57
|
+
Asserts a `CMDx::Result` completed with `state: "complete"` and `status: "success"`. Extra keyword args are forwarded to a `result.to_h` inclusion check, so any field can be constrained inline.
|
|
50
58
|
|
|
51
59
|
```ruby
|
|
52
60
|
expect(SomeTask.execute).to be_successful
|
|
@@ -55,19 +63,16 @@ expect(SomeTask.execute).to be_successful(metadata: { id: 1 })
|
|
|
55
63
|
|
|
56
64
|
#### `have_skipped`
|
|
57
65
|
|
|
58
|
-
Asserts a result was skipped (`state: interrupted`, `status: skipped`). Extra keyword args constrain other `result.to_h` fields.
|
|
66
|
+
Asserts a result was skipped (`state: "interrupted"`, `status: "skipped"`). Extra keyword args constrain other `result.to_h` fields.
|
|
59
67
|
|
|
60
68
|
```ruby
|
|
61
69
|
expect(result).to have_skipped
|
|
62
|
-
expect(result).to have_skipped(
|
|
63
|
-
reason: "out of stock",
|
|
64
|
-
cause: be_a(CMDx::SkipFault)
|
|
65
|
-
)
|
|
70
|
+
expect(result).to have_skipped(reason: "out of stock", metadata: { code: "NA" })
|
|
66
71
|
```
|
|
67
72
|
|
|
68
73
|
#### `have_failed`
|
|
69
74
|
|
|
70
|
-
Asserts a result failed (`state: interrupted`, `status: failed`). Extra keyword args constrain other `result.to_h` fields.
|
|
75
|
+
Asserts a result failed (`state: "interrupted"`, `status: "failed"`). Extra keyword args constrain other `result.to_h` fields.
|
|
71
76
|
|
|
72
77
|
```ruby
|
|
73
78
|
expect(result).to have_failed
|
|
@@ -79,7 +84,7 @@ expect(result).to have_failed(
|
|
|
79
84
|
|
|
80
85
|
#### `be_ok` / `be_ko`
|
|
81
86
|
|
|
82
|
-
`be_ok`
|
|
87
|
+
`be_ok` delegates to `Result#ok?` (success or skipped). `be_ko` delegates to `Result#ko?` (skipped or failed). Skipped results satisfy **both** matchers.
|
|
83
88
|
|
|
84
89
|
```ruby
|
|
85
90
|
expect(result).to be_ok
|
|
@@ -88,7 +93,7 @@ expect(result).to be_ko
|
|
|
88
93
|
|
|
89
94
|
#### `be_complete` / `be_interrupted`
|
|
90
95
|
|
|
91
|
-
State-only assertions. `be_complete` passes when `state ==
|
|
96
|
+
State-only assertions. `be_complete` passes when `state == "complete"`; `be_interrupted` when `state == "interrupted"`.
|
|
92
97
|
|
|
93
98
|
```ruby
|
|
94
99
|
expect(result).to be_complete
|
|
@@ -180,6 +185,8 @@ expect { SomeTask.execute! }
|
|
|
180
185
|
|
|
181
186
|
`with_reason` accepts a string (equality) or a Regexp; `with_cause` accepts a class (matches via `is_a?`) or a value (matched with `values_match?`).
|
|
182
187
|
|
|
188
|
+
In application code you can also use `CMDx::Fault.for?`, `Fault.reason?`, and `Fault.matches?` in `rescue` clauses; this matcher is the RSpec block equivalent for `execute!`.
|
|
189
|
+
|
|
183
190
|
### Chains
|
|
184
191
|
|
|
185
192
|
#### `have_chain_root`
|
|
@@ -198,6 +205,8 @@ Passes when the chain's size matches `expected`. Accepts a `CMDx::Chain` or `CMD
|
|
|
198
205
|
expect(result).to have_chain_size(3)
|
|
199
206
|
```
|
|
200
207
|
|
|
208
|
+
For failure propagation (`origin`, `caused_failure`, `threw_failure`), assert on the result directly — for example `expect(result.caused_failure.task).to eq(ValidateCard)` — as described in the [Result chain analysis](https://drexed.github.io/cmdx/outcomes/result/) docs.
|
|
209
|
+
|
|
201
210
|
### Task class declarations
|
|
202
211
|
|
|
203
212
|
These matchers introspect a Task class's configuration. They accept either the class or an instance.
|
|
@@ -220,7 +229,8 @@ Asserts the class declares the given input/output. Keyword args are matched (par
|
|
|
220
229
|
|
|
221
230
|
```ruby
|
|
222
231
|
expect(SomeTask).to have_input(:user_id)
|
|
223
|
-
expect(SomeTask).to have_input(:user_id,
|
|
232
|
+
expect(SomeTask).to have_input(:user_id, required: true)
|
|
233
|
+
expect(SomeTask).to have_input(:user_id, options: hash_including(coerce: :integer))
|
|
224
234
|
expect(SomeTask).to have_output(:total, required: true)
|
|
225
235
|
```
|
|
226
236
|
|
|
@@ -290,6 +300,30 @@ describe MyFeature do
|
|
|
290
300
|
end
|
|
291
301
|
```
|
|
292
302
|
|
|
303
|
+
### Result builders
|
|
304
|
+
|
|
305
|
+
Build a real `CMDx::Result` without stubbing `execute` — useful when a collaborator must return a result, when seeding a chain, or anywhere you need a result object in isolation. Extra keyword args (besides the documented ones) are forwarded to `task.new` as context data, same as the stubs below.
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
result = build_successful_result(snapshot:)
|
|
309
|
+
allow(FetchSnapshot).to receive(:execute).and_return(result)
|
|
310
|
+
|
|
311
|
+
build_skipped_result(reason: "not applicable")
|
|
312
|
+
build_failed_result(reason: "boom", cause: StandardError.new("kaput"))
|
|
313
|
+
build_echoed_result(upstream_failed_result)
|
|
314
|
+
|
|
315
|
+
build_result(:success, snapshot:, metadata: { id: 1 })
|
|
316
|
+
build_result(:failed, reason: "nope", strict: true, task: SomeTask)
|
|
317
|
+
build_result(:echo, upstream_result: failed_result)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Options:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
build_successful_result(task: SomeTask, chain: existing_chain, context: CMDx::Context.new(foo: 1))
|
|
324
|
+
build_result(:success, deprecated: true, retries: 2, rolled_back: true)
|
|
325
|
+
```
|
|
326
|
+
|
|
293
327
|
### Stubs
|
|
294
328
|
|
|
295
329
|
Each stub builds a frozen `CMDx::Result` carrying the requested signal and wires it into a fresh `CMDx::Chain`, so callers see realistic execution shape without invoking the task's `work`. Any extra keyword args (besides the documented ones) are forwarded to `command.new` as context overrides.
|
|
@@ -298,18 +332,18 @@ Each stub builds a frozen `CMDx::Result` carrying the requested signal and wires
|
|
|
298
332
|
|
|
299
333
|
```ruby
|
|
300
334
|
# Non-bang variants stub `SomeTask.execute`
|
|
301
|
-
stub_task_success(SomeTask)
|
|
335
|
+
stub_task_success(SomeTask) # alias: stub_task
|
|
302
336
|
stub_task_skip(SomeTask)
|
|
303
337
|
stub_task_fail(SomeTask)
|
|
304
338
|
|
|
305
339
|
# Bang variants stub `SomeTask.execute!`
|
|
306
|
-
stub_task_success!(SomeTask)
|
|
307
|
-
stub_task_skip!(SomeTask)
|
|
308
|
-
stub_task_fail!(SomeTask)
|
|
340
|
+
stub_task_success!(SomeTask) # alias: stub_task! — returns result
|
|
341
|
+
stub_task_skip!(SomeTask) # returns result (skips do not raise under execute!)
|
|
342
|
+
stub_task_fail!(SomeTask) # raises CMDx::Fault, or re-raises cause when set
|
|
309
343
|
|
|
310
344
|
# Stub a specific argument signature
|
|
311
|
-
stub_task_success(SomeTask, some: "value")
|
|
312
|
-
stub_task_skip!(SomeTask, some: "value")
|
|
345
|
+
stub_task_success(SomeTask, some: "value") # SomeTask.execute(some: "value")
|
|
346
|
+
stub_task_skip!(SomeTask, some: "value") # SomeTask.execute!(some: "value")
|
|
313
347
|
```
|
|
314
348
|
|
|
315
349
|
Common options:
|
|
@@ -343,8 +377,12 @@ stub_task_deprecated(SomeTask)
|
|
|
343
377
|
```ruby
|
|
344
378
|
stub_workflow_tasks(MyWorkflow) do |task|
|
|
345
379
|
case task
|
|
346
|
-
when
|
|
347
|
-
|
|
380
|
+
when TaskB
|
|
381
|
+
stub_task_fail(task)
|
|
382
|
+
when TaskC
|
|
383
|
+
stub_task_skip(task)
|
|
384
|
+
else
|
|
385
|
+
stub_task_success(task)
|
|
348
386
|
end
|
|
349
387
|
end
|
|
350
388
|
|
|
@@ -373,8 +411,37 @@ expect_task_execution(SomeTask, some: "value")
|
|
|
373
411
|
expect_no_task_execution(SomeTask)
|
|
374
412
|
expect_no_task_execution!(SomeTask)
|
|
375
413
|
expect_no_task_execution(SomeTask, some: "value")
|
|
414
|
+
|
|
415
|
+
# after the fact; requires a spy
|
|
416
|
+
expected_task_execution(SomeTask)
|
|
417
|
+
expected_task_execution!(SomeTask)
|
|
418
|
+
expected_task_execution(SomeTask, some: "value")
|
|
419
|
+
|
|
420
|
+
expected_no_task_execution(SomeTask)
|
|
421
|
+
expected_no_task_execution!(SomeTask)
|
|
422
|
+
expected_no_task_execution(SomeTask, some: "value")
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### RuboCop: `RSpec/NoExpectationExample`
|
|
426
|
+
|
|
427
|
+
The [RSpec/NoExpectationExample](https://docs.rubocop.org/rubocop-rspec/latest/cops_rspec.html#rspecnoexpectationexample) cop flags examples that do not call an RSpec expectation (for example `expect`, `is_expected`, or `should`). That is intentional for examples whose only assertion is a message expectation registered by [`expect_task_execution`](#mocks), [`expect_no_task_execution`](#mocks), or the bang variants — those helpers call `expect(command).to receive(...)` internally, but the example body often contains no top-level `expect`.
|
|
428
|
+
|
|
429
|
+
RuboCop already treats method names matching `^expect_` and `^assert_` as expectations via the cop’s default `AllowedPatterns`. Add `^expected_` if you use the post-action [`expected_*`](#mocks) helpers (`expected_task_execution`, `expected_no_task_execution`, and their `!` variants), which assert with `have_received` instead of `receive`.
|
|
430
|
+
|
|
431
|
+
Merge the following into `.rubocop.yml` (adjust if your file already configures this cop):
|
|
432
|
+
|
|
433
|
+
```yaml
|
|
434
|
+
RSpec/NoExpectationExample:
|
|
435
|
+
AllowedPatterns:
|
|
436
|
+
- "^expect_"
|
|
437
|
+
- "^expected_"
|
|
438
|
+
- "^assert_"
|
|
376
439
|
```
|
|
377
440
|
|
|
441
|
+
Examples that only stub or exercise behavior with an explicit matcher — for example `stub_task_success` followed by `expect(result).to be_successful` — do not need an allowed pattern. Workflow setup with `stub_workflow_tasks` is the same when you assert on outcomes afterward.
|
|
442
|
+
|
|
443
|
+
If you define your own expectation helpers, either name them with an `expect_` / `assert_` prefix or register them under `RSpec/Language/Expectations` in `.rubocop.yml` as described in the cop documentation.
|
|
444
|
+
|
|
378
445
|
### Diagnostics
|
|
379
446
|
|
|
380
447
|
#### `capture_cmdx_logs`
|
data/lib/cmdx/rspec/helpers.rb
CHANGED
|
@@ -10,6 +10,77 @@ module CMDx
|
|
|
10
10
|
# Mix into example groups via `config.include CMDx::RSpec::Helpers`.
|
|
11
11
|
module Helpers
|
|
12
12
|
|
|
13
|
+
# Anonymous task used by result builders when no +task+ is supplied.
|
|
14
|
+
STUB_TASK = Class.new(CMDx::Task) do
|
|
15
|
+
def self.name
|
|
16
|
+
"CMDx::RSpec::StubTask"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
private_constant :STUB_TASK
|
|
20
|
+
|
|
21
|
+
# Builds a frozen {CMDx::Result} without stubbing task execution.
|
|
22
|
+
# Use when a collaborator must return a result, when seeding a chain,
|
|
23
|
+
# or anywhere a real result is needed outside `stub_task_*`.
|
|
24
|
+
#
|
|
25
|
+
# @param status [Symbol, CMDx::Signal] `:success`, `:skipped`, `:failed`,
|
|
26
|
+
# `:echo`, or a pre-built signal
|
|
27
|
+
# @param reason [String, nil] human-readable outcome reason
|
|
28
|
+
# @param cause [Exception, nil] originating exception on failure
|
|
29
|
+
# @param metadata [Hash] signal metadata payload
|
|
30
|
+
# @param upstream_result [CMDx::Result] required for `:echo`; must be failed
|
|
31
|
+
# @param task [Class] task class for `result.task` (defaults to an internal stub)
|
|
32
|
+
# @param chain [CMDx::Chain, nil] chain to join (a fresh one is created when omitted)
|
|
33
|
+
# @param context [Hash, CMDx::Context, nil] task context; other keyword args
|
|
34
|
+
# are forwarded to `task.new` when +context+ is omitted
|
|
35
|
+
# @param root [Boolean] whether this result is the chain root
|
|
36
|
+
# @param strict [Boolean] whether the result was produced via `execute!`
|
|
37
|
+
# @param deprecated [Boolean] whether the result is flagged deprecated
|
|
38
|
+
# @param retries [Integer] retry count beyond the first attempt
|
|
39
|
+
# @param duration [Float, nil] execution duration in milliseconds
|
|
40
|
+
# @param rolled_back [Boolean] whether rollback ran for this result
|
|
41
|
+
# @param tid [String, nil] execution identifier (defaults to a uuid_v7)
|
|
42
|
+
# @return [CMDx::Result]
|
|
43
|
+
# @raise [ArgumentError] for unknown statuses or invalid echo input
|
|
44
|
+
# @example
|
|
45
|
+
# result = build_result(:success, snapshot:)
|
|
46
|
+
# allow(FetchSnapshot).to receive(:execute).and_return(result)
|
|
47
|
+
def build_result(status, **options)
|
|
48
|
+
signal = resolve_result_signal(
|
|
49
|
+
status,
|
|
50
|
+
reason: options[:reason],
|
|
51
|
+
cause: options[:cause],
|
|
52
|
+
metadata: options.fetch(:metadata, {}),
|
|
53
|
+
upstream_result: options[:upstream_result]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assemble_result(signal, **options.except(:reason, :cause, :metadata, :upstream_result))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [CMDx::Result] a successful result (`state: complete`, `status: success`)
|
|
60
|
+
# @see #build_result
|
|
61
|
+
def build_successful_result(metadata: {}, **context)
|
|
62
|
+
build_result(:success, metadata:, **context)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [CMDx::Result] a skipped result (`state: interrupted`, `status: skipped`)
|
|
66
|
+
# @see #build_result
|
|
67
|
+
def build_skipped_result(reason: nil, cause: nil, metadata: {}, **context)
|
|
68
|
+
build_result(:skipped, reason:, cause:, metadata:, **context)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [CMDx::Result] a failed result (`state: interrupted`, `status: failed`)
|
|
72
|
+
# @see #build_result
|
|
73
|
+
def build_failed_result(reason: nil, cause: nil, metadata: {}, **context)
|
|
74
|
+
build_result(:failed, reason:, cause:, metadata:, **context)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param upstream_result [CMDx::Result] the failed result being propagated
|
|
78
|
+
# @return [CMDx::Result] a failed result echoing +upstream_result+
|
|
79
|
+
# @see #build_result
|
|
80
|
+
def build_echoed_result(upstream_result, metadata: {}, **context)
|
|
81
|
+
build_result(:echo, upstream_result:, metadata:, **context)
|
|
82
|
+
end
|
|
83
|
+
|
|
13
84
|
# Stubs `command.execute` to return a frozen successful Result.
|
|
14
85
|
#
|
|
15
86
|
# @param command [Class] the Task class to stub
|
|
@@ -21,6 +92,7 @@ module CMDx
|
|
|
21
92
|
def stub_task_success(command, metadata: {}, **context)
|
|
22
93
|
build_stub(command, :execute, CMDx::Signal.success(nil, metadata:), context)
|
|
23
94
|
end
|
|
95
|
+
alias stub_task stub_task_success
|
|
24
96
|
|
|
25
97
|
# Stubs `command.execute!` (bang variant) to return a frozen successful Result.
|
|
26
98
|
#
|
|
@@ -31,6 +103,7 @@ module CMDx
|
|
|
31
103
|
def stub_task_success!(command, metadata: {}, **context)
|
|
32
104
|
build_stub(command, :execute!, CMDx::Signal.success(nil, metadata:), context, strict: true)
|
|
33
105
|
end
|
|
106
|
+
alias stub_task! stub_task_success!
|
|
34
107
|
|
|
35
108
|
# Stubs `command.execute` to return a frozen skipped Result.
|
|
36
109
|
#
|
|
@@ -68,14 +141,16 @@ module CMDx
|
|
|
68
141
|
build_stub(command, :execute, CMDx::Signal.failed(reason, metadata:, cause:), context)
|
|
69
142
|
end
|
|
70
143
|
|
|
71
|
-
# Stubs `command.execute!`
|
|
144
|
+
# Stubs `command.execute!` so a call raises like real strict execution:
|
|
145
|
+
# {CMDx::Fault} for signal failures, or the original exception when
|
|
146
|
+
# +cause+ is set (mirrors {CMDx::Runtime#raise_signal!}).
|
|
72
147
|
#
|
|
73
148
|
# @param command [Class] the Task class to stub
|
|
74
149
|
# @param reason [String, nil] human-readable failure reason
|
|
75
150
|
# @param cause [Exception, nil] originating cause attached to the signal
|
|
76
151
|
# @param metadata [Hash] payload exposed via `result.metadata`
|
|
77
152
|
# @param context [Hash{Symbol => Object}] context overrides forwarded to `command.new`
|
|
78
|
-
# @return [CMDx::Result] the frozen Result
|
|
153
|
+
# @return [CMDx::Result] the frozen failed Result backing the stub (for inspection)
|
|
79
154
|
def stub_task_fail!(command, reason: nil, cause: nil, metadata: {}, **context)
|
|
80
155
|
build_stub(command, :execute!, CMDx::Signal.failed(reason, metadata:, cause:), context, strict: true)
|
|
81
156
|
end
|
|
@@ -110,11 +185,7 @@ module CMDx
|
|
|
110
185
|
# @param context [Hash{Symbol => Object}]
|
|
111
186
|
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
112
187
|
def stub_task_throw(command, upstream_result, metadata: {}, **context)
|
|
113
|
-
|
|
114
|
-
raise ArgumentError,
|
|
115
|
-
"upstream_result must be a failed CMDx::Result"
|
|
116
|
-
end
|
|
117
|
-
|
|
188
|
+
validate_echo_upstream!(upstream_result)
|
|
118
189
|
build_stub(command, :execute, CMDx::Signal.echoed(upstream_result, metadata:), context)
|
|
119
190
|
end
|
|
120
191
|
|
|
@@ -142,8 +213,10 @@ module CMDx
|
|
|
142
213
|
raise ArgumentError, "block required" unless block_given?
|
|
143
214
|
|
|
144
215
|
io = StringIO.new
|
|
145
|
-
previous
|
|
146
|
-
|
|
216
|
+
previous = CMDx.configuration.logger
|
|
217
|
+
formatter = previous&.formatter || CMDx::LogFormatters::Line.new
|
|
218
|
+
|
|
219
|
+
CMDx.configuration.logger = Logger.new(io, formatter:)
|
|
147
220
|
yield
|
|
148
221
|
io.string.lines.map(&:chomp)
|
|
149
222
|
ensure
|
|
@@ -172,8 +245,8 @@ module CMDx
|
|
|
172
245
|
telemetry = command.telemetry
|
|
173
246
|
listener = ->(event) { captured << event }
|
|
174
247
|
|
|
175
|
-
events.each { |e| telemetry.subscribe(e, listener) }
|
|
176
248
|
begin
|
|
249
|
+
events.each { |e| telemetry.subscribe(e, listener) }
|
|
177
250
|
yield
|
|
178
251
|
ensure
|
|
179
252
|
events.each { |e| telemetry.unsubscribe(e, listener) }
|
|
@@ -199,12 +272,10 @@ module CMDx
|
|
|
199
272
|
|
|
200
273
|
captured = nil
|
|
201
274
|
telemetry = command.telemetry
|
|
202
|
-
listener =
|
|
203
|
-
captured ||= event.payload[:result].chain if event.root
|
|
204
|
-
end
|
|
275
|
+
listener = ->(event) { captured ||= event.payload[:result].chain if event.root }
|
|
205
276
|
|
|
206
|
-
telemetry.subscribe(:task_executed, listener)
|
|
207
277
|
begin
|
|
278
|
+
telemetry.subscribe(:task_executed, listener)
|
|
208
279
|
yield
|
|
209
280
|
ensure
|
|
210
281
|
telemetry.unsubscribe(:task_executed, listener)
|
|
@@ -219,11 +290,11 @@ module CMDx
|
|
|
219
290
|
# @param command [Class] the Task class to unstub
|
|
220
291
|
# @param context [Hash{Symbol => Object}] argument signature whose stub to release
|
|
221
292
|
# @return [void]
|
|
222
|
-
def unstub_task(command, **context)
|
|
223
|
-
if context.empty?
|
|
293
|
+
def unstub_task(command, *args, **context)
|
|
294
|
+
if args.empty? && context.empty?
|
|
224
295
|
allow(command).to receive(:execute).and_call_original
|
|
225
296
|
else
|
|
226
|
-
allow(command).to receive(:execute).with(**context).and_call_original
|
|
297
|
+
allow(command).to receive(:execute).with(*args, **context).and_call_original
|
|
227
298
|
end
|
|
228
299
|
end
|
|
229
300
|
|
|
@@ -233,11 +304,11 @@ module CMDx
|
|
|
233
304
|
# @param command [Class] the Task class to unstub
|
|
234
305
|
# @param context [Hash{Symbol => Object}] argument signature whose stub to release
|
|
235
306
|
# @return [void]
|
|
236
|
-
def unstub_task!(command, **context)
|
|
237
|
-
if context.empty?
|
|
307
|
+
def unstub_task!(command, *args, **context)
|
|
308
|
+
if args.empty? && context.empty?
|
|
238
309
|
allow(command).to receive(:execute!).and_call_original
|
|
239
310
|
else
|
|
240
|
-
allow(command).to receive(:execute!).with(**context).and_call_original
|
|
311
|
+
allow(command).to receive(:execute!).with(*args, **context).and_call_original
|
|
241
312
|
end
|
|
242
313
|
end
|
|
243
314
|
|
|
@@ -247,11 +318,11 @@ module CMDx
|
|
|
247
318
|
# @param command [Class] the Task class to expect
|
|
248
319
|
# @param context [Hash{Symbol => Object}] argument signature to match
|
|
249
320
|
# @return [RSpec::Mocks::MessageExpectation]
|
|
250
|
-
def expect_task_execution(command, **context)
|
|
251
|
-
if context.empty?
|
|
321
|
+
def expect_task_execution(command, *args, **context)
|
|
322
|
+
if args.empty? && context.empty?
|
|
252
323
|
expect(command).to receive(:execute)
|
|
253
324
|
else
|
|
254
|
-
expect(command).to receive(:execute).with(**context)
|
|
325
|
+
expect(command).to receive(:execute).with(*args, **context)
|
|
255
326
|
end
|
|
256
327
|
end
|
|
257
328
|
|
|
@@ -261,11 +332,11 @@ module CMDx
|
|
|
261
332
|
# @param command [Class] the Task class to expect
|
|
262
333
|
# @param context [Hash{Symbol => Object}] argument signature to match
|
|
263
334
|
# @return [RSpec::Mocks::MessageExpectation]
|
|
264
|
-
def expect_task_execution!(command, **context)
|
|
265
|
-
if context.empty?
|
|
335
|
+
def expect_task_execution!(command, *args, **context)
|
|
336
|
+
if args.empty? && context.empty?
|
|
266
337
|
expect(command).to receive(:execute!)
|
|
267
338
|
else
|
|
268
|
-
expect(command).to receive(:execute!).with(**context)
|
|
339
|
+
expect(command).to receive(:execute!).with(*args, **context)
|
|
269
340
|
end
|
|
270
341
|
end
|
|
271
342
|
|
|
@@ -275,11 +346,11 @@ module CMDx
|
|
|
275
346
|
# @param command [Class] the Task class to guard
|
|
276
347
|
# @param context [Hash{Symbol => Object}] argument signature to forbid
|
|
277
348
|
# @return [RSpec::Mocks::MessageExpectation]
|
|
278
|
-
def expect_no_task_execution(command, **context)
|
|
279
|
-
if context.empty?
|
|
349
|
+
def expect_no_task_execution(command, *args, **context)
|
|
350
|
+
if args.empty? && context.empty?
|
|
280
351
|
expect(command).not_to receive(:execute)
|
|
281
352
|
else
|
|
282
|
-
expect(command).not_to receive(:execute).with(**context)
|
|
353
|
+
expect(command).not_to receive(:execute).with(*args, **context)
|
|
283
354
|
end
|
|
284
355
|
end
|
|
285
356
|
|
|
@@ -289,11 +360,72 @@ module CMDx
|
|
|
289
360
|
# @param command [Class] the Task class to guard
|
|
290
361
|
# @param context [Hash{Symbol => Object}] argument signature to forbid
|
|
291
362
|
# @return [RSpec::Mocks::MessageExpectation]
|
|
292
|
-
def expect_no_task_execution!(command, **context)
|
|
293
|
-
if context.empty?
|
|
363
|
+
def expect_no_task_execution!(command, *args, **context)
|
|
364
|
+
if args.empty? && context.empty?
|
|
294
365
|
expect(command).not_to receive(:execute!)
|
|
295
366
|
else
|
|
296
|
-
expect(command).not_to receive(:execute!).with(**context)
|
|
367
|
+
expect(command).not_to receive(:execute!).with(*args, **context)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Asserts that `command.execute` was invoked (after the fact). Requires a
|
|
372
|
+
# prior spy, e.g. `allow(command).to receive(:execute).and_call_original`
|
|
373
|
+
# or a `stub_task_*` helper. When `context` is supplied, the assertion is
|
|
374
|
+
# constrained to that signature.
|
|
375
|
+
#
|
|
376
|
+
# @param command [Class] the Task class to assert against
|
|
377
|
+
# @param context [Hash{Symbol => Object}] argument signature to match
|
|
378
|
+
# @return [RSpec::Mocks::Matchers::HaveReceived]
|
|
379
|
+
def expected_task_execution(command, *args, **context)
|
|
380
|
+
if args.empty? && context.empty?
|
|
381
|
+
expect(command).to have_received(:execute)
|
|
382
|
+
else
|
|
383
|
+
expect(command).to have_received(:execute).with(*args, **context)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Asserts that `command.execute!` was invoked (after the fact). Requires a
|
|
388
|
+
# prior spy on `execute!`. When `context` is supplied, the assertion is
|
|
389
|
+
# constrained to that signature.
|
|
390
|
+
#
|
|
391
|
+
# @param command [Class] the Task class to assert against
|
|
392
|
+
# @param context [Hash{Symbol => Object}] argument signature to match
|
|
393
|
+
# @return [RSpec::Mocks::Matchers::HaveReceived]
|
|
394
|
+
def expected_task_execution!(command, *args, **context)
|
|
395
|
+
if args.empty? && context.empty?
|
|
396
|
+
expect(command).to have_received(:execute!)
|
|
397
|
+
else
|
|
398
|
+
expect(command).to have_received(:execute!).with(*args, **context)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Asserts that `command.execute` was not invoked (after the fact). Requires
|
|
403
|
+
# a prior spy on `execute`. When `context` is supplied, only the matching
|
|
404
|
+
# signature is checked.
|
|
405
|
+
#
|
|
406
|
+
# @param command [Class] the Task class to assert against
|
|
407
|
+
# @param context [Hash{Symbol => Object}] argument signature to forbid
|
|
408
|
+
# @return [RSpec::Mocks::Matchers::HaveReceived]
|
|
409
|
+
def expected_no_task_execution(command, *args, **context)
|
|
410
|
+
if args.empty? && context.empty?
|
|
411
|
+
expect(command).not_to have_received(:execute)
|
|
412
|
+
else
|
|
413
|
+
expect(command).not_to have_received(:execute).with(*args, **context)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Asserts that `command.execute!` was not invoked (after the fact). Requires
|
|
418
|
+
# a prior spy on `execute!`. When `context` is supplied, only the matching
|
|
419
|
+
# signature is checked.
|
|
420
|
+
#
|
|
421
|
+
# @param command [Class] the Task class to assert against
|
|
422
|
+
# @param context [Hash{Symbol => Object}] argument signature to forbid
|
|
423
|
+
# @return [RSpec::Mocks::Matchers::HaveReceived]
|
|
424
|
+
def expected_no_task_execution!(command, *args, **context)
|
|
425
|
+
if args.empty? && context.empty?
|
|
426
|
+
expect(command).not_to have_received(:execute!)
|
|
427
|
+
else
|
|
428
|
+
expect(command).not_to have_received(:execute!).with(*args, **context)
|
|
297
429
|
end
|
|
298
430
|
end
|
|
299
431
|
|
|
@@ -318,9 +450,66 @@ module CMDx
|
|
|
318
450
|
|
|
319
451
|
private
|
|
320
452
|
|
|
453
|
+
# @api private
|
|
454
|
+
def resolve_result_signal(status, reason:, cause:, metadata:, upstream_result:)
|
|
455
|
+
case status
|
|
456
|
+
when CMDx::Signal
|
|
457
|
+
status
|
|
458
|
+
when :success, :successful
|
|
459
|
+
CMDx::Signal.success(reason, metadata:, cause:)
|
|
460
|
+
when :skip, :skipped
|
|
461
|
+
CMDx::Signal.skipped(reason, metadata:, cause:)
|
|
462
|
+
when :fail, :failed, :failure
|
|
463
|
+
CMDx::Signal.failed(reason, metadata:, cause:)
|
|
464
|
+
when :echo, :echoed
|
|
465
|
+
validate_echo_upstream!(upstream_result)
|
|
466
|
+
CMDx::Signal.echoed(upstream_result, metadata:, cause:)
|
|
467
|
+
else
|
|
468
|
+
raise ArgumentError, "unknown result status #{status.inspect}"
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# @api private
|
|
473
|
+
def validate_echo_upstream!(upstream_result)
|
|
474
|
+
return if upstream_result.is_a?(CMDx::Result) && upstream_result.failed?
|
|
475
|
+
|
|
476
|
+
raise ArgumentError, "upstream result must be a failed CMDx::Result"
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Constructs a frozen {CMDx::Result} from +signal+ and wires it into a
|
|
480
|
+
# {CMDx::Chain} so callers observe the same shape as a real execution.
|
|
481
|
+
#
|
|
482
|
+
# @api private
|
|
483
|
+
# @return [CMDx::Result]
|
|
484
|
+
LIFECYCLE_OPTIONS = %i[root strict deprecated retries duration rolled_back tid].freeze
|
|
485
|
+
private_constant :LIFECYCLE_OPTIONS
|
|
486
|
+
|
|
487
|
+
def assemble_result(signal, task: nil, chain: nil, context: nil, **options)
|
|
488
|
+
context_data = options.except(*LIFECYCLE_OPTIONS)
|
|
489
|
+
task_class = task || STUB_TASK
|
|
490
|
+
context_input = context || context_data
|
|
491
|
+
task_instance = task_class.new(context_input)
|
|
492
|
+
chain ||= CMDx::Chain.new
|
|
493
|
+
result = CMDx::Result.new(
|
|
494
|
+
chain,
|
|
495
|
+
task_instance,
|
|
496
|
+
signal,
|
|
497
|
+
root: options.fetch(:root, true),
|
|
498
|
+
strict: options.fetch(:strict, false),
|
|
499
|
+
deprecated: options.fetch(:deprecated, false),
|
|
500
|
+
retries: options.fetch(:retries, 0),
|
|
501
|
+
duration: options.fetch(:duration, nil),
|
|
502
|
+
rolled_back: options.fetch(:rolled_back, false),
|
|
503
|
+
tid: options.fetch(:tid, SecureRandom.uuid_v7)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
chain.unshift(result)
|
|
507
|
+
|
|
508
|
+
result
|
|
509
|
+
end
|
|
510
|
+
|
|
321
511
|
# Constructs a frozen {CMDx::Result} from `signal` and stubs `command.method`
|
|
322
|
-
# to return it.
|
|
323
|
-
# observe the same shape as a real execution.
|
|
512
|
+
# to return it.
|
|
324
513
|
#
|
|
325
514
|
# @api private
|
|
326
515
|
# @param command [Class] the Task class being stubbed
|
|
@@ -328,25 +517,30 @@ module CMDx
|
|
|
328
517
|
# @param signal [CMDx::Signal] outcome payload to wrap
|
|
329
518
|
# @param context [Hash] context overrides for `command.new`
|
|
330
519
|
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
331
|
-
def build_stub(command, method, signal, context, **)
|
|
332
|
-
|
|
333
|
-
chain = CMDx::Chain.new
|
|
334
|
-
result = CMDx::Result.new(
|
|
335
|
-
chain,
|
|
336
|
-
task,
|
|
337
|
-
signal,
|
|
338
|
-
root: true,
|
|
339
|
-
id: SecureRandom.uuid_v7,
|
|
340
|
-
**
|
|
341
|
-
)
|
|
520
|
+
def build_stub(command, method, signal, context, **options)
|
|
521
|
+
raise ArgumentError, "command must be a Task class" unless command <= CMDx::Task
|
|
342
522
|
|
|
343
|
-
|
|
523
|
+
result = assemble_result(signal, **options, task: command, context:)
|
|
344
524
|
|
|
345
|
-
|
|
525
|
+
if options.fetch(:strict, false) && result.failed?
|
|
526
|
+
allow(command).to receive(method) { raise_strict_failure!(result) }
|
|
527
|
+
else
|
|
528
|
+
allow(command).to receive(method).and_return(result)
|
|
529
|
+
end
|
|
346
530
|
|
|
347
531
|
result
|
|
348
532
|
end
|
|
349
533
|
|
|
534
|
+
# @api private
|
|
535
|
+
# @param result [CMDx::Result] a failed strict result
|
|
536
|
+
# @raise [CMDx::Fault, StandardError]
|
|
537
|
+
def raise_strict_failure!(result)
|
|
538
|
+
cause = result.cause
|
|
539
|
+
raise(cause) if cause && !cause.is_a?(CMDx::Fault)
|
|
540
|
+
|
|
541
|
+
raise(CMDx::Fault, result.caused_failure, cause:)
|
|
542
|
+
end
|
|
543
|
+
|
|
350
544
|
end
|
|
351
545
|
end
|
|
352
546
|
end
|
data/lib/cmdx/rspec/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cmdx-rspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Juan Gomez
|
|
@@ -128,6 +128,7 @@ executables: []
|
|
|
128
128
|
extensions: []
|
|
129
129
|
extra_rdoc_files: []
|
|
130
130
|
files:
|
|
131
|
+
- ".DS_Store"
|
|
131
132
|
- CHANGELOG.md
|
|
132
133
|
- CODE_OF_CONDUCT.md
|
|
133
134
|
- LICENSE.txt
|
|
@@ -187,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
187
188
|
- !ruby/object:Gem::Version
|
|
188
189
|
version: '0'
|
|
189
190
|
requirements: []
|
|
190
|
-
rubygems_version: 4.0.
|
|
191
|
+
rubygems_version: 4.0.21
|
|
191
192
|
specification_version: 4
|
|
192
193
|
summary: Simple CMDx task testing via RSpec matchers.
|
|
193
194
|
test_files: []
|