chaotic_job 0.10.1 → 0.11.1
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 +9 -0
- data/README.md +8 -5
- data/lib/chaotic_job/scenario.rb +9 -2
- data/lib/chaotic_job/simulation.rb +27 -36
- data/lib/chaotic_job/version.rb +1 -1
- data/lib/chaotic_job.rb +15 -26
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65b239108a3f3c45022378231bb6bd49c85f29fba2441e71a0fc49b06fb91413
|
4
|
+
data.tar.gz: bce5cb7e9beefbaed434d0f4a232341d8cb427eaa0f569b765dfc9da3b692840
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb13d55b1ad97aab48e9d9a87cefb2dd76134a5fd7aae46409b17107dad1aa4070b890e4224139db43b3c53a9f8410bf8befbe758579d06a9d69fffdac049e30
|
7
|
+
data.tar.gz: 93f0394e4138e749beeb1a26bf62f3e14b7345ffcaa9d2b9343db22849568e50108dc0cf9b45d920c3ad1989f2f94b0cd8c1bd8846449ffa310238f264e7d0ea
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.11.1] - 2025-06-18
|
4
|
+
|
5
|
+
- Fix mistaken `job_id` assignment in `Simulation`
|
6
|
+
|
7
|
+
## [0.11.0] - 2025-06-18
|
8
|
+
|
9
|
+
- Allow a Simulation to control the ActiveSupport::Notifications captured, and capture all by default [#20](https://github.com/fractaledmind/chaotic_job/pull/20)
|
10
|
+
- Instead of running a simulation within a test case, introduce a builder class method that dynamically defines each scenario as a test case method. [#21](https://github.com/fractaledmind/chaotic_job/pull/21)
|
11
|
+
|
3
12
|
## [0.10.1] - 2025-06-18
|
4
13
|
|
5
14
|
- Add more switch methods [#19](https://github.com/fractaledmind/chaotic_job/pull/19)
|
data/README.md
CHANGED
@@ -167,10 +167,12 @@ Finally, if you need to inject a glitch right before a particular line of code i
|
|
167
167
|
run_scenario(Job.new, glitch: ChaoticJob::Glitch.before_line("#{__FILE__}:6"))
|
168
168
|
```
|
169
169
|
|
170
|
-
Scenario testing is useful to test the behavior of a job under a specific set of conditions. But, if you want to test the behavior of a job under a variety of conditions, you can use the `
|
170
|
+
Scenario testing is useful to test the behavior of a job under a specific set of conditions. But, if you want to test the behavior of a job under a variety of conditions, you can use the `test_simulation` method. Instead of running a single scenario, a simulation will define a full set of possible error scenarios for your job as individual test cases.
|
171
171
|
|
172
172
|
```ruby
|
173
|
-
|
173
|
+
class TestYourJob < ActiveJob::TestCase
|
174
|
+
include ChaoticJob::Helpers
|
175
|
+
|
174
176
|
class Job < ActiveJob::Base
|
175
177
|
def perform
|
176
178
|
step_1
|
@@ -183,7 +185,8 @@ test "simulation of a simple job" do
|
|
183
185
|
def step_3 = ChaoticJob::Journal.log
|
184
186
|
end
|
185
187
|
|
186
|
-
|
188
|
+
# will dynamically generate a test method for each failure scenario
|
189
|
+
test_simulation(Job.new) do |scenario|
|
187
190
|
assert_operator ChaoticJob::Journal.total, :>=, 3
|
188
191
|
end
|
189
192
|
end
|
@@ -206,9 +209,9 @@ More specifically, it will create a scenario injecting a glitch before every lin
|
|
206
209
|
[:return, "Job#perform"]}>
|
207
210
|
```
|
208
211
|
|
209
|
-
It generates all possible glitch scenarios by performing your job once with a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html) that captures every event executed as a part of your job running. The block that you pass to `
|
212
|
+
It generates all possible glitch scenarios by performing your job once with a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html) that captures every event executed as a part of your job running. The block that you pass to `test_simulation` will be called for each scenario, allowing you to make assertions about the behavior of your job under all scenarios.
|
210
213
|
|
211
|
-
If you want to have the simulation run against a larger collection of scenarios, you can capture a custom callstack using the `ChaoticJob::Tracer` class and pass it to the `
|
214
|
+
If you want to have the simulation run against a larger collection of scenarios, you can capture a custom callstack using the `ChaoticJob::Tracer` class and pass it to the `test_simulation` method as the `callstack` parameter. A `Tracer` is initialized with a block that determines which `TracePoint` events to collect. You then call `capture` with a block that defines the code to be traced. The default `Simulation` tracer collects all events for the passed job and then traces the job execution, essentially like this:
|
212
215
|
|
213
216
|
```ruby
|
214
217
|
job_file_path = YourJob.instance_method(:perform).source_location&.first
|
data/lib/chaotic_job/scenario.rb
CHANGED
@@ -7,7 +7,9 @@ module ChaoticJob
|
|
7
7
|
class Scenario
|
8
8
|
attr_reader :events, :glitch, :job
|
9
9
|
|
10
|
-
|
10
|
+
Event = Struct.new(:name, :started, :finished, :unique_id, :payload)
|
11
|
+
|
12
|
+
def initialize(job, glitch:, raise: RetryableError, capture: nil)
|
11
13
|
@job = job
|
12
14
|
@glitch = (Glitch === glitch) ? glitch : (raise Error.new("glitch: must be a Glitch instance, but got #{glitch.inspect}"))
|
13
15
|
@raise = binding.local_variable_get(:raise)
|
@@ -19,7 +21,7 @@ module ChaoticJob
|
|
19
21
|
@job.class.retry_on RetryableError, attempts: 10, wait: 1, jitter: 0
|
20
22
|
@glitch.set_action { raise @raise }
|
21
23
|
|
22
|
-
ActiveSupport::Notifications.subscribed(->(
|
24
|
+
ActiveSupport::Notifications.subscribed(->(*args) { @events << Event.new(*args) }, @capture) do
|
23
25
|
@glitch.inject! do
|
24
26
|
@job.enqueue
|
25
27
|
if block
|
@@ -73,6 +75,11 @@ module ChaoticJob
|
|
73
75
|
glitch_lines.each do |line|
|
74
76
|
buffer << " #{line}\n"
|
75
77
|
end
|
78
|
+
buffer << " events: [\n"
|
79
|
+
@events.sort_by { |it| it.started }.each do |it|
|
80
|
+
buffer << " #{it.started.utc.iso8601(6)}: #{it.name}\n"
|
81
|
+
end
|
82
|
+
buffer << " ]\n"
|
76
83
|
buffer << ")"
|
77
84
|
|
78
85
|
buffer
|
@@ -1,11 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Simulation.new(job).
|
4
|
-
# Simulation.new(job).variants
|
5
|
-
# Simulation.new(job).scenarios
|
3
|
+
# Simulation.new(job).define { |scenario| ... }
|
6
4
|
module ChaoticJob
|
7
5
|
class Simulation
|
8
|
-
def initialize(job, callstack: nil, variations: nil, test: nil, seed: nil, perform_only_jobs_within: nil)
|
6
|
+
def initialize(job, callstack: nil, variations: nil, test: nil, seed: nil, perform_only_jobs_within: nil, capture: nil)
|
9
7
|
@template = job
|
10
8
|
@callstack = callstack || capture_callstack
|
11
9
|
@variations = variations
|
@@ -13,21 +11,37 @@ module ChaoticJob
|
|
13
11
|
@seed = seed || Random.new_seed
|
14
12
|
@random = Random.new(@seed)
|
15
13
|
@perform_only_jobs_within = perform_only_jobs_within
|
14
|
+
@capture = capture
|
16
15
|
|
16
|
+
@template.class.retry_on RetryableError, attempts: 3, wait: 1, jitter: 0
|
17
17
|
raise Error.new("callstack must be a generated via ChaoticJob::Tracer") unless @callstack.is_a?(Stack)
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
@
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
20
|
+
def define(&assertions)
|
21
|
+
debug "👾 Defining #{@variations || "all"} simulated scenarios of the total #{variants.size} possibilities..."
|
22
|
+
|
23
|
+
scenarios.each do |scenario|
|
24
|
+
test_method_name = "test_simulation_scenario_before_#{scenario.glitch.event}_#{scenario.glitch.key}"
|
25
|
+
perform_only_jobs_within = @perform_only_jobs_within
|
26
|
+
|
27
|
+
@test.define_method(test_method_name) do
|
28
|
+
if perform_only_jobs_within
|
29
|
+
scenario.run do
|
30
|
+
Performer.perform_all_before(perform_only_jobs_within)
|
31
|
+
instance_exec(scenario, &assertions)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
scenario.run
|
35
|
+
instance_exec(scenario, &assertions)
|
36
|
+
end
|
37
|
+
|
38
|
+
assert scenario.glitched?, "Scenario did not execute glitch: #{scenario.glitch}"
|
39
|
+
end
|
28
40
|
end
|
29
41
|
end
|
30
42
|
|
43
|
+
private
|
44
|
+
|
31
45
|
def variants
|
32
46
|
error_locations = @callstack.map do |event, key|
|
33
47
|
["before_#{event}", key]
|
@@ -43,12 +57,10 @@ module ChaoticJob
|
|
43
57
|
job = clone_job_template
|
44
58
|
glitch = Glitch.public_send(event, key)
|
45
59
|
job.job_id = [job.job_id.split("-").first, glitch.event, glitch.key].join("-")
|
46
|
-
Scenario.new(job, glitch: glitch)
|
60
|
+
Scenario.new(job, glitch: glitch, capture: @capture)
|
47
61
|
end
|
48
62
|
end
|
49
63
|
|
50
|
-
private
|
51
|
-
|
52
64
|
def capture_callstack
|
53
65
|
tracer = Tracer.new { |tp| tp.defined_class == @template.class }
|
54
66
|
callstack = tracer.capture do
|
@@ -61,27 +73,6 @@ module ChaoticJob
|
|
61
73
|
callstack
|
62
74
|
end
|
63
75
|
|
64
|
-
def run_scenario(scenario, &assertions)
|
65
|
-
debug "👾 Running simulation with scenario: #{scenario}"
|
66
|
-
@test.before_setup
|
67
|
-
@test.simulation_scenario = scenario
|
68
|
-
|
69
|
-
if @perform_only_jobs_within
|
70
|
-
scenario.run do
|
71
|
-
Performer.perform_all_before(@perform_only_jobs_within)
|
72
|
-
assertions.call(scenario)
|
73
|
-
end
|
74
|
-
else
|
75
|
-
scenario.run
|
76
|
-
assertions.call(scenario)
|
77
|
-
end
|
78
|
-
|
79
|
-
@test.after_teardown
|
80
|
-
@test.assert scenario.glitched?, "Scenario did not execute glitch: #{scenario.glitch}"
|
81
|
-
ensure
|
82
|
-
@test.simulation_scenario = nil
|
83
|
-
end
|
84
|
-
|
85
76
|
def clone_job_template
|
86
77
|
serialized_template = @template.serialize
|
87
78
|
job = ActiveJob::Base.deserialize(serialized_template)
|
data/lib/chaotic_job/version.rb
CHANGED
data/lib/chaotic_job.rb
CHANGED
@@ -80,7 +80,21 @@ module ChaoticJob
|
|
80
80
|
end
|
81
81
|
|
82
82
|
module Helpers
|
83
|
-
|
83
|
+
def self.included(base)
|
84
|
+
base.extend(ClassMethods)
|
85
|
+
end
|
86
|
+
|
87
|
+
module ClassMethods
|
88
|
+
def test_simulation(job, variations: nil, callstack: nil, perform_only_jobs_within: nil, &block)
|
89
|
+
seed = defined?(RSpec) ? RSpec.configuration.seed : Minitest.seed
|
90
|
+
kwargs = {test: self, seed: seed}
|
91
|
+
kwargs[:variations] = variations if variations
|
92
|
+
kwargs[:callstack] = callstack if callstack
|
93
|
+
kwargs[:perform_only_jobs_within] = perform_only_jobs_within if perform_only_jobs_within
|
94
|
+
|
95
|
+
Simulation.new(job, **kwargs).define(&block)
|
96
|
+
end
|
97
|
+
end
|
84
98
|
|
85
99
|
def perform_all_jobs
|
86
100
|
Performer.perform_all
|
@@ -95,16 +109,6 @@ module ChaoticJob
|
|
95
109
|
Performer.perform_all_after(time)
|
96
110
|
end
|
97
111
|
|
98
|
-
def run_simulation(job, variations: nil, callstack: nil, perform_only_jobs_within: nil, &block)
|
99
|
-
seed = defined?(RSpec) ? RSpec.configuration.seed : Minitest.seed
|
100
|
-
kwargs = {test: self, seed: seed}
|
101
|
-
kwargs[:variations] = variations if variations
|
102
|
-
kwargs[:callstack] = callstack if callstack
|
103
|
-
kwargs[:perform_only_jobs_within] = perform_only_jobs_within if perform_only_jobs_within
|
104
|
-
self.simulation_scenario = nil
|
105
|
-
Simulation.new(job, **kwargs).run(&block)
|
106
|
-
end
|
107
|
-
|
108
112
|
def run_scenario(job, glitch:, raise: nil, capture: nil, &block)
|
109
113
|
kwargs = {}
|
110
114
|
|
@@ -130,20 +134,5 @@ module ChaoticJob
|
|
130
134
|
def glitch_before_return(key, return_type = nil, &block)
|
131
135
|
Glitch.before_return(key, return_type, &block)
|
132
136
|
end
|
133
|
-
|
134
|
-
def assert(test, msg = nil)
|
135
|
-
return super unless @simulation_scenario
|
136
|
-
|
137
|
-
contextual_msg = lambda do
|
138
|
-
# copied from the original `assert` method in Minitest::Assertions
|
139
|
-
default_msg = "Expected #{mu_pp test} to be truthy."
|
140
|
-
custom_msg = msg.is_a?(Proc) ? msg.call : msg
|
141
|
-
full_msg = custom_msg || default_msg
|
142
|
-
indented_scenario = @simulation_scenario.to_s.split("\n").join("\n ")
|
143
|
-
" #{indented_scenario}\n#{full_msg}"
|
144
|
-
end
|
145
|
-
|
146
|
-
super(test, contextual_msg)
|
147
|
-
end
|
148
137
|
end
|
149
138
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chaotic_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Margheim
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-06-
|
10
|
+
date: 2025-06-18 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activejob
|