chaotic_job 0.6.0 โ†’ 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10db3cce1a0eedffe044477a348122d781e19cfbb6f561c9e389926d7de21153
4
- data.tar.gz: a48f0d63526613593a25cc92d1c3eca794adb99466ef697a3c932e581a08434a
3
+ metadata.gz: 94de9ce766a9042a925882e70f29a8022fbd3b22fbd175ed3254260e04962cba
4
+ data.tar.gz: e05d2546038d72cd9181fef3eff223d7adec84225b0fa1267d89d0b3880351ef
5
5
  SHA512:
6
- metadata.gz: a1ea02d93adf652616f25478c8d7f628f30d56137485b10988bd2f08b9a62153327cd60c787555d7ba4286a7c26f94616ee45c92a58778e0689011461a39bb3b
7
- data.tar.gz: 6d94662caef5559bca3de08c69674ad10dd4fa349fbd8434a42dc65c8baa8ba6da13f6cc8d13b9af815f4610148a874ba3f9fa0b9e5c8aa97e7b131afc5c3689
6
+ metadata.gz: 29fedcda5ca8ec4c98fa68d4f1e0661194c558a72a7a70b6b19a445de0e4bb0741e873c1365911315a8be73461248ccbca3005dd604e0a1f8229cf75aa4f7f69
7
+ data.tar.gz: 93423ffd252f3faf4b3c0e0c0e7097fc9e14468d884935fed12421ca653cc99a47819ab80833d4068e0c8ebf23752e45385923384c56e4072e08554d7fe872f2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2025-06-09
4
+
5
+ - Glitch only works with singular event + key definition [#6](https://github.com/fractaledmind/chaotic_job/pull/6)
6
+ - Scenarios assert the glitch was executed [#7](https://github.com/fractaledmind/chaotic_job/pull/7)
7
+ - Add helper methods to create a Glitch of the various kinds [#8](https://github.com/fractaledmind/chaotic_job/pull/8)
8
+ - Improve test coverage [#9](https://github.com/fractaledmind/chaotic_job/pull/9)
9
+
3
10
  ## [0.6.0] - 2025-06-08
4
11
 
5
12
  - `run_scenario` requires a Glitch instance [#5](https://github.com/fractaledmind/chaotic_job/pull/5)
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/chaotic_job.svg)](https://rubygems.org/gems/chaotic_job)
4
4
  [![Gem Downloads](https://img.shields.io/gem/dt/chaotic_job)](https://rubygems.org/gems/chaotic_job)
5
5
  ![Tests](https://github.com/fractaledmind/chaotic_job/actions/workflows/main.yml/badge.svg)
6
- ![Coverage](https://img.shields.io/badge/code%20coverage-92%25-success)
6
+ ![Coverage](https://img.shields.io/badge/code%20coverage-98%25-success)
7
7
  [![Sponsors](https://img.shields.io/github/sponsors/fractaledmind?color=eb4aaa&logo=GitHub%20Sponsors)](https://github.com/sponsors/fractaledmind)
8
8
  [![Twitter Follow](https://img.shields.io/twitter/url?label=%40fractaledmind&style=social&url=https%3A%2F%2Ftwitter.com%2Ffractaledmind)](https://twitter.com/fractaledmind)
9
9
 
@@ -167,17 +167,6 @@ 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
- If you want to simulate multiple glitches affecting a job run, you simply define additional failure points using the fluid interface of the `ChaoticJob::Glitch` class:
171
-
172
- ```ruby
173
- run_scenario(
174
- Job.new,
175
- glitch: ChaoticJob::Glitch
176
- .before_call("Job#step_1")
177
- .before_return("Job#step_3")
178
- )
179
- ```
180
-
181
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.
182
171
 
183
172
  ```ruby
@@ -203,23 +192,21 @@ end
203
192
  More specifically, it will create a scenario injecting a glitch before every line of code executed in your job. So, in this example, the simulation will run 12 scenarios:
204
193
 
205
194
  ```ruby
206
- [
207
- [[:before_line, "test_chaotic_job.rb:69"]],
208
- [[:before_line, "test_chaotic_job.rb:75"]],
209
- [[:before_line, "test_chaotic_job.rb:74"]],
210
- [[:before_line, "test_chaotic_job.rb:74"]],
211
- [[:before_line, "test_chaotic_job.rb:68"]],
212
- [[:before_line, "test_chaotic_job.rb:70"]],
213
- [[:before_line, "test_chaotic_job.rb:68"]],
214
- [[:before_line, "test_chaotic_job.rb:73"]],
215
- [[:before_line, "test_chaotic_job.rb:75"]],
216
- [[:before_line, "test_chaotic_job.rb:69"]],
217
- [[:before_line, "test_chaotic_job.rb:70"]],
218
- [[:before_line, "test_chaotic_job.rb:73"]]
219
- ]
220
- ```
221
-
222
- 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 each line executed in your job. It then computes all possible glitch locations to produce a set of scenarios that will be run. 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.
195
+ #<Set:
196
+ {[:call, "Job#perform"],
197
+ [:line, "file.rb:3"],
198
+ [:call, "Job#step_1"],
199
+ [:return, "Job#step_1"],
200
+ [:line, "file.rb:4"],
201
+ [:call, "Job#step_2"],
202
+ [:return, "Job#step_2"],
203
+ [:line, "file.rb:5"],
204
+ [:call, "Job#step_3"],
205
+ [:return, "Job#step_3"],
206
+ [:return, "Job#perform"]}>
207
+ ```
208
+
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.
223
210
 
224
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:
225
212
 
@@ -8,81 +8,57 @@
8
8
  module ChaoticJob
9
9
  class Glitch
10
10
  def self.before_line(key, &block)
11
- new.before_line(key, &block)
11
+ new(key, :line, &block)
12
12
  end
13
13
 
14
14
  def self.before_call(key, ...)
15
- new.before_call(key, ...)
15
+ new(key, :call, ...)
16
16
  end
17
17
 
18
18
  def self.before_return(key, return_type = nil, &block)
19
- new.before_return(key, return_type, &block)
19
+ new(key, :return, retval: return_type, &block)
20
20
  end
21
21
 
22
- def initialize
23
- @breakpoints = {}
24
- end
25
-
26
- def before_line(key, &block)
27
- set_breakpoint(key, :line, &block)
28
- self
29
- end
30
-
31
- def before_call(key, ...)
32
- set_breakpoint(key, :call, ...)
33
- self
34
- end
35
-
36
- def before_return(key, return_type = nil, &block)
37
- set_breakpoint(key, :return, retval: return_type, &block)
38
- self
22
+ def initialize(key, event, *args, retval: nil, **kwargs, &block)
23
+ @event = event
24
+ @key = key
25
+ @args = args
26
+ @retval = retval
27
+ @kwargs = kwargs
28
+ @block = block
29
+ @executed = false
39
30
  end
40
31
 
41
32
  def set_action(force: false, &block)
42
- @breakpoints.each do |_key, handlers|
43
- handlers.each do |_event, handler|
44
- handler[:block] = block if handler[:block].nil? || force
45
- end
46
- end
47
- self
33
+ @block = block if @block.nil? || force
48
34
  end
49
35
 
50
36
  def inject!(&block)
51
- breakpoints = @breakpoints
52
-
53
- trace = TracePoint.new(:line, :call, :return) do |tp|
37
+ trace = TracePoint.new(@event) do |tp|
54
38
  # :nocov: SimpleCov cannot track code executed _within_ a TracePoint
55
39
  key = derive_key(tp)
56
- matchers = derive_matchers(tp)
40
+ next unless @key == key
57
41
 
58
- next unless (defn = breakpoints.dig(key, tp.event))
59
- next unless matches?(defn, matchers)
42
+ matchers = derive_matchers(tp)
43
+ next unless matches?(matchers)
60
44
 
61
- execute_block(defn)
45
+ execute_block
62
46
  # :nocov:
63
47
  end
64
48
 
65
49
  trace.enable(&block)
66
50
  end
67
51
 
68
- def all_executed?
69
- @breakpoints.all? do |_key, handlers|
70
- handlers.all? { |_event, handler| handler[:executed] }
71
- end
52
+ def executed?
53
+ @executed
72
54
  end
73
55
 
74
56
  private
75
57
 
76
- def set_breakpoint(key, event, *args, retval: nil, **kwargs, &block)
77
- @breakpoints[key] ||= {}
78
- @breakpoints[key][event] = {args: args, kwargs: kwargs, retval: retval, block: block, executed: false}
79
- end
80
-
81
58
  # :nocov: SimpleCov cannot track code executed _within_ a TracePoint
82
- def matches?(defn, matchers)
83
- return true if defn.nil?
59
+ def matches?(matchers)
84
60
  return true if matchers.nil?
85
- return true if defn[:args].empty? && defn[:kwargs].empty? && defn[:retval].nil?
61
+ return true if @args.empty? && @kwargs.empty? && @retval.nil?
86
62
 
87
63
  args = []
88
64
  kwargs = {}
@@ -107,25 +83,24 @@ module ChaoticJob
107
83
  end
108
84
  end
109
85
 
110
- defn[:args].each_with_index do |type, index|
86
+ @args.each_with_index do |type, index|
111
87
  return false unless type === args[index]
112
88
  end
113
89
 
114
- defn[:kwargs].each do |key, type|
90
+ @kwargs.each do |key, type|
115
91
  return false unless type === kwargs[key]
116
92
  end
117
93
 
118
- return false unless defn[:retval] === retval
94
+ return false unless @retval === retval
119
95
 
120
96
  true
121
97
  end
122
98
 
123
- def execute_block(handler)
124
- return unless handler
125
- return if handler[:executed]
99
+ def execute_block
100
+ return if @executed
126
101
 
127
- handler[:executed] = true
128
- handler[:block].call
102
+ @executed = true
103
+ @block.call
129
104
  end
130
105
 
131
106
  def derive_key(trace)
@@ -76,8 +76,11 @@ module ChaoticJob
76
76
  cutoff.from_now
77
77
  in Time
78
78
  cutoff
79
+ else
80
+ raise Error.new("cutoff must be Time or ActiveSupport::Duration, but got #{cutoff.inspect}")
79
81
  end
80
82
  delta = (Time.now - time).abs.floor
83
+
81
84
  changeset = case delta
82
85
  when 0..59 # seconds
83
86
  {usec: 0}
@@ -85,7 +88,7 @@ module ChaoticJob
85
88
  {sec: 0, usec: 0}
86
89
  when 3600..86_399 # hours
87
90
  {min: 0, sec: 0, usec: 0}
88
- when 86_400..Float::INFINITY # days+
91
+ else # days+
89
92
  {hour: 0, min: 0, sec: 0, usec: 0}
90
93
  end
91
94
  time.change(**changeset)
@@ -5,7 +5,7 @@
5
5
 
6
6
  module ChaoticJob
7
7
  class Scenario
8
- attr_reader :events
8
+ attr_reader :events, :glitch, :job
9
9
 
10
10
  def initialize(job, glitch:, raise: RetryableError, capture: /active_job/)
11
11
  @job = job
@@ -30,11 +30,11 @@ module ChaoticJob
30
30
  end
31
31
  end
32
32
 
33
- # TODO: assert that all glitch ran
33
+ self
34
34
  end
35
35
 
36
- def all_glitched?
37
- @glitch.all_executed?
36
+ def glitched?
37
+ @glitch.executed?
38
38
  end
39
39
  end
40
40
  end
@@ -1,54 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Simulation.new(job).run { |scenario| ... }
4
- # Simulation.new(job).permutations
5
4
  # Simulation.new(job).variants
6
5
  # Simulation.new(job).scenarios
7
6
  module ChaoticJob
8
7
  class Simulation
9
- def initialize(job, callstack: nil, depth: 1, variations: 100, test: nil, seed: nil)
8
+ def initialize(job, callstack: nil, variations: nil, test: nil, seed: nil)
10
9
  @template = job
11
10
  @callstack = callstack || capture_callstack
12
- @depth = depth
13
11
  @variations = variations
14
12
  @test = test
15
13
  @seed = seed || Random.new_seed
16
14
  @random = Random.new(@seed)
17
15
 
18
- raise Error.new("callstack must be a generated via the ChaoticJob::Tracer") unless @callstack.is_a?(Stack)
16
+ raise Error.new("callstack must be a generated via ChaoticJob::Tracer") unless @callstack.is_a?(Stack)
19
17
  end
20
18
 
21
- def run(&callback)
22
- @template.class.retry_on RetryableError, attempts: @depth + 2, wait: 1, jitter: 0
19
+ def run(&assertions)
20
+ @template.class.retry_on RetryableError, attempts: 3, wait: 1, jitter: 0
23
21
 
24
- debug "๐Ÿ‘พ Running #{variants.size} simulations of the total #{permutations.size} possibilities..."
22
+ debug "๐Ÿ‘พ Running #{@variations || "all"} simulations of the total #{variants.size} possibilities..."
25
23
 
26
24
  scenarios.map do |scenario|
27
- run_scenario(scenario, &callback)
25
+ run_scenario(scenario, &assertions)
28
26
  print "ยท"
29
27
  end
30
28
  end
31
29
 
32
- def permutations
30
+ def variants
33
31
  error_locations = @callstack.map do |event, key|
34
32
  ["before_#{event}", key]
35
33
  end
36
- error_locations.permutation(@depth)
37
- end
38
34
 
39
- def variants
40
- return permutations if @variations.nil?
35
+ return error_locations if @variations.nil?
41
36
 
42
- permutations.to_a.sample(@variations, random: @random)
37
+ error_locations.sample(@variations, random: @random)
43
38
  end
44
39
 
45
40
  def scenarios
46
- variants.map do |glitches|
41
+ variants.map do |(event, key)|
47
42
  job = clone_job_template
48
- glitch = Glitch.new.tap { |g| glitches.each { |event, key| g.public_send(event, key) } }
49
- scenario = Scenario.new(job, glitch: glitch)
50
- job.job_id = scenario.to_s
51
- scenario
43
+ glitch = Glitch.public_send(event, key)
44
+ Scenario.new(job, glitch: glitch)
52
45
  end
53
46
  end
54
47
 
@@ -64,13 +57,14 @@ module ChaoticJob
64
57
  callstack
65
58
  end
66
59
 
67
- def run_scenario(scenario, &callback)
60
+ def run_scenario(scenario, &assertions)
68
61
  debug "๐Ÿ‘พ Running simulation with scenario: #{scenario}"
69
62
  @test.before_setup
70
- @test.simulation_scenario = scenario.to_s
63
+ @test.simulation_scenario = scenario
71
64
  scenario.run
72
65
  @test.after_teardown
73
- callback.call(scenario)
66
+ @test.assert scenario.glitched?, "Scenario did not execute glitch: #{scenario.glitch}"
67
+ assertions.call(scenario)
74
68
  ensure
75
69
  @test.simulation_scenario = nil
76
70
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChaoticJob
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/chaotic_job.rb CHANGED
@@ -66,10 +66,9 @@ module ChaoticJob
66
66
  Performer.perform_all_after(time)
67
67
  end
68
68
 
69
- def run_simulation(job, depth: nil, variations: nil, callstack: nil, &block)
69
+ def run_simulation(job, variations: nil, callstack: nil, &block)
70
70
  seed = defined?(RSpec) ? RSpec.configuration.seed : Minitest.seed
71
71
  kwargs = {test: self, seed: seed}
72
- kwargs[:depth] = depth if depth
73
72
  kwargs[:variations] = variations if variations
74
73
  kwargs[:callstack] = callstack if callstack
75
74
  self.simulation_scenario = nil
@@ -90,7 +89,17 @@ module ChaoticJob
90
89
  end
91
90
  end
92
91
 
93
- private
92
+ def glitch_before_line(key, &block)
93
+ Glitch.before_line(key, &block)
94
+ end
95
+
96
+ def glitch_before_call(key, ...)
97
+ Glitch.before_call(key, ...)
98
+ end
99
+
100
+ def glitch_before_return(key, return_type = nil, &block)
101
+ Glitch.before_return(key, return_type, &block)
102
+ end
94
103
 
95
104
  def assert(test, msg = nil)
96
105
  return super unless @simulation_scenario
@@ -100,7 +109,8 @@ module ChaoticJob
100
109
  default_msg = "Expected #{mu_pp test} to be truthy."
101
110
  custom_msg = msg.is_a?(Proc) ? msg.call : msg
102
111
  full_msg = custom_msg || default_msg
103
- " #{@simulation_scenario}\n#{full_msg}"
112
+ indented_scenario = @simulation_scenario.to_s.split("\n").join("\n ")
113
+ " #{indented_scenario}\n#{full_msg}"
104
114
  end
105
115
 
106
116
  super(test, contextual_msg)
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-06-08 00:00:00.000000000 Z
10
+ date: 2025-06-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activejob