cmdx 1.0.0 → 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 +5 -0
- data/CHANGELOG.md +101 -49
- data/README.md +2 -1
- data/docs/ai_prompts.md +10 -0
- data/docs/basics/call.md +11 -2
- data/docs/basics/chain.md +10 -1
- data/docs/basics/context.md +9 -0
- data/docs/basics/setup.md +9 -0
- data/docs/callbacks.md +14 -37
- data/docs/configuration.md +68 -27
- data/docs/getting_started.md +11 -0
- data/docs/internationalization.md +148 -0
- data/docs/interruptions/exceptions.md +10 -1
- data/docs/interruptions/faults.md +11 -2
- data/docs/interruptions/halt.md +9 -0
- data/docs/logging.md +14 -4
- data/docs/middlewares.md +53 -43
- data/docs/outcomes/result.md +9 -0
- data/docs/outcomes/states.md +9 -0
- data/docs/outcomes/statuses.md +9 -0
- data/docs/parameters/coercions.md +58 -38
- data/docs/parameters/defaults.md +10 -1
- data/docs/parameters/definitions.md +9 -0
- data/docs/parameters/namespacing.md +9 -0
- data/docs/parameters/validations.md +8 -67
- data/docs/testing.md +22 -13
- data/docs/tips_and_tricks.md +9 -0
- data/docs/workflows.md +14 -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 +61 -11
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +35 -0
- data/lib/locales/cs.yml +35 -0
- data/lib/locales/da.yml +35 -0
- data/lib/locales/de.yml +35 -0
- data/lib/locales/el.yml +35 -0
- data/lib/locales/en.yml +19 -20
- data/lib/locales/es.yml +19 -20
- data/lib/locales/fi.yml +35 -0
- data/lib/locales/fr.yml +35 -0
- data/lib/locales/he.yml +35 -0
- data/lib/locales/hi.yml +35 -0
- data/lib/locales/it.yml +35 -0
- data/lib/locales/ja.yml +35 -0
- data/lib/locales/ko.yml +35 -0
- data/lib/locales/nl.yml +35 -0
- data/lib/locales/no.yml +35 -0
- data/lib/locales/pl.yml +35 -0
- data/lib/locales/pt.yml +35 -0
- data/lib/locales/ru.yml +35 -0
- data/lib/locales/sv.yml +35 -0
- data/lib/locales/th.yml +35 -0
- data/lib/locales/tr.yml +35 -0
- data/lib/locales/vi.yml +35 -0
- data/lib/locales/zh.yml +35 -0
- metadata +57 -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,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has no metadata.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object's metadata hash is empty.
|
6
|
+
# Metadata is typically used to store additional information about task execution
|
7
|
+
# such as failure reasons, timing details, error contexts, or other diagnostic data.
|
8
|
+
# Testing for empty metadata is useful when verifying that successful tasks execute
|
9
|
+
# cleanly without generating unnecessary metadata, or when ensuring default states.
|
10
|
+
#
|
11
|
+
# @return [Boolean] true if the result's metadata hash is empty
|
12
|
+
#
|
13
|
+
# @example Testing successful task with no metadata
|
14
|
+
# result = SimpleTask.call(data: "valid")
|
15
|
+
# expect(result).to have_empty_metadata
|
16
|
+
#
|
17
|
+
# @example Testing clean task execution
|
18
|
+
# result = CalculateTask.call(a: 10, b: 20)
|
19
|
+
# expect(result).to be_success.and have_empty_metadata
|
20
|
+
#
|
21
|
+
# @example Testing default result state
|
22
|
+
# result = MyTask.new.result
|
23
|
+
# expect(result).to have_empty_metadata
|
24
|
+
#
|
25
|
+
# @example Negative assertion - expecting metadata to be present
|
26
|
+
# result = ValidationTask.call(data: "invalid")
|
27
|
+
# expect(result).not_to have_empty_metadata
|
28
|
+
#
|
29
|
+
# @example Comparing with tasks that set metadata
|
30
|
+
# successful_result = CleanTask.call(data: "valid")
|
31
|
+
# failed_result = FailingTask.call(data: "invalid")
|
32
|
+
# expect(successful_result).to have_empty_metadata
|
33
|
+
# expect(failed_result).not_to have_empty_metadata
|
34
|
+
#
|
35
|
+
# @example Testing metadata cleanup
|
36
|
+
# result = ResetTask.call(clear_metadata: true)
|
37
|
+
# expect(result).to have_empty_metadata
|
38
|
+
RSpec::Matchers.define :have_empty_metadata do
|
39
|
+
match do |result|
|
40
|
+
result.metadata.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
failure_message do |result|
|
44
|
+
"expected metadata to be empty, but was #{result.metadata}"
|
45
|
+
end
|
46
|
+
|
47
|
+
failure_message_when_negated do |_result|
|
48
|
+
"expected metadata not to be empty, but it was"
|
49
|
+
end
|
50
|
+
|
51
|
+
description do
|
52
|
+
"have empty metadata"
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has a good outcome.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object represents a successful completion,
|
6
|
+
# which includes both successful and skipped results. A result has a good outcome when
|
7
|
+
# its status is either "success" or "skipped" (i.e., anything other than "failed").
|
8
|
+
# This is useful for testing that tasks complete without errors, even if they were
|
9
|
+
# skipped due to conditions, as skipped tasks are still considered successful outcomes.
|
10
|
+
#
|
11
|
+
# @return [Boolean] true if the result has a good outcome (success or skipped)
|
12
|
+
#
|
13
|
+
# @example Testing successful task outcome
|
14
|
+
# result = ProcessDataTask.call(data: "valid")
|
15
|
+
# expect(result).to have_good_outcome
|
16
|
+
#
|
17
|
+
# @example Testing skipped task outcome (still good)
|
18
|
+
# result = BackupTask.call(force: false)
|
19
|
+
# expect(result).to have_good_outcome # Skipped is still good
|
20
|
+
#
|
21
|
+
# @example Testing non-error completion paths
|
22
|
+
# result = ConditionalTask.call(condition: false)
|
23
|
+
# expect(result).to have_good_outcome # Either success or skip is good
|
24
|
+
#
|
25
|
+
# @example Negative assertion for failed tasks
|
26
|
+
# result = ValidationTask.call(data: "invalid")
|
27
|
+
# expect(result).not_to have_good_outcome
|
28
|
+
#
|
29
|
+
# @example Distinguishing from bad outcomes
|
30
|
+
# successful_result = CleanTask.call(data: "valid")
|
31
|
+
# failed_result = BrokenTask.call(data: "invalid")
|
32
|
+
# expect(successful_result).to have_good_outcome
|
33
|
+
# expect(failed_result).to have_bad_outcome
|
34
|
+
#
|
35
|
+
# @example Testing workflow completion
|
36
|
+
# workflow_result = ProcessingWorkflow.call(data: "test")
|
37
|
+
# expect(workflow_result).to have_good_outcome.and be_complete
|
38
|
+
RSpec::Matchers.define :have_good_outcome do
|
39
|
+
match(&:good?)
|
40
|
+
|
41
|
+
failure_message do |result|
|
42
|
+
"expected result to have good outcome (success or skipped), but was #{result.status}"
|
43
|
+
end
|
44
|
+
|
45
|
+
failure_message_when_negated do |result|
|
46
|
+
"expected result not to have good outcome, but it did (status: #{result.status})"
|
47
|
+
end
|
48
|
+
|
49
|
+
description do
|
50
|
+
"have good outcome"
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has specific metadata.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object's metadata hash contains expected
|
6
|
+
# key-value pairs. Metadata is typically used to store additional information about
|
7
|
+
# task execution such as failure reasons, timing details, error contexts, or other
|
8
|
+
# diagnostic data. The matcher supports both direct value comparisons and RSpec
|
9
|
+
# matchers for flexible assertions, and can be chained with `including` for
|
10
|
+
# additional metadata expectations.
|
11
|
+
#
|
12
|
+
# @param expected_metadata [Hash] optional hash of expected metadata key-value pairs
|
13
|
+
#
|
14
|
+
# @return [Boolean] true if the result's metadata contains all expected pairs
|
15
|
+
#
|
16
|
+
# @example Testing basic metadata
|
17
|
+
# result = FailedTask.call(data: "invalid")
|
18
|
+
# expect(result).to have_metadata(reason: "validation_failed", code: 422)
|
19
|
+
#
|
20
|
+
# @example Using RSpec matchers for flexible assertions
|
21
|
+
# result = ProcessingTask.call(data: "test")
|
22
|
+
# expect(result).to have_metadata(
|
23
|
+
# started_at: be_a(Time),
|
24
|
+
# duration: be > 0,
|
25
|
+
# user_id: be_present
|
26
|
+
# )
|
27
|
+
#
|
28
|
+
# @example Using the including chain for additional metadata
|
29
|
+
# result = ValidationTask.call(data: "invalid")
|
30
|
+
# expect(result).to have_metadata(reason: "validation_failed")
|
31
|
+
# .including(field: "email", rule: "format")
|
32
|
+
#
|
33
|
+
# @example Testing failure metadata
|
34
|
+
# result = DatabaseTask.call(connection: nil)
|
35
|
+
# expect(result).to have_metadata(
|
36
|
+
# error_class: "ConnectionError",
|
37
|
+
# error_message: include("connection failed"),
|
38
|
+
# retry_count: 3
|
39
|
+
# )
|
40
|
+
#
|
41
|
+
# @example Testing skip metadata
|
42
|
+
# result = BackupTask.call(force: false)
|
43
|
+
# expect(result).to have_metadata(
|
44
|
+
# reason: "backup_not_needed",
|
45
|
+
# last_backup: be_a(Time),
|
46
|
+
# next_backup: be_a(Time)
|
47
|
+
# )
|
48
|
+
#
|
49
|
+
# @example Negative assertion
|
50
|
+
# result = CleanTask.call(data: "valid")
|
51
|
+
# expect(result).not_to have_metadata(error_code: anything)
|
52
|
+
#
|
53
|
+
# @example Complex metadata validation
|
54
|
+
# result = WorkflowTask.call(data: "complex")
|
55
|
+
# expect(result).to have_metadata(
|
56
|
+
# steps_completed: be >= 5,
|
57
|
+
# total_steps: 10,
|
58
|
+
# performance_data: be_a(Hash)
|
59
|
+
# ).including(
|
60
|
+
# memory_usage: be_within(10).of(100),
|
61
|
+
# cpu_time: be_positive
|
62
|
+
# )
|
63
|
+
RSpec::Matchers.define :have_metadata do |expected_metadata = {}|
|
64
|
+
match do |result|
|
65
|
+
expected_metadata.all? do |key, value|
|
66
|
+
actual_value = result.metadata[key]
|
67
|
+
if value.respond_to?(:matches?)
|
68
|
+
value.matches?(actual_value)
|
69
|
+
else
|
70
|
+
actual_value == value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
chain :including do |metadata|
|
76
|
+
@additional_metadata = metadata
|
77
|
+
end
|
78
|
+
|
79
|
+
match do |result|
|
80
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
81
|
+
all_metadata.all? do |key, value|
|
82
|
+
actual_value = result.metadata[key]
|
83
|
+
if value.respond_to?(:matches?)
|
84
|
+
value.matches?(actual_value)
|
85
|
+
else
|
86
|
+
actual_value == value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
failure_message do |result|
|
92
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
93
|
+
mismatches = all_metadata.filter_map do |key, expected_value|
|
94
|
+
actual_value = result.metadata[key]
|
95
|
+
match_result = if expected_value.respond_to?(:matches?)
|
96
|
+
expected_value.matches?(actual_value)
|
97
|
+
else
|
98
|
+
actual_value == expected_value
|
99
|
+
end
|
100
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" unless match_result
|
101
|
+
end
|
102
|
+
"expected metadata to include #{all_metadata}, but #{mismatches.join(', ')}"
|
103
|
+
end
|
104
|
+
|
105
|
+
failure_message_when_negated do |_result|
|
106
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
107
|
+
"expected metadata not to include #{all_metadata}, but it did"
|
108
|
+
end
|
109
|
+
|
110
|
+
description do
|
111
|
+
all_metadata = expected_metadata.merge(@additional_metadata || {})
|
112
|
+
"have metadata #{all_metadata}"
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has preserved specific context values.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object's context contains values that were
|
6
|
+
# preserved from the original input or previous task execution. Unlike `have_context`
|
7
|
+
# which tests for side effects and new values, this matcher specifically verifies
|
8
|
+
# that certain context attributes retained their expected values throughout task
|
9
|
+
# execution, ensuring data integrity and proper context passing between tasks.
|
10
|
+
#
|
11
|
+
# @param preserved_attributes [Hash] hash of expected preserved context key-value pairs
|
12
|
+
#
|
13
|
+
# @return [Boolean] true if the context has preserved all expected attributes
|
14
|
+
#
|
15
|
+
# @example Testing basic context preservation
|
16
|
+
# result = ProcessDataTask.call(user_id: 123, data: "input")
|
17
|
+
# expect(result).to have_preserved_context(user_id: 123, data: "input")
|
18
|
+
#
|
19
|
+
# @example Testing workflow context preservation
|
20
|
+
# workflow_result = UserWorkflow.call(user_id: 456, email: "user@example.com")
|
21
|
+
# expect(workflow_result).to have_preserved_context(
|
22
|
+
# user_id: 456,
|
23
|
+
# email: "user@example.com"
|
24
|
+
# )
|
25
|
+
#
|
26
|
+
# @example Testing preservation through multiple tasks
|
27
|
+
# result = MultiStepTask.call(original_data: "important", temp_data: "process")
|
28
|
+
# expect(result).to have_preserved_context(original_data: "important")
|
29
|
+
#
|
30
|
+
# @example Testing that critical data survives failures
|
31
|
+
# result = FailingTask.call(user_id: 789, critical_flag: true)
|
32
|
+
# expect(result).to have_preserved_context(
|
33
|
+
# user_id: 789,
|
34
|
+
# critical_flag: true
|
35
|
+
# )
|
36
|
+
#
|
37
|
+
# @example Negative assertion for modified context
|
38
|
+
# result = TransformTask.call(data: "original")
|
39
|
+
# expect(result).not_to have_preserved_context(data: "original")
|
40
|
+
#
|
41
|
+
# @example Testing partial preservation
|
42
|
+
# result = SelectiveTask.call(keep_this: "value", change_this: "old")
|
43
|
+
# expect(result).to have_preserved_context(keep_this: "value")
|
44
|
+
RSpec::Matchers.define :have_preserved_context do |preserved_attributes|
|
45
|
+
match do |result|
|
46
|
+
preserved_attributes.all? do |key, expected_value|
|
47
|
+
result.context.public_send(key) == expected_value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
failure_message do |result|
|
52
|
+
mismatches = preserved_attributes.filter_map do |key, expected_value|
|
53
|
+
actual_value = result.context.public_send(key)
|
54
|
+
"#{key}: expected #{expected_value}, got #{actual_value}" if actual_value != expected_value
|
55
|
+
end
|
56
|
+
"expected context to preserve #{preserved_attributes}, but #{mismatches.join(', ')}"
|
57
|
+
end
|
58
|
+
|
59
|
+
failure_message_when_negated do |_result|
|
60
|
+
"expected context not to preserve #{preserved_attributes}, but it did"
|
61
|
+
end
|
62
|
+
|
63
|
+
description do
|
64
|
+
"preserve context #{preserved_attributes}"
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has received a thrown failure.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object represents a failure that was
|
6
|
+
# thrown from another task and received by this task. This is distinct from
|
7
|
+
# failures that were caused by the task itself or thrown by the task to others.
|
8
|
+
# A result has received a thrown failure when it's both failed and the failure
|
9
|
+
# was propagated from elsewhere in the chain, making this useful for testing
|
10
|
+
# error propagation and workflow failure handling.
|
11
|
+
#
|
12
|
+
# @return [Boolean] true if the result is failed and received a thrown failure
|
13
|
+
#
|
14
|
+
# @example Testing error propagation in workflows
|
15
|
+
# workflow_result = ProcessingWorkflow.call(data: "invalid")
|
16
|
+
# receiving_task = workflow_result.chain.find { |r| r.thrown_failure? }
|
17
|
+
# expect(receiving_task).to have_received_thrown_failure
|
18
|
+
#
|
19
|
+
# @example Testing downstream task failure handling
|
20
|
+
# result = CleanupTask.call(previous_task_failed: true)
|
21
|
+
# expect(result).to have_received_thrown_failure
|
22
|
+
#
|
23
|
+
# @example Distinguishing failure types in chain
|
24
|
+
# workflow_result = MultiStepWorkflow.call(data: "problematic")
|
25
|
+
# original_failure = workflow_result.chain.find(&:caused_failure?)
|
26
|
+
# received_failure = workflow_result.chain.find(&:thrown_failure?)
|
27
|
+
# expect(original_failure).to have_caused_failure
|
28
|
+
# expect(received_failure).to have_received_thrown_failure
|
29
|
+
#
|
30
|
+
# @example Testing error handling middleware
|
31
|
+
# result = ErrorHandlingTask.call(upstream_error: error_obj)
|
32
|
+
# expect(result).to have_received_thrown_failure
|
33
|
+
#
|
34
|
+
# @example Negative assertion for self-caused failures
|
35
|
+
# result = ValidatingTask.call(data: "invalid")
|
36
|
+
# expect(result).not_to have_received_thrown_failure
|
37
|
+
#
|
38
|
+
# @example Testing workflow interruption propagation
|
39
|
+
# workflow_result = InterruptedWorkflow.call(data: "test")
|
40
|
+
# interrupted_tasks = workflow_result.chain.select(&:thrown_failure?)
|
41
|
+
# interrupted_tasks.each do |task|
|
42
|
+
# expect(task).to have_received_thrown_failure
|
43
|
+
# end
|
44
|
+
RSpec::Matchers.define :have_received_thrown_failure do
|
45
|
+
match do |result|
|
46
|
+
result.failed? && result.thrown_failure?
|
47
|
+
end
|
48
|
+
|
49
|
+
failure_message do |result|
|
50
|
+
if result.failed?
|
51
|
+
"expected result to have received thrown failure, but it #{result.caused_failure? ? 'caused' : 'threw'} failure instead"
|
52
|
+
else
|
53
|
+
"expected result to have received thrown failure, but it was not failed (status: #{result.status})"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
failure_message_when_negated do |_result|
|
58
|
+
"expected result not to have received thrown failure, but it did"
|
59
|
+
end
|
60
|
+
|
61
|
+
description do
|
62
|
+
"have received thrown failure"
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has runtime information.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object has recorded runtime information
|
6
|
+
# from task execution. Runtime represents the elapsed time taken to execute the
|
7
|
+
# task, measured in seconds as a Float. The matcher can be used to verify that
|
8
|
+
# runtime was captured, or to test that runtime meets specific expectations
|
9
|
+
# using direct values or RSpec matchers for performance testing.
|
10
|
+
#
|
11
|
+
# @param expected_runtime [Float, RSpec::Matchers::BuiltIn::BaseMatcher, nil]
|
12
|
+
# optional expected runtime value or matcher
|
13
|
+
#
|
14
|
+
# @return [Boolean] true if the result has runtime and optionally matches expected value
|
15
|
+
#
|
16
|
+
# @example Testing that runtime was captured
|
17
|
+
# result = ProcessDataTask.call(data: "test")
|
18
|
+
# expect(result).to have_runtime
|
19
|
+
#
|
20
|
+
# @example Testing specific runtime value
|
21
|
+
# result = QuickTask.call(data: "simple")
|
22
|
+
# expect(result).to have_runtime(0.1)
|
23
|
+
#
|
24
|
+
# @example Testing runtime with RSpec matchers
|
25
|
+
# result = ProcessingTask.call(data: "complex")
|
26
|
+
# expect(result).to have_runtime(be > 0.5)
|
27
|
+
#
|
28
|
+
# @example Testing runtime ranges
|
29
|
+
# result = OptimizedTask.call(data: "test")
|
30
|
+
# expect(result).to have_runtime(be_between(0.1, 1.0))
|
31
|
+
#
|
32
|
+
# @example Testing performance constraints
|
33
|
+
# result = PerformanceCriticalTask.call(data: "large_dataset")
|
34
|
+
# expect(result).to have_runtime(be < 2.0)
|
35
|
+
#
|
36
|
+
# @example Negative assertion for unexecuted tasks
|
37
|
+
# result = UnexecutedTask.new.result
|
38
|
+
# expect(result).not_to have_runtime
|
39
|
+
#
|
40
|
+
# @example Testing runtime precision
|
41
|
+
# result = PreciseTask.call(data: "test")
|
42
|
+
# expect(result).to have_runtime(be_within(0.01).of(0.25))
|
43
|
+
RSpec::Matchers.define :have_runtime do |expected_runtime = nil|
|
44
|
+
match do |result|
|
45
|
+
return false if result.runtime.nil?
|
46
|
+
return true if expected_runtime.nil?
|
47
|
+
|
48
|
+
if expected_runtime.respond_to?(:matches?)
|
49
|
+
expected_runtime.matches?(result.runtime)
|
50
|
+
else
|
51
|
+
result.runtime == expected_runtime
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
failure_message do |result|
|
56
|
+
if result.runtime.nil?
|
57
|
+
"expected result to have runtime, but it was nil"
|
58
|
+
elsif expected_runtime
|
59
|
+
"expected result runtime to #{expected_runtime}, but was #{result.runtime}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
failure_message_when_negated do |result|
|
64
|
+
if expected_runtime
|
65
|
+
"expected result runtime not to #{expected_runtime}, but it was #{result.runtime}"
|
66
|
+
else
|
67
|
+
"expected result not to have runtime, but it was #{result.runtime}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
description do
|
72
|
+
if expected_runtime
|
73
|
+
"have runtime #{expected_runtime}"
|
74
|
+
else
|
75
|
+
"have runtime"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task result has thrown a failure.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Result object represents a failure that was
|
6
|
+
# thrown to another task. This is distinct from failures that were caused by
|
7
|
+
# the task itself or received from other tasks. A result has thrown a failure
|
8
|
+
# when it's both failed and actively passed the failure to another task in
|
9
|
+
# the chain. Optionally verifies that the thrown failure came from a specific
|
10
|
+
# original result, useful for testing complex failure propagation scenarios.
|
11
|
+
#
|
12
|
+
# @param expected_original_result [CMDx::Result, nil] optional original result that was thrown
|
13
|
+
#
|
14
|
+
# @return [Boolean] true if the result is failed, threw a failure, and optionally matches expected original
|
15
|
+
#
|
16
|
+
# @example Testing basic failure throwing
|
17
|
+
# workflow_result = ProcessingWorkflow.call(data: "invalid")
|
18
|
+
# throwing_task = workflow_result.chain.find(&:threw_failure?)
|
19
|
+
# expect(throwing_task).to have_thrown_failure
|
20
|
+
#
|
21
|
+
# @example Testing failure propagation with specific original
|
22
|
+
# workflow_result = MultiStepWorkflow.call(data: "problematic")
|
23
|
+
# original_failure = workflow_result.chain.find(&:caused_failure?)
|
24
|
+
# throwing_task = workflow_result.chain.find(&:threw_failure?)
|
25
|
+
# expect(throwing_task).to have_thrown_failure(original_failure)
|
26
|
+
#
|
27
|
+
# @example Testing middleware failure handling
|
28
|
+
# result = ErrorHandlingMiddleware.call(upstream_failure: failure_obj)
|
29
|
+
# expect(result).to have_thrown_failure
|
30
|
+
#
|
31
|
+
# @example Distinguishing failure types in chain
|
32
|
+
# workflow_result = FailingWorkflow.call(data: "invalid")
|
33
|
+
# caused_task = workflow_result.chain.find(&:caused_failure?)
|
34
|
+
# threw_task = workflow_result.chain.find(&:threw_failure?)
|
35
|
+
# received_task = workflow_result.chain.find(&:thrown_failure?)
|
36
|
+
# expect(caused_task).to have_caused_failure
|
37
|
+
# expect(threw_task).to have_thrown_failure
|
38
|
+
# expect(received_task).to have_received_thrown_failure
|
39
|
+
#
|
40
|
+
# @example Negative assertion for self-caused failures
|
41
|
+
# result = ValidatingTask.call(data: "invalid")
|
42
|
+
# expect(result).not_to have_thrown_failure
|
43
|
+
#
|
44
|
+
# @example Testing workflow interruption propagation
|
45
|
+
# workflow_result = InterruptedWorkflow.call(data: "test")
|
46
|
+
# propagating_tasks = workflow_result.chain.select(&:threw_failure?)
|
47
|
+
# propagating_tasks.each do |task|
|
48
|
+
# expect(task).to have_thrown_failure
|
49
|
+
# end
|
50
|
+
RSpec::Matchers.define :have_thrown_failure do |expected_original_result = nil|
|
51
|
+
match do |result|
|
52
|
+
result.failed? &&
|
53
|
+
result.threw_failure? &&
|
54
|
+
(expected_original_result.nil? || result.threw_failure == expected_original_result)
|
55
|
+
end
|
56
|
+
|
57
|
+
failure_message do |result|
|
58
|
+
messages = []
|
59
|
+
messages << "expected result to be failed, but was #{result.status}" unless result.failed?
|
60
|
+
messages << "expected result to have thrown failure, but it #{result.caused_failure? ? 'caused' : 'received'} failure instead" unless result.threw_failure?
|
61
|
+
|
62
|
+
messages << "expected to throw failure from #{expected_original_result}, but threw from #{result.threw_failure}" if expected_original_result && result.threw_failure != expected_original_result
|
63
|
+
|
64
|
+
messages.join(", ")
|
65
|
+
end
|
66
|
+
|
67
|
+
failure_message_when_negated do |_result|
|
68
|
+
"expected result not to have thrown failure, but it did"
|
69
|
+
end
|
70
|
+
|
71
|
+
description do
|
72
|
+
desc = "have thrown failure"
|
73
|
+
desc += " from #{expected_original_result}" if expected_original_result
|
74
|
+
desc
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task class is well-formed.
|
4
|
+
#
|
5
|
+
# This matcher checks if a task class meets all the requirements to be a properly
|
6
|
+
# structured CMDx::Task. A well-formed task must inherit from CMDx::Task, implement
|
7
|
+
# the call method, and have properly initialized registries for parameters, callbacks,
|
8
|
+
# and middlewares. This is essential for ensuring task classes will function correctly
|
9
|
+
# within the CMDx framework and can be used in workflows.
|
10
|
+
#
|
11
|
+
# @return [Boolean] true if the task class is well-formed with all required components
|
12
|
+
#
|
13
|
+
# @example Testing a basic task class
|
14
|
+
# class MyTask < CMDx::Task
|
15
|
+
# def call; end
|
16
|
+
# end
|
17
|
+
# expect(MyTask).to be_well_formed_task
|
18
|
+
#
|
19
|
+
# @example Testing a task with parameters, callbacks and middlewares
|
20
|
+
# class ComplexTask < CMDx::Task
|
21
|
+
# before_validation :refresh_cache
|
22
|
+
# use :middleware, CMDx::Middlewares::Timeout, timeout: 10
|
23
|
+
# required :data
|
24
|
+
# def call; end
|
25
|
+
# end
|
26
|
+
# expect(ComplexTask).to be_well_formed_task
|
27
|
+
#
|
28
|
+
# @example Testing generated task classes
|
29
|
+
# task_class = Class.new(CMDx::Task) { def call; end }
|
30
|
+
# expect(task_class).to be_well_formed_task
|
31
|
+
#
|
32
|
+
# @example Negative assertion for malformed tasks
|
33
|
+
# class BrokenTask; end # Missing inheritance
|
34
|
+
# expect(BrokenTask).not_to be_well_formed_task
|
35
|
+
RSpec::Matchers.define :be_well_formed_task do
|
36
|
+
match do |task_class|
|
37
|
+
(task_class < CMDx::Task) &&
|
38
|
+
task_class.instance_methods.include?(:call) &&
|
39
|
+
task_class.cmd_parameters.is_a?(CMDx::ParameterRegistry) &&
|
40
|
+
task_class.cmd_callbacks.is_a?(CMDx::CallbackRegistry) &&
|
41
|
+
task_class.cmd_middlewares.is_a?(CMDx::MiddlewareRegistry)
|
42
|
+
end
|
43
|
+
|
44
|
+
failure_message do |task_class|
|
45
|
+
issues = []
|
46
|
+
issues << "does not inherit from CMDx::Task" unless task_class < CMDx::Task
|
47
|
+
issues << "does not implement call method" unless task_class.instance_methods.include?(:call)
|
48
|
+
issues << "does not have parameter registry" unless task_class.cmd_parameters.is_a?(CMDx::ParameterRegistry)
|
49
|
+
issues << "does not have callback registry" unless task_class.cmd_callbacks.is_a?(CMDx::CallbackRegistry)
|
50
|
+
issues << "does not have middleware registry" unless task_class.cmd_middlewares.is_a?(CMDx::MiddlewareRegistry)
|
51
|
+
|
52
|
+
"expected task to be well-formed, but #{issues.join(', ')}"
|
53
|
+
end
|
54
|
+
|
55
|
+
failure_message_when_negated do |_task_class|
|
56
|
+
"expected task not to be well-formed, but it was"
|
57
|
+
end
|
58
|
+
|
59
|
+
description do
|
60
|
+
"be a well-formed task"
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher for asserting that a task class has a specific callback.
|
4
|
+
#
|
5
|
+
# This matcher checks if a CMDx::Task class has registered a callback with the
|
6
|
+
# specified name. Callbacks are methods that execute before, after, or around
|
7
|
+
# the main task logic. The matcher can optionally verify that the callback has
|
8
|
+
# a specific callable (method name, proc, or lambda) using the `with_callable`
|
9
|
+
# chain method for more precise callback validation.
|
10
|
+
#
|
11
|
+
# @param callback_name [Symbol, String] the name of the callback to check for
|
12
|
+
#
|
13
|
+
# @return [Boolean] true if the task has the specified callback and optionally the expected callable
|
14
|
+
#
|
15
|
+
# @example Testing basic callback presence
|
16
|
+
# class MyTask < CMDx::Task
|
17
|
+
# before_execution :validate_input
|
18
|
+
# def call; end
|
19
|
+
# end
|
20
|
+
# expect(MyTask).to have_callback(:before_execution)
|
21
|
+
#
|
22
|
+
# @example Testing callback with specific callable
|
23
|
+
# class ProcessTask < CMDx::Task
|
24
|
+
# after_execution :log_completion
|
25
|
+
# def call; end
|
26
|
+
# end
|
27
|
+
# expect(ProcessTask).to have_callback(:after_execution).with_callable(:log_completion)
|
28
|
+
#
|
29
|
+
# @example Testing callbacks with procs
|
30
|
+
# class CustomTask < CMDx::Task
|
31
|
+
# before_execution -> { puts "Starting" }
|
32
|
+
# def call; end
|
33
|
+
# end
|
34
|
+
# expect(CustomTask).to have_callback(:before_execution)
|
35
|
+
#
|
36
|
+
# @example Negative assertion
|
37
|
+
# class SimpleTask < CMDx::Task
|
38
|
+
# def call; end
|
39
|
+
# end
|
40
|
+
# expect(SimpleTask).not_to have_callback(:before_execution)
|
41
|
+
RSpec::Matchers.define :have_callback do |callback_name|
|
42
|
+
match do |task_class|
|
43
|
+
task_class.cmd_callbacks.registered?(callback_name)
|
44
|
+
end
|
45
|
+
|
46
|
+
chain :with_callable do |callable|
|
47
|
+
@expected_callable = callable
|
48
|
+
end
|
49
|
+
|
50
|
+
match do |task_class|
|
51
|
+
callbacks_registered = task_class.cmd_callbacks.registered?(callback_name)
|
52
|
+
return false unless callbacks_registered
|
53
|
+
|
54
|
+
if @expected_callable
|
55
|
+
task_class.cmd_callbacks.find(callback_name).any? do |callback|
|
56
|
+
callback.callable == @expected_callable
|
57
|
+
end
|
58
|
+
else
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
failure_message do |task_class|
|
64
|
+
if @expected_callable
|
65
|
+
"expected task to have callback #{callback_name} with callable #{@expected_callable}, but it didn't"
|
66
|
+
else
|
67
|
+
registered_callbacks = task_class.cmd_callbacks.registered_callbacks
|
68
|
+
"expected task to have callback #{callback_name}, but had #{registered_callbacks}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
failure_message_when_negated do |_task_class|
|
73
|
+
if @expected_callable
|
74
|
+
"expected task not to have callback #{callback_name} with callable #{@expected_callable}, but it did"
|
75
|
+
else
|
76
|
+
"expected task not to have callback #{callback_name}, but it did"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
description do
|
81
|
+
desc = "have callback #{callback_name}"
|
82
|
+
desc += " with callable #{@expected_callable}" if @expected_callable
|
83
|
+
desc
|
84
|
+
end
|
85
|
+
end
|