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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +16 -29
- data/lib/chaotic_job/glitch.rb +28 -53
- data/lib/chaotic_job/performer.rb +4 -1
- data/lib/chaotic_job/scenario.rb +4 -4
- data/lib/chaotic_job/simulation.rb +16 -22
- data/lib/chaotic_job/version.rb +1 -1
- data/lib/chaotic_job.rb +14 -4
- 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: 94de9ce766a9042a925882e70f29a8022fbd3b22fbd175ed3254260e04962cba
|
4
|
+
data.tar.gz: e05d2546038d72cd9181fef3eff223d7adec84225b0fa1267d89d0b3880351ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
[](https://rubygems.org/gems/chaotic_job)
|
4
4
|
[](https://rubygems.org/gems/chaotic_job)
|
5
5
|

|
6
|
-

|
7
7
|
[](https://github.com/sponsors/fractaledmind)
|
8
8
|
[](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
|
-
|
208
|
-
[
|
209
|
-
[
|
210
|
-
[
|
211
|
-
[
|
212
|
-
[
|
213
|
-
[
|
214
|
-
[
|
215
|
-
[
|
216
|
-
[
|
217
|
-
[
|
218
|
-
|
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
|
|
data/lib/chaotic_job/glitch.rb
CHANGED
@@ -8,81 +8,57 @@
|
|
8
8
|
module ChaoticJob
|
9
9
|
class Glitch
|
10
10
|
def self.before_line(key, &block)
|
11
|
-
new
|
11
|
+
new(key, :line, &block)
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.before_call(key, ...)
|
15
|
-
new
|
15
|
+
new(key, :call, ...)
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.before_return(key, return_type = nil, &block)
|
19
|
-
new
|
19
|
+
new(key, :return, retval: return_type, &block)
|
20
20
|
end
|
21
21
|
|
22
|
-
def initialize
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
@
|
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
|
-
|
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
|
-
|
40
|
+
next unless @key == key
|
57
41
|
|
58
|
-
|
59
|
-
next unless matches?(
|
42
|
+
matchers = derive_matchers(tp)
|
43
|
+
next unless matches?(matchers)
|
60
44
|
|
61
|
-
execute_block
|
45
|
+
execute_block
|
62
46
|
# :nocov:
|
63
47
|
end
|
64
48
|
|
65
49
|
trace.enable(&block)
|
66
50
|
end
|
67
51
|
|
68
|
-
def
|
69
|
-
@
|
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?(
|
83
|
-
return true if defn.nil?
|
59
|
+
def matches?(matchers)
|
84
60
|
return true if matchers.nil?
|
85
|
-
return true if
|
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
|
-
|
86
|
+
@args.each_with_index do |type, index|
|
111
87
|
return false unless type === args[index]
|
112
88
|
end
|
113
89
|
|
114
|
-
|
90
|
+
@kwargs.each do |key, type|
|
115
91
|
return false unless type === kwargs[key]
|
116
92
|
end
|
117
93
|
|
118
|
-
return false unless
|
94
|
+
return false unless @retval === retval
|
119
95
|
|
120
96
|
true
|
121
97
|
end
|
122
98
|
|
123
|
-
def execute_block
|
124
|
-
return
|
125
|
-
return if handler[:executed]
|
99
|
+
def execute_block
|
100
|
+
return if @executed
|
126
101
|
|
127
|
-
|
128
|
-
|
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
|
-
|
91
|
+
else # days+
|
89
92
|
{hour: 0, min: 0, sec: 0, usec: 0}
|
90
93
|
end
|
91
94
|
time.change(**changeset)
|
data/lib/chaotic_job/scenario.rb
CHANGED
@@ -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
|
-
|
33
|
+
self
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
37
|
-
@glitch.
|
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,
|
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
|
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(&
|
22
|
-
@template.class.retry_on RetryableError, attempts:
|
19
|
+
def run(&assertions)
|
20
|
+
@template.class.retry_on RetryableError, attempts: 3, wait: 1, jitter: 0
|
23
21
|
|
24
|
-
debug "๐พ Running #{
|
22
|
+
debug "๐พ Running #{@variations || "all"} simulations of the total #{variants.size} possibilities..."
|
25
23
|
|
26
24
|
scenarios.map do |scenario|
|
27
|
-
run_scenario(scenario, &
|
25
|
+
run_scenario(scenario, &assertions)
|
28
26
|
print "ยท"
|
29
27
|
end
|
30
28
|
end
|
31
29
|
|
32
|
-
def
|
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
|
-
|
40
|
-
return permutations if @variations.nil?
|
35
|
+
return error_locations if @variations.nil?
|
41
36
|
|
42
|
-
|
37
|
+
error_locations.sample(@variations, random: @random)
|
43
38
|
end
|
44
39
|
|
45
40
|
def scenarios
|
46
|
-
variants.map do |
|
41
|
+
variants.map do |(event, key)|
|
47
42
|
job = clone_job_template
|
48
|
-
glitch = Glitch.
|
49
|
-
|
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, &
|
60
|
+
def run_scenario(scenario, &assertions)
|
68
61
|
debug "๐พ Running simulation with scenario: #{scenario}"
|
69
62
|
@test.before_setup
|
70
|
-
@test.simulation_scenario = scenario
|
63
|
+
@test.simulation_scenario = scenario
|
71
64
|
scenario.run
|
72
65
|
@test.after_teardown
|
73
|
-
|
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
|
data/lib/chaotic_job/version.rb
CHANGED
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,
|
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
|
-
|
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
|
-
|
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.
|
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-
|
10
|
+
date: 2025-06-09 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activejob
|