cmdx-rspec 1.3.0 → 2.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/CHANGELOG.md +13 -0
- data/README.md +279 -156
- data/lib/cmdx/rspec/helpers.rb +257 -259
- data/lib/cmdx/rspec/matchers/be_complete.rb +20 -0
- data/lib/cmdx/rspec/matchers/be_deprecated.rb +14 -52
- data/lib/cmdx/rspec/matchers/be_interrupted.rb +20 -0
- data/lib/cmdx/rspec/matchers/be_ko.rb +19 -0
- data/lib/cmdx/rspec/matchers/be_ok.rb +19 -0
- data/lib/cmdx/rspec/matchers/be_successful.rb +8 -20
- data/lib/cmdx/rspec/matchers/have_been_retried.rb +41 -0
- data/lib/cmdx/rspec/matchers/have_been_rolled_back.rb +23 -0
- data/lib/cmdx/rspec/matchers/have_callback.rb +46 -0
- data/lib/cmdx/rspec/matchers/have_chain_root.rb +30 -0
- data/lib/cmdx/rspec/matchers/have_chain_size.rb +29 -0
- data/lib/cmdx/rspec/matchers/have_duration.rb +30 -0
- data/lib/cmdx/rspec/matchers/have_empty_context.rb +4 -16
- data/lib/cmdx/rspec/matchers/have_empty_metadata.rb +3 -9
- data/lib/cmdx/rspec/matchers/have_errors_on.rb +50 -0
- data/lib/cmdx/rspec/matchers/have_failed.rb +7 -24
- data/lib/cmdx/rspec/matchers/have_input.rb +41 -0
- data/lib/cmdx/rspec/matchers/have_matching_context.rb +5 -20
- data/lib/cmdx/rspec/matchers/have_matching_metadata.rb +5 -17
- data/lib/cmdx/rspec/matchers/have_middleware.rb +29 -0
- data/lib/cmdx/rspec/matchers/have_no_errors.rb +30 -0
- data/lib/cmdx/rspec/matchers/have_output.rb +46 -0
- data/lib/cmdx/rspec/matchers/have_pipeline_tasks.rb +27 -0
- data/lib/cmdx/rspec/matchers/have_retry_on.rb +36 -0
- data/lib/cmdx/rspec/matchers/have_skipped.rb +7 -24
- data/lib/cmdx/rspec/matchers/have_tag.rb +30 -0
- data/lib/cmdx/rspec/matchers/raise_cmdx_fault.rb +74 -0
- data/lib/cmdx/rspec/version.rb +2 -1
- data/lib/cmdx/rspec.rb +22 -3
- metadata +24 -15
- data/.cursor/prompts/docs.md +0 -12
- data/.cursor/prompts/llms.md +0 -20
- data/.cursor/prompts/rspec.md +0 -24
- data/.cursor/prompts/yardoc.md +0 -14
- data/.cursor/rules/cursor-instructions.mdc +0 -62
- data/.rspec +0 -4
- data/.rubocop.yml +0 -64
- data/src/cmdx-dark-logo.png +0 -0
- data/src/cmdx-light-logo.png +0 -0
- data/src/cmdx-logo.svg +0 -1
data/lib/cmdx/rspec/helpers.rb
CHANGED
|
@@ -2,225 +2,223 @@
|
|
|
2
2
|
|
|
3
3
|
module CMDx
|
|
4
4
|
module RSpec
|
|
5
|
-
#
|
|
5
|
+
# RSpec helpers for stubbing and asserting Task execution. Each helper
|
|
6
|
+
# builds a frozen {CMDx::Result} carrying the requested {CMDx::Signal}
|
|
7
|
+
# and wires it into a fresh {CMDx::Chain} so callers see realistic
|
|
8
|
+
# execution output without invoking the Task's `work`.
|
|
9
|
+
#
|
|
10
|
+
# Mix into example groups via `config.include CMDx::RSpec::Helpers`.
|
|
6
11
|
module Helpers
|
|
7
12
|
|
|
8
|
-
#
|
|
13
|
+
# Stubs `command.execute` to return a frozen successful Result.
|
|
9
14
|
#
|
|
10
|
-
# @param command [Class]
|
|
11
|
-
# @param metadata [Hash]
|
|
12
|
-
# @param context [Hash
|
|
13
|
-
#
|
|
14
|
-
# @
|
|
15
|
-
#
|
|
16
|
-
# @example Stubbing successful execution with context
|
|
17
|
-
# stub_task_success(MyCommand, user_id: 123, role: "admin")
|
|
18
|
-
#
|
|
19
|
-
# result = MyCommand.execute(user_id: 123, role: "admin")
|
|
20
|
-
# expect(result).to be_successful
|
|
21
|
-
#
|
|
22
|
-
# @example Stubbing successful execution without context
|
|
23
|
-
# stub_task_success(MyCommand)
|
|
24
|
-
#
|
|
25
|
-
# result = MyCommand.execute
|
|
26
|
-
# expect(result).to be_successful
|
|
15
|
+
# @param command [Class] the Task class to stub
|
|
16
|
+
# @param metadata [Hash] payload exposed via `result.metadata`
|
|
17
|
+
# @param context [Hash{Symbol => Object}] context overrides forwarded to `command.new`
|
|
18
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
19
|
+
# @example
|
|
20
|
+
# stub_task_success(SomeTask, metadata: { id: 1 })
|
|
27
21
|
def stub_task_success(command, metadata: {}, **context)
|
|
28
|
-
|
|
29
|
-
result = task.result
|
|
30
|
-
|
|
31
|
-
result.metadata.merge!(metadata)
|
|
32
|
-
result.executing!
|
|
33
|
-
result.executed!
|
|
34
|
-
|
|
35
|
-
allow(command).to receive(:execute).and_return(result)
|
|
36
|
-
|
|
37
|
-
result
|
|
22
|
+
build_stub(command, :execute, CMDx::Signal.success(nil, metadata:), context)
|
|
38
23
|
end
|
|
39
24
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# @param command [Class] The command class to stub execution on
|
|
43
|
-
# @param metadata [Hash] Optional metadata to pass to the result
|
|
44
|
-
# @param context [Hash] Optional keyword arguments to pass to the command
|
|
45
|
-
#
|
|
46
|
-
# @return [CMDx::Result] The successful result object
|
|
47
|
-
#
|
|
48
|
-
# @example Stubbing successful execution with context
|
|
49
|
-
# stub_task_success!(MyCommand, user_id: 123, role: "admin")
|
|
50
|
-
#
|
|
51
|
-
# result = MyCommand.execute!(user_id: 123, role: "admin")
|
|
52
|
-
# expect(result).to be_successful
|
|
53
|
-
#
|
|
54
|
-
# @example Stubbing successful execution without context
|
|
55
|
-
# stub_task_success!(MyCommand)
|
|
25
|
+
# Stubs `command.execute!` (bang variant) to return a frozen successful Result.
|
|
56
26
|
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
27
|
+
# @param command [Class] the Task class to stub
|
|
28
|
+
# @param metadata [Hash] payload exposed via `result.metadata`
|
|
29
|
+
# @param context [Hash{Symbol => Object}] context overrides forwarded to `command.new`
|
|
30
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
59
31
|
def stub_task_success!(command, metadata: {}, **context)
|
|
60
|
-
|
|
61
|
-
result = task.result
|
|
62
|
-
|
|
63
|
-
result.metadata.merge!(metadata)
|
|
64
|
-
result.executing!
|
|
65
|
-
result.executed!
|
|
66
|
-
|
|
67
|
-
allow(command).to receive(:execute!).and_return(result)
|
|
68
|
-
|
|
69
|
-
result
|
|
32
|
+
build_stub(command, :execute!, CMDx::Signal.success(nil, metadata:), context, strict: true)
|
|
70
33
|
end
|
|
71
34
|
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# @param command [Class] The command class to stub execution on
|
|
75
|
-
# @param reason [String, nil] Optional reason for skipping
|
|
76
|
-
# @param cause [CMDx::Fault, nil] Optional cause for skipping
|
|
77
|
-
# @param metadata [Hash] Optional metadata to pass to the result
|
|
78
|
-
# @param context [Hash] Optional keyword arguments to pass to the command
|
|
79
|
-
#
|
|
80
|
-
# @return [CMDx::Result] The skipped result object
|
|
35
|
+
# Stubs `command.execute` to return a frozen skipped Result.
|
|
81
36
|
#
|
|
82
|
-
# @
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
# @example Stubbing skipped execution with reason
|
|
89
|
-
# stub_task_skip(MyCommand, reason: "Skipped for testing", foo: "bar")
|
|
90
|
-
#
|
|
91
|
-
# result = MyCommand.execute(foo: "bar")
|
|
92
|
-
# expect(result).to have_skipped(reason: "Skipped for testing")
|
|
37
|
+
# @param command [Class] the Task class to stub
|
|
38
|
+
# @param reason [String, nil] human-readable skip reason
|
|
39
|
+
# @param cause [Exception, nil] originating cause attached to the signal
|
|
40
|
+
# @param metadata [Hash] payload exposed via `result.metadata`
|
|
41
|
+
# @param context [Hash{Symbol => Object}] context overrides forwarded to `command.new`
|
|
42
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
93
43
|
def stub_task_skip(command, reason: nil, cause: nil, metadata: {}, **context)
|
|
94
|
-
|
|
95
|
-
result = task.result
|
|
96
|
-
cause ||= CMDx::SkipFault.new(result)
|
|
97
|
-
|
|
98
|
-
result.executing!
|
|
99
|
-
result.skip!(reason, halt: false, cause:, **metadata)
|
|
100
|
-
|
|
101
|
-
allow(command).to receive(:execute).and_return(result)
|
|
102
|
-
|
|
103
|
-
result
|
|
44
|
+
build_stub(command, :execute, CMDx::Signal.skipped(reason, metadata:, cause:), context)
|
|
104
45
|
end
|
|
105
46
|
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
# @param command [Class] The command class to stub execution on
|
|
109
|
-
# @param reason [String, nil] Optional reason for skipping
|
|
110
|
-
# @param cause [CMDx::Fault, nil] Optional cause for skipping
|
|
111
|
-
# @param metadata [Hash] Optional metadata to pass to the result
|
|
112
|
-
# @param context [Hash] Optional keyword arguments to pass to the command
|
|
113
|
-
#
|
|
114
|
-
# @return [CMDx::Result] The skipped result object
|
|
115
|
-
#
|
|
116
|
-
# @example Stubbing skipped execution with context
|
|
117
|
-
# stub_task_skip!(MyCommand, foo: "bar")
|
|
47
|
+
# Stubs `command.execute!` (bang variant) to return a frozen skipped Result.
|
|
118
48
|
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
49
|
+
# @param command [Class] the Task class to stub
|
|
50
|
+
# @param reason [String, nil] human-readable skip reason
|
|
51
|
+
# @param cause [Exception, nil] originating cause attached to the signal
|
|
52
|
+
# @param metadata [Hash] payload exposed via `result.metadata`
|
|
53
|
+
# @param context [Hash{Symbol => Object}] context overrides forwarded to `command.new`
|
|
54
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
55
|
+
def stub_task_skip!(command, reason: nil, cause: nil, metadata: {}, **context)
|
|
56
|
+
build_stub(command, :execute!, CMDx::Signal.skipped(reason, metadata:, cause:), context, strict: true)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Stubs `command.execute` to return a frozen failed Result.
|
|
121
60
|
#
|
|
122
|
-
# @
|
|
123
|
-
#
|
|
61
|
+
# @param command [Class] the Task class to stub
|
|
62
|
+
# @param reason [String, nil] human-readable failure reason
|
|
63
|
+
# @param cause [Exception, nil] originating cause attached to the signal
|
|
64
|
+
# @param metadata [Hash] payload exposed via `result.metadata`
|
|
65
|
+
# @param context [Hash{Symbol => Object}] context overrides forwarded to `command.new`
|
|
66
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
67
|
+
def stub_task_fail(command, reason: nil, cause: nil, metadata: {}, **context)
|
|
68
|
+
build_stub(command, :execute, CMDx::Signal.failed(reason, metadata:, cause:), context)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Stubs `command.execute!` (bang variant) to return a frozen failed Result.
|
|
124
72
|
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
73
|
+
# @param command [Class] the Task class to stub
|
|
74
|
+
# @param reason [String, nil] human-readable failure reason
|
|
75
|
+
# @param cause [Exception, nil] originating cause attached to the signal
|
|
76
|
+
# @param metadata [Hash] payload exposed via `result.metadata`
|
|
77
|
+
# @param context [Hash{Symbol => Object}] context overrides forwarded to `command.new`
|
|
78
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
79
|
+
def stub_task_fail!(command, reason: nil, cause: nil, metadata: {}, **context)
|
|
80
|
+
build_stub(command, :execute!, CMDx::Signal.failed(reason, metadata:, cause:), context, strict: true)
|
|
81
|
+
end
|
|
131
82
|
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
# Stubs `command.execute` to return a frozen failed Result whose
|
|
84
|
+
# `cause` is an instance of +exception+. Models the "rescued
|
|
85
|
+
# StandardError -> failed signal" path that Runtime takes when a
|
|
86
|
+
# task's `work` raises something other than a Fault.
|
|
87
|
+
#
|
|
88
|
+
# @param command [Class] the Task class to stub
|
|
89
|
+
# @param exception [Class<StandardError>, StandardError] the cause
|
|
90
|
+
# @param message [String, nil] message used when constructing the
|
|
91
|
+
# exception (when +exception+ is a Class) and to derive the reason
|
|
92
|
+
# @param metadata [Hash]
|
|
93
|
+
# @param context [Hash{Symbol => Object}]
|
|
94
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
95
|
+
# @example
|
|
96
|
+
# stub_task_error(MyCommand, Net::OpenTimeout, "boom")
|
|
97
|
+
def stub_task_error(command, exception, message = nil, metadata: {}, **context)
|
|
98
|
+
ex = exception.is_a?(Class) ? exception.new(message || "stubbed") : exception
|
|
99
|
+
reason = "[#{ex.class}] #{ex.message}"
|
|
100
|
+
build_stub(command, :execute, CMDx::Signal.failed(reason, metadata:, cause: ex), context)
|
|
101
|
+
end
|
|
134
102
|
|
|
135
|
-
|
|
103
|
+
# Stubs `command.execute` to return a frozen failed Result that
|
|
104
|
+
# echoes +upstream_result+. Models the `throw!`-then-propagate path
|
|
105
|
+
# used by nested tasks/workflows.
|
|
106
|
+
#
|
|
107
|
+
# @param command [Class] the Task class to stub
|
|
108
|
+
# @param upstream_result [CMDx::Result] the originating failure
|
|
109
|
+
# @param metadata [Hash]
|
|
110
|
+
# @param context [Hash{Symbol => Object}]
|
|
111
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
112
|
+
def stub_task_throw(command, upstream_result, metadata: {}, **context)
|
|
113
|
+
unless upstream_result.is_a?(CMDx::Result) && upstream_result.failed?
|
|
114
|
+
raise ArgumentError,
|
|
115
|
+
"upstream_result must be a failed CMDx::Result"
|
|
116
|
+
end
|
|
136
117
|
|
|
137
|
-
|
|
118
|
+
build_stub(command, :execute, CMDx::Signal.echoed(upstream_result, metadata:), context)
|
|
138
119
|
end
|
|
139
120
|
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
# @param reason [String, nil] Optional reason for failure
|
|
144
|
-
# @param cause [CMDx::Fault, nil] Optional cause for failure
|
|
145
|
-
# @param metadata [Hash] Optional metadata to pass to the result
|
|
146
|
-
# @param context [Hash] Optional keyword arguments to pass to the command
|
|
147
|
-
#
|
|
148
|
-
# @return [CMDx::Result] The failed result object
|
|
149
|
-
#
|
|
150
|
-
# @example Stubbing failed execution with context
|
|
151
|
-
# stub_task_fail(MyCommand, foo: "bar")
|
|
152
|
-
#
|
|
153
|
-
# result = MyCommand.execute(foo: "bar")
|
|
154
|
-
# expect(result).to have_failed
|
|
121
|
+
# Stubs `command.execute` to return a successful Result flagged as
|
|
122
|
+
# `deprecated?`. Useful when asserting deprecation surfaces without
|
|
123
|
+
# triggering the real `Deprecation` action.
|
|
155
124
|
#
|
|
156
|
-
# @
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
result = task.result
|
|
164
|
-
cause ||= CMDx::FailFault.new(result)
|
|
125
|
+
# @param command [Class] the Task class to stub
|
|
126
|
+
# @param metadata [Hash]
|
|
127
|
+
# @param context [Hash{Symbol => Object}]
|
|
128
|
+
# @return [CMDx::Result]
|
|
129
|
+
def stub_task_deprecated(command, metadata: {}, **context)
|
|
130
|
+
build_stub(command, :execute, CMDx::Signal.success(nil, metadata:), context, deprecated: true)
|
|
131
|
+
end
|
|
165
132
|
|
|
166
|
-
|
|
167
|
-
|
|
133
|
+
# Captures lines written to a temporary CMDx logger for the duration
|
|
134
|
+
# of the block. Restores the previous logger on exit.
|
|
135
|
+
#
|
|
136
|
+
# @yield runs the block with `CMDx.configuration.logger` swapped
|
|
137
|
+
# @return [Array<String>] captured log lines
|
|
138
|
+
# @example
|
|
139
|
+
# logs = capture_cmdx_logs { MyCommand.execute }
|
|
140
|
+
# expect(logs.join).to include("status=success")
|
|
141
|
+
def capture_cmdx_logs(&)
|
|
142
|
+
raise ArgumentError, "block required" unless block_given?
|
|
143
|
+
|
|
144
|
+
io = StringIO.new
|
|
145
|
+
previous = CMDx.configuration.logger
|
|
146
|
+
CMDx.configuration.logger = Logger.new(io, formatter: previous&.formatter || CMDx::LogFormatters::Line.new)
|
|
147
|
+
yield
|
|
148
|
+
io.string.lines.map(&:chomp)
|
|
149
|
+
ensure
|
|
150
|
+
CMDx.configuration.logger = previous if previous
|
|
151
|
+
end
|
|
168
152
|
|
|
169
|
-
|
|
153
|
+
# Subscribes to telemetry events on +command+'s telemetry registry
|
|
154
|
+
# for the duration of the block. Captures every emitted event.
|
|
155
|
+
# Tasks subclassing +command+ also fire (telemetry is cloned at
|
|
156
|
+
# class definition; the registry array is shared by reference until
|
|
157
|
+
# +dup+).
|
|
158
|
+
#
|
|
159
|
+
# @param command [Class] the Task class whose telemetry to listen on
|
|
160
|
+
# @param events [Array<Symbol>] event names to subscribe to;
|
|
161
|
+
# defaults to all of {CMDx::Telemetry::EVENTS}
|
|
162
|
+
# @yield runs the block with subscribers attached
|
|
163
|
+
# @return [Array<CMDx::Telemetry::Event>] captured events in emission order
|
|
164
|
+
# @example
|
|
165
|
+
# events = subscribe_telemetry(MyCommand, :task_executed) { MyCommand.execute }
|
|
166
|
+
# expect(events.map(&:name)).to eq([:task_executed])
|
|
167
|
+
def subscribe_telemetry(command, *events, &)
|
|
168
|
+
raise ArgumentError, "block required" unless block_given?
|
|
169
|
+
|
|
170
|
+
events = CMDx::Telemetry::EVENTS if events.empty?
|
|
171
|
+
captured = []
|
|
172
|
+
telemetry = command.telemetry
|
|
173
|
+
listener = ->(event) { captured << event }
|
|
174
|
+
|
|
175
|
+
events.each { |e| telemetry.subscribe(e, listener) }
|
|
176
|
+
begin
|
|
177
|
+
yield
|
|
178
|
+
ensure
|
|
179
|
+
events.each { |e| telemetry.unsubscribe(e, listener) }
|
|
180
|
+
end
|
|
170
181
|
|
|
171
|
-
|
|
182
|
+
captured
|
|
172
183
|
end
|
|
173
184
|
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
# @param
|
|
180
|
-
# @
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# expect(result).to have_failed(reason: "Failed for testing")
|
|
195
|
-
def stub_task_fail!(command, reason: nil, cause: nil, metadata: {}, **context)
|
|
196
|
-
task = command.new(context)
|
|
197
|
-
result = task.result
|
|
198
|
-
cause ||= CMDx::FailFault.new(result)
|
|
199
|
-
|
|
200
|
-
result.executing!
|
|
201
|
-
result.fail!(reason, halt: false, cause:, **metadata)
|
|
185
|
+
# Captures the {CMDx::Chain} produced by +command+'s execution
|
|
186
|
+
# within the block. Subscribes to +command+'s `:task_executed`
|
|
187
|
+
# telemetry to grab the chain reference before Runtime teardown
|
|
188
|
+
# clears it.
|
|
189
|
+
#
|
|
190
|
+
# @param command [Class] the Task class whose chain to capture
|
|
191
|
+
# @yield the block to execute
|
|
192
|
+
# @return [CMDx::Chain, nil] the chain (frozen by Runtime), or nil
|
|
193
|
+
# when +command+ didn't run as a root during the block
|
|
194
|
+
# @example
|
|
195
|
+
# chain = with_cmdx_chain(MyWorkflow) { MyWorkflow.execute }
|
|
196
|
+
# expect(chain.size).to be > 1
|
|
197
|
+
def with_cmdx_chain(command)
|
|
198
|
+
raise ArgumentError, "block required" unless block_given?
|
|
199
|
+
|
|
200
|
+
captured = nil
|
|
201
|
+
telemetry = command.telemetry
|
|
202
|
+
listener = lambda do |event|
|
|
203
|
+
captured ||= event.payload[:result].chain if event.root
|
|
204
|
+
end
|
|
202
205
|
|
|
203
|
-
|
|
206
|
+
telemetry.subscribe(:task_executed, listener)
|
|
207
|
+
begin
|
|
208
|
+
yield
|
|
209
|
+
ensure
|
|
210
|
+
telemetry.unsubscribe(:task_executed, listener)
|
|
211
|
+
end
|
|
204
212
|
|
|
205
|
-
|
|
213
|
+
captured
|
|
206
214
|
end
|
|
207
215
|
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
# @param command [Class] The command class to unstub execution on
|
|
211
|
-
# @param context [Hash] Optional keyword arguments to match against
|
|
216
|
+
# Restores `command.execute` to its original implementation.
|
|
217
|
+
# When `context` is supplied, only the matching argument signature is unstubbed.
|
|
212
218
|
#
|
|
219
|
+
# @param command [Class] the Task class to unstub
|
|
220
|
+
# @param context [Hash{Symbol => Object}] argument signature whose stub to release
|
|
213
221
|
# @return [void]
|
|
214
|
-
#
|
|
215
|
-
# @example Unstubbing execute with context
|
|
216
|
-
# unstub_task(MyCommand, foo: "bar")
|
|
217
|
-
#
|
|
218
|
-
# MyCommand.execute(foo: "bar")
|
|
219
|
-
#
|
|
220
|
-
# @example Unstubbing execute without context
|
|
221
|
-
# unstub_task(MyCommand)
|
|
222
|
-
#
|
|
223
|
-
# MyCommand.execute
|
|
224
222
|
def unstub_task(command, **context)
|
|
225
223
|
if context.empty?
|
|
226
224
|
allow(command).to receive(:execute).and_call_original
|
|
@@ -229,22 +227,12 @@ module CMDx
|
|
|
229
227
|
end
|
|
230
228
|
end
|
|
231
229
|
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
# @param command [Class] The command class to unstub execution on
|
|
235
|
-
# @param context [Hash] Optional keyword arguments to match against
|
|
230
|
+
# Restores `command.execute!` to its original implementation.
|
|
231
|
+
# When `context` is supplied, only the matching argument signature is unstubbed.
|
|
236
232
|
#
|
|
233
|
+
# @param command [Class] the Task class to unstub
|
|
234
|
+
# @param context [Hash{Symbol => Object}] argument signature whose stub to release
|
|
237
235
|
# @return [void]
|
|
238
|
-
#
|
|
239
|
-
# @example Unstubbing execute!
|
|
240
|
-
# unstub_task!(MyCommand, foo: "bar")
|
|
241
|
-
#
|
|
242
|
-
# MyCommand.execute!(foo: "bar")
|
|
243
|
-
#
|
|
244
|
-
# @example Unstubbing execute! without context
|
|
245
|
-
# unstub_task!(MyCommand)
|
|
246
|
-
#
|
|
247
|
-
# MyCommand.execute!
|
|
248
236
|
def unstub_task!(command, **context)
|
|
249
237
|
if context.empty?
|
|
250
238
|
allow(command).to receive(:execute!).and_call_original
|
|
@@ -253,22 +241,12 @@ module CMDx
|
|
|
253
241
|
end
|
|
254
242
|
end
|
|
255
243
|
|
|
256
|
-
# Sets
|
|
244
|
+
# Sets a positive message expectation that `command.execute` is invoked.
|
|
245
|
+
# When `context` is supplied, the expectation is constrained to that signature.
|
|
257
246
|
#
|
|
258
|
-
# @param command [Class]
|
|
259
|
-
# @param context [Hash]
|
|
260
|
-
#
|
|
261
|
-
# @return [RSpec::Mocks::MessageExpectation] The RSpec expectation object
|
|
262
|
-
#
|
|
263
|
-
# @example Expecting execution with context
|
|
264
|
-
# expect_task_execution(MyCommand, user_id: 123, role: "admin")
|
|
265
|
-
#
|
|
266
|
-
# MyCommand.execute(user_id: 123, role: "admin")
|
|
267
|
-
#
|
|
268
|
-
# @example Expecting execution without context
|
|
269
|
-
# expect_task_execution(MyCommand)
|
|
270
|
-
#
|
|
271
|
-
# MyCommand.execute
|
|
247
|
+
# @param command [Class] the Task class to expect
|
|
248
|
+
# @param context [Hash{Symbol => Object}] argument signature to match
|
|
249
|
+
# @return [RSpec::Mocks::MessageExpectation]
|
|
272
250
|
def expect_task_execution(command, **context)
|
|
273
251
|
if context.empty?
|
|
274
252
|
expect(command).to receive(:execute)
|
|
@@ -277,22 +255,12 @@ module CMDx
|
|
|
277
255
|
end
|
|
278
256
|
end
|
|
279
257
|
|
|
280
|
-
# Sets
|
|
281
|
-
#
|
|
282
|
-
# @param command [Class] The command class to expect execution on
|
|
283
|
-
# @param context [Hash] Optional keyword arguments to match against
|
|
284
|
-
#
|
|
285
|
-
# @return [RSpec::Mocks::MessageExpectation] The RSpec expectation object
|
|
258
|
+
# Sets a positive message expectation that `command.execute!` is invoked.
|
|
259
|
+
# When `context` is supplied, the expectation is constrained to that signature.
|
|
286
260
|
#
|
|
287
|
-
# @
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
# MyCommand.execute!(user_id: 123, role: "admin")
|
|
291
|
-
#
|
|
292
|
-
# @example Expecting execution without context
|
|
293
|
-
# expect_task_execution!(MyCommand)
|
|
294
|
-
#
|
|
295
|
-
# MyCommand.execute!
|
|
261
|
+
# @param command [Class] the Task class to expect
|
|
262
|
+
# @param context [Hash{Symbol => Object}] argument signature to match
|
|
263
|
+
# @return [RSpec::Mocks::MessageExpectation]
|
|
296
264
|
def expect_task_execution!(command, **context)
|
|
297
265
|
if context.empty?
|
|
298
266
|
expect(command).to receive(:execute!)
|
|
@@ -301,22 +269,12 @@ module CMDx
|
|
|
301
269
|
end
|
|
302
270
|
end
|
|
303
271
|
|
|
304
|
-
# Sets
|
|
305
|
-
#
|
|
306
|
-
# @param command [Class] The command class to expect execution on
|
|
307
|
-
# @param context [Hash] Optional keyword arguments to match against
|
|
308
|
-
#
|
|
309
|
-
# @return [RSpec::Mocks::MessageExpectation] The RSpec expectation object
|
|
310
|
-
#
|
|
311
|
-
# @example Expecting no execution with context
|
|
312
|
-
# expect_no_task_execution(MyCommand, foo: "bar")
|
|
313
|
-
#
|
|
314
|
-
# MyCommand.execute(foo: "bar")
|
|
272
|
+
# Sets a negative message expectation that `command.execute` is not invoked.
|
|
273
|
+
# When `context` is supplied, only the matching signature is forbidden.
|
|
315
274
|
#
|
|
316
|
-
# @
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
# MyCommand.execute
|
|
275
|
+
# @param command [Class] the Task class to guard
|
|
276
|
+
# @param context [Hash{Symbol => Object}] argument signature to forbid
|
|
277
|
+
# @return [RSpec::Mocks::MessageExpectation]
|
|
320
278
|
def expect_no_task_execution(command, **context)
|
|
321
279
|
if context.empty?
|
|
322
280
|
expect(command).not_to receive(:execute)
|
|
@@ -325,22 +283,12 @@ module CMDx
|
|
|
325
283
|
end
|
|
326
284
|
end
|
|
327
285
|
|
|
328
|
-
# Sets
|
|
329
|
-
#
|
|
330
|
-
# @param command [Class] The command class to expect execution on
|
|
331
|
-
# @param context [Hash] Optional keyword arguments to match against
|
|
332
|
-
#
|
|
333
|
-
# @return [RSpec::Mocks::MessageExpectation] The RSpec expectation object
|
|
334
|
-
#
|
|
335
|
-
# @example Expecting no execution! with context
|
|
336
|
-
# expect_no_task_execution!(MyCommand, foo: "bar")
|
|
286
|
+
# Sets a negative message expectation that `command.execute!` is not invoked.
|
|
287
|
+
# When `context` is supplied, only the matching signature is forbidden.
|
|
337
288
|
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
# @
|
|
341
|
-
# expect_no_task_execution!(MyCommand)
|
|
342
|
-
#
|
|
343
|
-
# MyCommand.execute!
|
|
289
|
+
# @param command [Class] the Task class to guard
|
|
290
|
+
# @param context [Hash{Symbol => Object}] argument signature to forbid
|
|
291
|
+
# @return [RSpec::Mocks::MessageExpectation]
|
|
344
292
|
def expect_no_task_execution!(command, **context)
|
|
345
293
|
if context.empty?
|
|
346
294
|
expect(command).not_to receive(:execute!)
|
|
@@ -349,6 +297,56 @@ module CMDx
|
|
|
349
297
|
end
|
|
350
298
|
end
|
|
351
299
|
|
|
300
|
+
# Yields each distinct Task class reachable from a Workflow's pipeline,
|
|
301
|
+
# in first-seen order, so callers can stub them in a single block.
|
|
302
|
+
#
|
|
303
|
+
# @param command [Class] a Workflow class (must include `CMDx::Workflow`)
|
|
304
|
+
# @yieldparam task [Class] a Task class referenced by the workflow pipeline
|
|
305
|
+
# @return [void]
|
|
306
|
+
# @raise [ArgumentError] when no block is given or `command` is not a Workflow
|
|
307
|
+
# @example
|
|
308
|
+
# stub_workflow_tasks(MyWorkflow) { |t| stub_task_success(t) }
|
|
309
|
+
def stub_workflow_tasks(command, &)
|
|
310
|
+
if !block_given?
|
|
311
|
+
raise ArgumentError, "block required"
|
|
312
|
+
elsif !command.include?(Workflow)
|
|
313
|
+
raise ArgumentError, "#{command.inspect} must be a workflow"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
command.pipeline.flat_map(&:tasks).uniq.each(&)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
private
|
|
320
|
+
|
|
321
|
+
# Constructs a frozen {CMDx::Result} from `signal` and stubs `command.method`
|
|
322
|
+
# to return it. The Result is unshifted onto a new {CMDx::Chain} so callers
|
|
323
|
+
# observe the same shape as a real execution.
|
|
324
|
+
#
|
|
325
|
+
# @api private
|
|
326
|
+
# @param command [Class] the Task class being stubbed
|
|
327
|
+
# @param method [Symbol] the message to stub (`:execute` or `:execute!`)
|
|
328
|
+
# @param signal [CMDx::Signal] outcome payload to wrap
|
|
329
|
+
# @param context [Hash] context overrides for `command.new`
|
|
330
|
+
# @return [CMDx::Result] the frozen Result installed on the stub
|
|
331
|
+
def build_stub(command, method, signal, context, **)
|
|
332
|
+
task = command.new(context)
|
|
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
|
+
)
|
|
342
|
+
|
|
343
|
+
chain.unshift(result)
|
|
344
|
+
|
|
345
|
+
allow(command).to receive(method).and_return(result)
|
|
346
|
+
|
|
347
|
+
result
|
|
348
|
+
end
|
|
349
|
+
|
|
352
350
|
end
|
|
353
351
|
end
|
|
354
352
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Matcher to verify that a result completed without interruption.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# expect(MyCommand.execute).to be_complete
|
|
7
|
+
RSpec::Matchers.define :be_complete do
|
|
8
|
+
description { "have completed" }
|
|
9
|
+
|
|
10
|
+
failure_message do |result|
|
|
11
|
+
"expected #{result.inspect} to have state #{CMDx::Signal::COMPLETE.inspect}, " \
|
|
12
|
+
"but was #{result.state.inspect}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
match do |result|
|
|
16
|
+
raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
|
|
17
|
+
|
|
18
|
+
result.state == CMDx::Signal::COMPLETE
|
|
19
|
+
end
|
|
20
|
+
end
|