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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 304867076ba4ade8191d2802075fadc2a2bfe83c35bdf3af8217b35b3097fc2d
4
- data.tar.gz: 0a5ca5c1df4253011f0b9894f92424deb85262646eae89d40fd5c293ae6b8263
3
+ metadata.gz: 65b239108a3f3c45022378231bb6bd49c85f29fba2441e71a0fc49b06fb91413
4
+ data.tar.gz: bce5cb7e9beefbaed434d0f4a232341d8cb427eaa0f569b765dfc9da3b692840
5
5
  SHA512:
6
- metadata.gz: 6877fdc030d30b9c53486a8d60f8be9d70dca9c3327e53f080e58a6dc73c6ca067f4ff91cb50f7de8e1b0812dddcbde5a5fafc37b8a5732d2e3119d614525044
7
- data.tar.gz: 772b32d2e2d444cd0d9b1da623a3049071234b6548aab13eac409d15abccee8a49c476211e84d9e774ea6a6bf4a2675af1c4a991ce35e05e49cf6af658bc55ff
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 `run_simulation` method. Instead of running a single scenario, a simulation will run the full set of possible error scenarios for your job.
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
- test "simulation of a simple job" do
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
- run_simulation(Job.new) do |scenario|
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 `run_simulation` will be called for each scenario, allowing you to make assertions about the behavior of your job under all scenarios.
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 `run_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:
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
@@ -7,7 +7,9 @@ module ChaoticJob
7
7
  class Scenario
8
8
  attr_reader :events, :glitch, :job
9
9
 
10
- def initialize(job, glitch:, raise: RetryableError, capture: /active_job/)
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(->(event) { @events << event.dup }, @capture) do
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).run { |scenario| ... }
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 run(&assertions)
21
- @template.class.retry_on RetryableError, attempts: 3, wait: 1, jitter: 0
22
-
23
- debug "👾 Running #{@variations || "all"} simulations of the total #{variants.size} possibilities..."
24
-
25
- scenarios.map do |scenario|
26
- run_scenario(scenario, &assertions)
27
- print "·"
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChaoticJob
4
- VERSION = "0.10.1"
4
+ VERSION = "0.11.1"
5
5
  end
data/lib/chaotic_job.rb CHANGED
@@ -80,7 +80,21 @@ module ChaoticJob
80
80
  end
81
81
 
82
82
  module Helpers
83
- attr_accessor :simulation_scenario
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.10.1
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-17 00:00:00.000000000 Z
10
+ date: 2025-06-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activejob