chaotic_job 0.6.0 โ†’ 0.8.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: eb8c72d5ca7192303c7c337d472c3eb6b02d3fc42eca6261dde846246b6526c6
4
+ data.tar.gz: 34726750b6c695d27f7d9b9260ad45a31710f7f870444d4ce5b2ca2f167cdd97
5
5
  SHA512:
6
- metadata.gz: a1ea02d93adf652616f25478c8d7f628f30d56137485b10988bd2f08b9a62153327cd60c787555d7ba4286a7c26f94616ee45c92a58778e0689011461a39bb3b
7
- data.tar.gz: 6d94662caef5559bca3de08c69674ad10dd4fa349fbd8434a42dc65c8baa8ba6da13f6cc8d13b9af815f4610148a874ba3f9fa0b9e5c8aa97e7b131afc5c3689
6
+ metadata.gz: 92a1d4958ab971e5850b0f60770fbf09c0c2556cdf25eb9cf23d494f8dabdce660ebd47718bb590b65d124c096d2bca391b4198bcbef6b241fe4b11c26468239
7
+ data.tar.gz: 319f7d9d1fa9f78c5490a5e48a58e464907432cdb018a298d365d84a8e514ac7791ea92f8fcc54e275da36da85b4ad411e21406ac46a8ad19c03d1054d478bbd
data/.standard.yml CHANGED
@@ -5,3 +5,5 @@ ruby_version: 3.0
5
5
  ignore:
6
6
  - 'test/**/*':
7
7
  - Lint/ConstantDefinitionInBlock
8
+ - 'lib/**/*':
9
+ - Style/YodaCondition
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.0] - 2025-06-11
4
+
5
+ - Add methods to allow checking if a Scenario matches a particular Glitch key + event pair [#10](https://github.com/fractaledmind/chaotic_job/pull/10)
6
+
7
+ ## [0.7.0] - 2025-06-09
8
+
9
+ - Glitch only works with singular event + key definition [#6](https://github.com/fractaledmind/chaotic_job/pull/6)
10
+ - Scenarios assert the glitch was executed [#7](https://github.com/fractaledmind/chaotic_job/pull/7)
11
+ - Add helper methods to create a Glitch of the various kinds [#8](https://github.com/fractaledmind/chaotic_job/pull/8)
12
+ - Improve test coverage [#9](https://github.com/fractaledmind/chaotic_job/pull/9)
13
+
3
14
  ## [0.6.0] - 2025-06-08
4
15
 
5
16
  - `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,59 @@
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
22
+ attr_reader :key, :event
30
23
 
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
24
+ def initialize(key, event, *args, retval: nil, **kwargs, &block)
25
+ @event = event
26
+ @key = key
27
+ @args = args
28
+ @retval = retval
29
+ @kwargs = kwargs
30
+ @block = block
31
+ @executed = false
39
32
  end
40
33
 
41
34
  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
35
+ @block = block if @block.nil? || force
48
36
  end
49
37
 
50
38
  def inject!(&block)
51
- breakpoints = @breakpoints
52
-
53
- trace = TracePoint.new(:line, :call, :return) do |tp|
39
+ trace = TracePoint.new(@event) do |tp|
54
40
  # :nocov: SimpleCov cannot track code executed _within_ a TracePoint
55
41
  key = derive_key(tp)
56
- matchers = derive_matchers(tp)
42
+ next unless @key == key
57
43
 
58
- next unless (defn = breakpoints.dig(key, tp.event))
59
- next unless matches?(defn, matchers)
44
+ matchers = derive_matchers(tp)
45
+ next unless matches?(matchers)
60
46
 
61
- execute_block(defn)
47
+ execute_block
62
48
  # :nocov:
63
49
  end
64
50
 
65
51
  trace.enable(&block)
66
52
  end
67
53
 
68
- def all_executed?
69
- @breakpoints.all? do |_key, handlers|
70
- handlers.all? { |_event, handler| handler[:executed] }
71
- end
54
+ def executed?
55
+ @executed
72
56
  end
73
57
 
74
58
  private
75
59
 
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
60
  # :nocov: SimpleCov cannot track code executed _within_ a TracePoint
82
- def matches?(defn, matchers)
83
- return true if defn.nil?
61
+ def matches?(matchers)
84
62
  return true if matchers.nil?
85
- return true if defn[:args].empty? && defn[:kwargs].empty? && defn[:retval].nil?
63
+ return true if @args.empty? && @kwargs.empty? && @retval.nil?
86
64
 
87
65
  args = []
88
66
  kwargs = {}
@@ -107,25 +85,24 @@ module ChaoticJob
107
85
  end
108
86
  end
109
87
 
110
- defn[:args].each_with_index do |type, index|
88
+ @args.each_with_index do |type, index|
111
89
  return false unless type === args[index]
112
90
  end
113
91
 
114
- defn[:kwargs].each do |key, type|
92
+ @kwargs.each do |key, type|
115
93
  return false unless type === kwargs[key]
116
94
  end
117
95
 
118
- return false unless defn[:retval] === retval
96
+ return false unless @retval === retval
119
97
 
120
98
  true
121
99
  end
122
100
 
123
- def execute_block(handler)
124
- return unless handler
125
- return if handler[:executed]
101
+ def execute_block
102
+ return if @executed
126
103
 
127
- handler[:executed] = true
128
- handler[:block].call
104
+ @executed = true
105
+ @block.call
129
106
  end
130
107
 
131
108
  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,29 @@ 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
+ end
39
+
40
+ def before_line?(key)
41
+ return false unless :line == @glitch.event
42
+
43
+ key == @glitch.key
44
+ end
45
+
46
+ def before_call?(key)
47
+ return false unless :call == @glitch.event
48
+
49
+ key == @glitch.key
50
+ end
51
+
52
+ def before_return?(key)
53
+ return false unless :return == @glitch.event
54
+
55
+ key == @glitch.key
38
56
  end
39
57
  end
40
58
  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.8.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.8.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-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activejob