cmdx 1.0.1 → 1.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/.cursor/prompts/rspec.md +20 -0
- data/.cursor/prompts/yardoc.md +8 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +17 -2
- data/README.md +1 -1
- data/docs/basics/call.md +2 -2
- data/docs/basics/chain.md +1 -1
- data/docs/callbacks.md +3 -36
- data/docs/configuration.md +58 -12
- data/docs/interruptions/exceptions.md +1 -1
- data/docs/interruptions/faults.md +2 -2
- data/docs/logging.md +4 -4
- data/docs/middlewares.md +43 -43
- data/docs/parameters/coercions.md +49 -38
- data/docs/parameters/defaults.md +1 -1
- data/docs/parameters/validations.md +0 -39
- data/docs/testing.md +11 -12
- data/docs/workflows.md +4 -4
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +36 -56
- data/lib/cmdx/callback_registry.rb +82 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +22 -115
- data/lib/cmdx/chain_serializer.rb +17 -148
- data/lib/cmdx/coercion.rb +49 -0
- data/lib/cmdx/coercion_registry.rb +94 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +57 -171
- data/lib/cmdx/context.rb +22 -165
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +40 -156
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +165 -202
- data/lib/cmdx/fault.rb +55 -158
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -109
- data/lib/cmdx/lazy_struct.rb +103 -187
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +20 -82
- data/lib/cmdx/logger_ansi.rb +18 -75
- data/lib/cmdx/logger_serializer.rb +24 -114
- data/lib/cmdx/middleware.rb +38 -60
- data/lib/cmdx/middleware_registry.rb +81 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +120 -198
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +25 -56
- data/lib/cmdx/parameter_registry.rb +59 -84
- data/lib/cmdx/parameter_serializer.rb +23 -74
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -260
- data/lib/cmdx/result_ansi.rb +19 -85
- data/lib/cmdx/result_inspector.rb +27 -68
- data/lib/cmdx/result_logger.rb +18 -81
- data/lib/cmdx/result_serializer.rb +28 -132
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +213 -425
- data/lib/cmdx/task_deprecator.rb +55 -0
- data/lib/cmdx/task_processor.rb +245 -0
- data/lib/cmdx/task_serializer.rb +22 -70
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +48 -0
- data/lib/cmdx/validator_registry.rb +86 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +46 -339
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +34 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has failed with specific conditions.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object is in a failed state, which means
|
6
|
+
# the task was executed but encountered an error or failure condition. A result
|
7
|
+
# is considered failed when it's in both "failed" status and "interrupted" state,
|
8
|
+
# and has been executed. Optionally checks for specific failure reasons and metadata.
|
9
|
+
#
|
10
|
+
# @param expected_reason [String, Symbol, nil] optional expected failure reason
|
11
|
+
#
|
12
|
+
# @return [Boolean] true if the result is failed, interrupted, executed, and matches expected criteria
|
13
|
+
#
|
14
|
+
# @example Basic usage with failed task
|
15
|
+
# result = ValidateUserTask.call(user_id: nil)
|
16
|
+
# expect(result).to be_failed_task
|
17
|
+
#
|
18
|
+
# @example Checking for specific failure reason
|
19
|
+
# result = ProcessPaymentTask.call(amount: -100)
|
20
|
+
# expect(result).to be_failed_task("invalid_amount")
|
21
|
+
#
|
22
|
+
# @example Using with_reason chain
|
23
|
+
# result = AuthenticateUserTask.call(token: "invalid")
|
24
|
+
# expect(result).to be_failed_task.with_reason(:authentication_failed)
|
25
|
+
#
|
26
|
+
# @example Checking failure with metadata
|
27
|
+
# result = UploadFileTask.call(file: corrupted_file)
|
28
|
+
# expect(result).to be_failed_task.with_metadata(file_size: 0, error_code: "CORRUPTED")
|
29
|
+
#
|
30
|
+
# @example Combining reason and metadata checks
|
31
|
+
# result = ValidateDataTask.call(data: invalid_data)
|
32
|
+
# expect(result).to be_failed_task("validation_error").with_metadata(field: "email", rule: "format")
|
33
|
+
#
|
34
|
+
# @example Negative assertion
|
35
|
+
# result = SuccessfulTask.call(data: "valid")
|
36
|
+
# expect(result).not_to be_failed_task
|
37
|
+
RSpec::Matchers.define :be_failed_task do |expected_reason = nil|
|
38
|
+
match do |result|
|
39
|
+
result.failed? &&
|
40
|
+
result.interrupted? &&
|
41
|
+
result.executed? &&
|
42
|
+
(expected_reason.nil? || result.metadata[:reason] == expected_reason)
|
43
|
+
end
|
44
|
+
|
45
|
+
chain :with_reason do |reason|
|
46
|
+
@expected_reason = reason
|
47
|
+
end
|
48
|
+
|
49
|
+
chain :with_metadata do |metadata|
|
50
|
+
@expected_metadata = metadata
|
51
|
+
end
|
52
|
+
|
53
|
+
match do |result|
|
54
|
+
reason = @expected_reason || expected_reason
|
55
|
+
metadata = @expected_metadata || {}
|
56
|
+
|
57
|
+
result.failed? &&
|
58
|
+
result.interrupted? &&
|
59
|
+
result.executed? &&
|
60
|
+
(reason.nil? || result.metadata[:reason] == reason) &&
|
61
|
+
(metadata.empty? || metadata.all? { |k, v| result.metadata[k] == v })
|
62
|
+
end
|
63
|
+
|
64
|
+
failure_message do |result|
|
65
|
+
messages = []
|
66
|
+
messages << "expected result to be failed, but was #{result.status}" unless result.failed?
|
67
|
+
messages << "expected result to be interrupted, but was #{result.state}" unless result.interrupted?
|
68
|
+
messages << "expected result to be executed, but was not" unless result.executed?
|
69
|
+
|
70
|
+
reason = @expected_reason || expected_reason
|
71
|
+
messages << "expected failure reason to be '#{reason}', but was '#{result.metadata[:reason]}'" if reason && result.metadata[:reason] != reason
|
72
|
+
|
73
|
+
if @expected_metadata&.any?
|
74
|
+
mismatches = @expected_metadata.filter_map do |k, v|
|
75
|
+
"#{k}: expected #{v}, got #{result.metadata[k]}" if result.metadata[k] != v
|
76
|
+
end
|
77
|
+
messages.concat(mismatches)
|
78
|
+
end
|
79
|
+
|
80
|
+
messages.join(", ")
|
81
|
+
end
|
82
|
+
|
83
|
+
failure_message_when_negated do |_result|
|
84
|
+
"expected result not to be failed, but it was"
|
85
|
+
end
|
86
|
+
|
87
|
+
description do
|
88
|
+
desc = "be a failed task"
|
89
|
+
reason = @expected_reason || expected_reason
|
90
|
+
desc += " with reason '#{reason}'" if reason
|
91
|
+
desc += " with metadata #{@expected_metadata}" if @expected_metadata&.any?
|
92
|
+
desc
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has been skipped with specific conditions.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object is in a skipped state, which means
|
6
|
+
# the task was executed but was intentionally skipped due to some condition. A result
|
7
|
+
# is considered skipped when it's in both "skipped" status and "interrupted" state,
|
8
|
+
# and has been executed. Optionally checks for specific skip reasons and metadata.
|
9
|
+
#
|
10
|
+
# @param expected_reason [String, Symbol, nil] optional expected skip reason
|
11
|
+
#
|
12
|
+
# @return [Boolean] true if the result is skipped, interrupted, executed, and matches expected criteria
|
13
|
+
#
|
14
|
+
# @example Basic usage with skipped task
|
15
|
+
# result = ProcessUserTask.call(user_id: 123)
|
16
|
+
# expect(result).to be_skipped_task
|
17
|
+
#
|
18
|
+
# @example Checking for specific skip reason
|
19
|
+
# result = SendEmailTask.call(user: inactive_user)
|
20
|
+
# expect(result).to be_skipped_task("user_inactive")
|
21
|
+
#
|
22
|
+
# @example Using with_reason chain
|
23
|
+
# result = BackupDataTask.call(force: false)
|
24
|
+
# expect(result).to be_skipped_task.with_reason(:backup_not_needed)
|
25
|
+
#
|
26
|
+
# @example Checking skip with metadata
|
27
|
+
# result = ProcessQueueTask.call(queue: empty_queue)
|
28
|
+
# expect(result).to be_skipped_task.with_metadata(queue_size: 0, processed_count: 0)
|
29
|
+
#
|
30
|
+
# @example Combining reason and metadata checks
|
31
|
+
# result = SyncDataTask.call(data: outdated_data)
|
32
|
+
# expect(result).to be_skipped_task("data_unchanged").with_metadata(last_sync: timestamp, changes: 0)
|
33
|
+
#
|
34
|
+
# @example Negative assertion
|
35
|
+
# result = ExecutedTask.call(data: "valid")
|
36
|
+
# expect(result).not_to be_skipped_task
|
37
|
+
RSpec::Matchers.define :be_skipped_task do |expected_reason = nil|
|
38
|
+
match do |result|
|
39
|
+
result.skipped? &&
|
40
|
+
result.interrupted? &&
|
41
|
+
result.executed? &&
|
42
|
+
(expected_reason.nil? || result.metadata[:reason] == expected_reason)
|
43
|
+
end
|
44
|
+
|
45
|
+
chain :with_reason do |reason|
|
46
|
+
@expected_reason = reason
|
47
|
+
end
|
48
|
+
|
49
|
+
chain :with_metadata do |metadata|
|
50
|
+
@expected_metadata = metadata
|
51
|
+
end
|
52
|
+
|
53
|
+
match do |result|
|
54
|
+
reason = @expected_reason || expected_reason
|
55
|
+
metadata = @expected_metadata || {}
|
56
|
+
|
57
|
+
result.skipped? &&
|
58
|
+
result.interrupted? &&
|
59
|
+
result.executed? &&
|
60
|
+
(reason.nil? || result.metadata[:reason] == reason) &&
|
61
|
+
(metadata.empty? || metadata.all? { |k, v| result.metadata[k] == v })
|
62
|
+
end
|
63
|
+
|
64
|
+
failure_message do |result|
|
65
|
+
messages = []
|
66
|
+
messages << "expected result to be skipped, but was #{result.status}" unless result.skipped?
|
67
|
+
messages << "expected result to be interrupted, but was #{result.state}" unless result.interrupted?
|
68
|
+
messages << "expected result to be executed, but was not" unless result.executed?
|
69
|
+
|
70
|
+
reason = @expected_reason || expected_reason
|
71
|
+
messages << "expected skip reason to be '#{reason}', but was '#{result.metadata[:reason]}'" if reason && result.metadata[:reason] != reason
|
72
|
+
|
73
|
+
if @expected_metadata&.any?
|
74
|
+
mismatches = @expected_metadata.filter_map do |k, v|
|
75
|
+
"#{k}: expected #{v}, got #{result.metadata[k]}" if result.metadata[k] != v
|
76
|
+
end
|
77
|
+
messages.concat(mismatches)
|
78
|
+
end
|
79
|
+
|
80
|
+
messages.join(", ")
|
81
|
+
end
|
82
|
+
|
83
|
+
failure_message_when_negated do |_result|
|
84
|
+
"expected result not to be skipped, but it was"
|
85
|
+
end
|
86
|
+
|
87
|
+
description do
|
88
|
+
desc = "be a skipped task"
|
89
|
+
reason = @expected_reason || expected_reason
|
90
|
+
desc += " with reason '#{reason}'" if reason
|
91
|
+
desc += " with metadata #{@expected_metadata}" if @expected_metadata&.any?
|
92
|
+
desc
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matchers for asserting task result states.
|
4
|
+
#
|
5
|
+
# This file dynamically generates RSpec matchers for each execution state defined
|
6
|
+
# in CMDx::Result::STATES. These matchers check the current execution state of a
|
7
|
+
# task result, which represents where the task is in its lifecycle from
|
8
|
+
# initialization through completion or interruption.
|
9
|
+
#
|
10
|
+
# The following matchers are automatically generated:
|
11
|
+
# - `be_initialized` - Task has been created but not yet started
|
12
|
+
# - `be_executing` - Task is currently running its logic
|
13
|
+
# - `be_complete` - Task has successfully finished execution
|
14
|
+
# - `be_interrupted` - Task execution was halted due to failure or skip
|
15
|
+
#
|
16
|
+
# @return [Boolean] true if the result matches the expected state
|
17
|
+
#
|
18
|
+
# @example Testing initialized state
|
19
|
+
# result = MyTask.new.result
|
20
|
+
# expect(result).to be_initialized
|
21
|
+
#
|
22
|
+
# @example Testing executing state
|
23
|
+
# result = MyTask.call(data: "processing")
|
24
|
+
# expect(result).to be_executing # During execution
|
25
|
+
#
|
26
|
+
# @example Testing complete state
|
27
|
+
# result = SuccessfulTask.call(data: "valid")
|
28
|
+
# expect(result).to be_complete
|
29
|
+
#
|
30
|
+
# @example Testing interrupted state
|
31
|
+
# result = FailedTask.call(data: "invalid")
|
32
|
+
# expect(result).to be_interrupted
|
33
|
+
#
|
34
|
+
# @example Negative assertion
|
35
|
+
# result = SuccessfulTask.call(data: "valid")
|
36
|
+
# expect(result).not_to be_initialized
|
37
|
+
#
|
38
|
+
# @example Using with other matchers
|
39
|
+
# result = ProcessDataTask.call(data: invalid_data)
|
40
|
+
# expect(result).to be_interrupted.and be_failed
|
41
|
+
CMDx::Result::STATES.each do |state|
|
42
|
+
RSpec::Matchers.define :"be_#{state}" do
|
43
|
+
match do |result|
|
44
|
+
result.public_send(:"#{state}?")
|
45
|
+
end
|
46
|
+
|
47
|
+
failure_message do |result|
|
48
|
+
"expected result to be #{state}, but was #{result.state}"
|
49
|
+
end
|
50
|
+
|
51
|
+
failure_message_when_negated do |_result|
|
52
|
+
"expected result not to be #{state}, but it was"
|
53
|
+
end
|
54
|
+
|
55
|
+
description do
|
56
|
+
"be #{state}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matchers for asserting task result statuses.
|
4
|
+
#
|
5
|
+
# This file dynamically generates RSpec matchers for each execution status defined
|
6
|
+
# in CMDx::Result::STATUSES. These matchers check the outcome of task logic execution,
|
7
|
+
# which represents what happened when the task's business logic ran (success, skip, or failure).
|
8
|
+
#
|
9
|
+
# The following matchers are automatically generated:
|
10
|
+
# - `be_success` - Task completed successfully without errors
|
11
|
+
# - `be_skipped` - Task was intentionally skipped due to conditions
|
12
|
+
# - `be_failed` - Task failed due to errors or validation issues
|
13
|
+
#
|
14
|
+
# @return [Boolean] true if the result matches the expected status
|
15
|
+
#
|
16
|
+
# @example Testing success status
|
17
|
+
# result = ProcessDataTask.call(data: "valid")
|
18
|
+
# expect(result).to be_success
|
19
|
+
#
|
20
|
+
# @example Testing skipped status
|
21
|
+
# result = SendEmailTask.call(user: inactive_user)
|
22
|
+
# expect(result).to be_skipped
|
23
|
+
#
|
24
|
+
# @example Testing failed status
|
25
|
+
# result = ValidateUserTask.call(user_id: nil)
|
26
|
+
# expect(result).to be_failed
|
27
|
+
#
|
28
|
+
# @example Negative assertion
|
29
|
+
# result = SuccessfulTask.call(data: "valid")
|
30
|
+
# expect(result).not_to be_failed
|
31
|
+
#
|
32
|
+
# @example Using with state matchers
|
33
|
+
# result = ProcessPaymentTask.call(amount: -100)
|
34
|
+
# expect(result).to be_failed.and be_interrupted
|
35
|
+
#
|
36
|
+
# @example Testing good vs bad outcomes
|
37
|
+
# result = BackupTask.call(force: false)
|
38
|
+
# expect(result).to be_skipped # Skipped is still a "good" outcome
|
39
|
+
CMDx::Result::STATUSES.each do |status|
|
40
|
+
RSpec::Matchers.define :"be_#{status}" do
|
41
|
+
match do |result|
|
42
|
+
result.public_send(:"#{status}?")
|
43
|
+
end
|
44
|
+
|
45
|
+
failure_message do |result|
|
46
|
+
"expected result to be #{status}, but was #{result.status}"
|
47
|
+
end
|
48
|
+
|
49
|
+
failure_message_when_negated do |_result|
|
50
|
+
"expected result not to be #{status}, but it was"
|
51
|
+
end
|
52
|
+
|
53
|
+
description do
|
54
|
+
"be #{status}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has completed successfully.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object represents a fully successful task
|
6
|
+
# execution, which means the task completed without errors and reached the end of
|
7
|
+
# its lifecycle. A result is considered a successful task when it has "success" status,
|
8
|
+
# "complete" state, and has been executed. Optionally validates expected context values.
|
9
|
+
#
|
10
|
+
# @param expected_context [Hash] optional hash of expected context key-value pairs
|
11
|
+
#
|
12
|
+
# @return [Boolean] true if the result is successful, complete, executed, and matches expected context
|
13
|
+
#
|
14
|
+
# @example Basic usage with successful task
|
15
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
16
|
+
# expect(result).to be_successful_task
|
17
|
+
#
|
18
|
+
# @example Checking successful task with context validation
|
19
|
+
# result = CalculateTotalTask.call(items: [item1, item2])
|
20
|
+
# expect(result).to be_successful_task(total: 150.00, tax: 12.50)
|
21
|
+
#
|
22
|
+
# @example Validating multiple context attributes
|
23
|
+
# result = UserRegistrationTask.call(email: "user@example.com")
|
24
|
+
# expect(result).to be_successful_task(
|
25
|
+
# user_id: 42,
|
26
|
+
# email_sent: true,
|
27
|
+
# activation_token: be_present
|
28
|
+
# )
|
29
|
+
#
|
30
|
+
# @example Negative assertion
|
31
|
+
# result = FailedValidationTask.call(data: "invalid")
|
32
|
+
# expect(result).not_to be_successful_task
|
33
|
+
#
|
34
|
+
# @example Combining with other matchers
|
35
|
+
# result = ProcessPaymentTask.call(amount: 100)
|
36
|
+
# expect(result).to be_successful_task.and have_runtime
|
37
|
+
#
|
38
|
+
# @example Testing context without specific values
|
39
|
+
# result = DataProcessingTask.call(data: dataset)
|
40
|
+
# expect(result).to be_successful_task({}) # Just check success without context
|
41
|
+
RSpec::Matchers.define :be_successful_task do |expected_context = {}|
|
42
|
+
match do |result|
|
43
|
+
result.success? &&
|
44
|
+
result.complete? &&
|
45
|
+
result.executed? &&
|
46
|
+
(expected_context.empty? || context_matches?(result, expected_context))
|
47
|
+
end
|
48
|
+
|
49
|
+
failure_message do |result|
|
50
|
+
messages = []
|
51
|
+
messages << "expected result to be successful, but was #{result.status}" unless result.success?
|
52
|
+
messages << "expected result to be complete, but was #{result.state}" unless result.complete?
|
53
|
+
messages << "expected result to be executed, but was not" unless result.executed?
|
54
|
+
|
55
|
+
unless expected_context.empty?
|
56
|
+
mismatches = context_mismatches(result, expected_context)
|
57
|
+
messages << "expected context to match #{expected_context}, but #{mismatches}" if mismatches.any?
|
58
|
+
end
|
59
|
+
|
60
|
+
messages.join(", ")
|
61
|
+
end
|
62
|
+
|
63
|
+
failure_message_when_negated do |_result|
|
64
|
+
"expected result not to be successful, but it was"
|
65
|
+
end
|
66
|
+
|
67
|
+
description do
|
68
|
+
desc = "be a successful task"
|
69
|
+
desc += " with context #{expected_context}" unless expected_context.empty?
|
70
|
+
desc
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def context_matches?(result, expected_context)
|
76
|
+
expected_context.all? do |key, value|
|
77
|
+
result.context.public_send(key) == value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def context_mismatches(result, expected_context)
|
82
|
+
expected_context.filter_map do |key, expected_value|
|
83
|
+
actual_value = result.context.public_send(key)
|
84
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" if actual_value != expected_value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has a bad outcome.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object represents a non-successful outcome,
|
6
|
+
# which includes both failed and skipped results. A result has a bad outcome when
|
7
|
+
# its status is anything other than "success" (i.e., either "failed" or "skipped").
|
8
|
+
# This is useful for testing error handling and conditional logic paths.
|
9
|
+
#
|
10
|
+
# @return [Boolean] true if the result has a bad outcome (failed or skipped)
|
11
|
+
#
|
12
|
+
# @example Testing failed task outcome
|
13
|
+
# result = ValidateDataTask.call(data: "invalid")
|
14
|
+
# expect(result).to have_bad_outcome
|
15
|
+
#
|
16
|
+
# @example Testing skipped task outcome
|
17
|
+
# result = ProcessQueueTask.call(queue: empty_queue)
|
18
|
+
# expect(result).to have_bad_outcome
|
19
|
+
#
|
20
|
+
# @example Testing error handling paths
|
21
|
+
# result = ProcessPaymentTask.call(amount: -100)
|
22
|
+
# expect(result).to have_bad_outcome.and be_failed
|
23
|
+
#
|
24
|
+
# @example Negative assertion for successful tasks
|
25
|
+
# result = SuccessfulTask.call(data: "valid")
|
26
|
+
# expect(result).not_to have_bad_outcome
|
27
|
+
#
|
28
|
+
# @example Using in conditional test logic
|
29
|
+
# result = ConditionalTask.call(condition: false)
|
30
|
+
# if result.bad?
|
31
|
+
# expect(result).to have_bad_outcome
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @example Opposite of good outcome
|
35
|
+
# result = SkippedTask.call(reason: "not_needed")
|
36
|
+
# expect(result).to have_bad_outcome.and not_to have_good_outcome
|
37
|
+
RSpec::Matchers.define :have_bad_outcome do
|
38
|
+
match(&:bad?)
|
39
|
+
|
40
|
+
failure_message do |result|
|
41
|
+
"expected result to have bad outcome (not success), but was #{result.status}"
|
42
|
+
end
|
43
|
+
|
44
|
+
failure_message_when_negated do |result|
|
45
|
+
"expected result not to have bad outcome, but it did (status: #{result.status})"
|
46
|
+
end
|
47
|
+
|
48
|
+
description do
|
49
|
+
"have bad outcome"
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has caused its own failure.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object represents a failure that originated
|
6
|
+
# within the task itself, as opposed to a failure that was thrown from or received
|
7
|
+
# from another task. A result is considered to have caused failure when it's both
|
8
|
+
# failed and the failure was generated by the task's own logic rather than propagated
|
9
|
+
# from elsewhere in the chain.
|
10
|
+
#
|
11
|
+
# @return [Boolean] true if the result is failed and caused the failure itself
|
12
|
+
#
|
13
|
+
# @example Testing task that fails due to validation
|
14
|
+
# result = ValidateUserTask.call(email: "invalid-email")
|
15
|
+
# expect(result).to have_caused_failure
|
16
|
+
#
|
17
|
+
# @example Testing task that fails due to business logic
|
18
|
+
# result = ProcessPaymentTask.call(amount: -100)
|
19
|
+
# expect(result).to have_caused_failure
|
20
|
+
#
|
21
|
+
# @example Distinguishing from thrown failures
|
22
|
+
# result = TaskThatThrowsFailure.call(data: "invalid")
|
23
|
+
# expect(result).to have_caused_failure # This task caused its own failure
|
24
|
+
# expect(result).not_to have_thrown_failure
|
25
|
+
#
|
26
|
+
# @example Testing in workflow context
|
27
|
+
# workflow_result = MyWorkflow.call(data: "invalid")
|
28
|
+
# failing_task = workflow_result.chain.find(&:failed?)
|
29
|
+
# expect(failing_task).to have_caused_failure
|
30
|
+
#
|
31
|
+
# @example Negative assertion
|
32
|
+
# result = SuccessfulTask.call(data: "valid")
|
33
|
+
# expect(result).not_to have_caused_failure
|
34
|
+
#
|
35
|
+
# @example Testing error handling origin
|
36
|
+
# result = DatabaseTask.call(connection: nil)
|
37
|
+
# expect(result).to have_caused_failure.and be_failed
|
38
|
+
RSpec::Matchers.define :have_caused_failure do
|
39
|
+
match do |result|
|
40
|
+
result.failed? && result.caused_failure?
|
41
|
+
end
|
42
|
+
|
43
|
+
failure_message do |result|
|
44
|
+
if result.failed?
|
45
|
+
"expected result to have caused failure, but it threw/received a failure instead"
|
46
|
+
else
|
47
|
+
"expected result to have caused failure, but it was not failed (status: #{result.status})"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
failure_message_when_negated do |_result|
|
52
|
+
"expected result not to have caused failure, but it did"
|
53
|
+
end
|
54
|
+
|
55
|
+
description do
|
56
|
+
"have caused failure"
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has a specific chain index.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object is positioned at the expected index
|
6
|
+
# within its execution chain. The chain index represents the zero-based position of
|
7
|
+
# the task in the workflow execution order, which is useful for testing workflow
|
8
|
+
# structure, execution order, and identifying specific tasks within complex chains.
|
9
|
+
#
|
10
|
+
# @param expected_index [Integer] the expected zero-based index position in the chain
|
11
|
+
#
|
12
|
+
# @return [Boolean] true if the result's chain index matches the expected index
|
13
|
+
#
|
14
|
+
# @example Testing first task in workflow
|
15
|
+
# workflow_result = MyWorkflow.call(data: "test")
|
16
|
+
# first_task = workflow_result.chain.first
|
17
|
+
# expect(first_task).to have_chain_index(0)
|
18
|
+
#
|
19
|
+
# @example Testing specific task position
|
20
|
+
# workflow_result = ProcessingWorkflow.call(items: [1, 2, 3])
|
21
|
+
# validation_task = workflow_result.chain[2]
|
22
|
+
# expect(validation_task).to have_chain_index(2)
|
23
|
+
#
|
24
|
+
# @example Testing failed task position
|
25
|
+
# workflow_result = FailingWorkflow.call(data: "invalid")
|
26
|
+
# failed_task = workflow_result.chain.find(&:failed?)
|
27
|
+
# expect(failed_task).to have_chain_index(1)
|
28
|
+
#
|
29
|
+
# @example Testing last task in chain
|
30
|
+
# workflow_result = CompletedWorkflow.call(data: "valid")
|
31
|
+
# last_task = workflow_result.chain.last
|
32
|
+
# expect(last_task).to have_chain_index(workflow_result.chain.length - 1)
|
33
|
+
#
|
34
|
+
# @example Negative assertion
|
35
|
+
# workflow_result = MyWorkflow.call(data: "test")
|
36
|
+
# middle_task = workflow_result.chain[1]
|
37
|
+
# expect(middle_task).not_to have_chain_index(0)
|
38
|
+
#
|
39
|
+
# @example Testing workflow interruption point
|
40
|
+
# workflow_result = InterruptedWorkflow.call(data: "invalid")
|
41
|
+
# interrupting_task = workflow_result.chain.find(&:interrupted?)
|
42
|
+
# expect(interrupting_task).to have_chain_index(3)
|
43
|
+
RSpec::Matchers.define :have_chain_index do |expected_index|
|
44
|
+
match do |result|
|
45
|
+
result.index == expected_index
|
46
|
+
end
|
47
|
+
|
48
|
+
failure_message do |result|
|
49
|
+
"expected result to have chain index #{expected_index}, but was #{result.index}"
|
50
|
+
end
|
51
|
+
|
52
|
+
failure_message_when_negated do |_result|
|
53
|
+
"expected result not to have chain index #{expected_index}, but it did"
|
54
|
+
end
|
55
|
+
|
56
|
+
description do
|
57
|
+
"have chain index #{expected_index}"
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has specific context side effects.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object's context contains expected values
|
6
|
+
# or side effects that were set during task execution. Tasks often modify the context
|
7
|
+
# to store computed values, intermediate results, or other data that needs to be
|
8
|
+
# passed between tasks in a workflow. This matcher supports both direct value
|
9
|
+
# comparisons and RSpec matchers for flexible assertions.
|
10
|
+
#
|
11
|
+
# @param expected_effects [Hash] hash of expected context key-value pairs or matchers
|
12
|
+
#
|
13
|
+
# @return [Boolean] true if the context has all expected side effects
|
14
|
+
#
|
15
|
+
# @example Testing simple context values
|
16
|
+
# result = CalculateTask.call(a: 10, b: 20)
|
17
|
+
# expect(result).to have_context(sum: 30, product: 200)
|
18
|
+
#
|
19
|
+
# @example Using RSpec matchers for flexible assertions
|
20
|
+
# result = ProcessUserTask.call(user_id: 123)
|
21
|
+
# expect(result).to have_context(
|
22
|
+
# user: be_a(User),
|
23
|
+
# created_at: be_a(Time),
|
24
|
+
# email: match(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
25
|
+
# )
|
26
|
+
#
|
27
|
+
# @example Testing computed values
|
28
|
+
# result = AnalyzeDataTask.call(data: dataset)
|
29
|
+
# expect(result).to have_context(
|
30
|
+
# average: be_within(0.1).of(15.5),
|
31
|
+
# count: be > 100,
|
32
|
+
# processed: be_truthy
|
33
|
+
# )
|
34
|
+
#
|
35
|
+
# @example Testing workflow context passing
|
36
|
+
# workflow_result = DataProcessingWorkflow.call(input: "raw_data")
|
37
|
+
# expect(workflow_result).to have_context(
|
38
|
+
# raw_data: "raw_data",
|
39
|
+
# processed_data: be_present,
|
40
|
+
# validation_errors: be_empty
|
41
|
+
# )
|
42
|
+
#
|
43
|
+
# @example Negative assertion
|
44
|
+
# result = SimpleTask.call(data: "test")
|
45
|
+
# expect(result).not_to have_context(unexpected_key: "value")
|
46
|
+
#
|
47
|
+
# @example Testing side effects in failed tasks
|
48
|
+
# result = ValidateTask.call(data: "invalid")
|
49
|
+
# expect(result).to have_context(
|
50
|
+
# validation_errors: include("Data is invalid"),
|
51
|
+
# attempted_at: be_a(Time)
|
52
|
+
# )
|
53
|
+
RSpec::Matchers.define :have_context do |expected_effects|
|
54
|
+
match do |result|
|
55
|
+
expected_effects.all? do |key, expected_value|
|
56
|
+
actual_value = result.context.public_send(key)
|
57
|
+
if expected_value.respond_to?(:matches?)
|
58
|
+
expected_value.matches?(actual_value)
|
59
|
+
else
|
60
|
+
actual_value == expected_value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
failure_message do |result|
|
66
|
+
mismatches = expected_effects.filter_map do |key, expected_value|
|
67
|
+
actual_value = result.context.public_send(key)
|
68
|
+
match_result = if expected_value.respond_to?(:matches?)
|
69
|
+
expected_value.matches?(actual_value)
|
70
|
+
else
|
71
|
+
actual_value == expected_value
|
72
|
+
end
|
73
|
+
|
74
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" unless match_result
|
75
|
+
end
|
76
|
+
"expected context to have side effects #{expected_effects}, but #{mismatches.join(', ')}"
|
77
|
+
end
|
78
|
+
|
79
|
+
failure_message_when_negated do |_result|
|
80
|
+
"expected context not to have side effects #{expected_effects}, but it did"
|
81
|
+
end
|
82
|
+
|
83
|
+
description do
|
84
|
+
"have side effects #{expected_effects}"
|
85
|
+
end
|
86
|
+
end
|