chaotic_job 0.3.0 → 0.5.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 +8 -0
- data/README.md +66 -21
- data/lib/chaotic_job/glitch.rb +99 -30
- data/lib/chaotic_job/journal.rb +2 -1
- data/lib/chaotic_job/scenario.rb +3 -3
- data/lib/chaotic_job/simulation.rb +9 -27
- data/lib/chaotic_job/tracer.rb +50 -0
- data/lib/chaotic_job/version.rb +1 -1
- data/lib/chaotic_job.rb +9 -5
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c4da24d7fc4aa39030976d4f7249a14c87995ef5d5b05c523ad5e1e83724f37
|
4
|
+
data.tar.gz: 0a7be90ecb5381a5c9e67c6673bb41bc3ad2997c904ca42bfedbb4c701ef6bf0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11a054f050a3e95aa40c392e281b69bf2c43dbba3b91bce3d17473d589f4e2057d26a18af5071d7a5f4cb2b055775bbdd293af6c712792fbe1d042c4c99cca85
|
7
|
+
data.tar.gz: d6750f62adecc71334dd42cfb15e35e33fb36680be62a6c9d0d5f4e2ed29eb598f9f92fac6a09ec1a4636816789f13996bc0241faa5a1ef9eb4acac0577fc06f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.0] - 2025-06-04
|
4
|
+
|
5
|
+
- Add a Tracer class [#3](https://github.com/fractaledmind/chaotic_job/pull/3)
|
6
|
+
|
7
|
+
## [0.4.0] - 2025-05-27
|
8
|
+
|
9
|
+
- Allow a Glitch to be defined for a method call or method return [#4](https://github.com/fractaledmind/chaotic_job/pull/4)
|
10
|
+
|
3
11
|
## [0.3.0] - 2024-12-17
|
4
12
|
|
5
13
|
- Ensure that assertion failure messages raised within a simulation contain the scenario description
|
data/README.md
CHANGED
@@ -44,7 +44,7 @@ The `ChaoticJob::Helpers` module provides 6 methods, 4 of which simply allow you
|
|
44
44
|
|
45
45
|
### Glitches
|
46
46
|
|
47
|
-
A central concept in `ChaoticJob` is the _glitch_. A glitch is an error injected into the job execution flow via a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html). Glitches are transient errors, which means they occur
|
47
|
+
A central concept in `ChaoticJob` is the _glitch_. A glitch is an error injected into the job execution flow via a [`TracePoint`](https://docs.ruby-lang.org/en/master/TracePoint.html). Glitches are transient errors, which means they occur _once_ and **only once**, making them perfect for testing a job's resilience to unpredictable failures that can occur while running jobs, like network issues, upstream API outages, rate limits, or infrastructure failure. By default, `ChaoticJob` raises a custom error defined by the gem (`ChaoticJob::RetryableError`), which the internals of the gem ensure that the job under test is configured to retry on; you can, however, raise specific errors as needed when setting up your [scenarios](#simulating-failures). By forcing a retry via the error handling mechanisms of Active Job, glitches are a simple but effective way to test that your job is resilient to any kind of transient error that the job is configured to retry on.
|
48
48
|
|
49
49
|
### Performing Jobs
|
50
50
|
|
@@ -107,7 +107,7 @@ test "scenario of a simple job" do
|
|
107
107
|
def step_3; ChaoticJob::Journal.log; end
|
108
108
|
end
|
109
109
|
|
110
|
-
run_scenario(Job.new, glitch: [
|
110
|
+
run_scenario(Job.new, glitch: [:before_call, "Job#step_3"])
|
111
111
|
|
112
112
|
assert_equal 5, ChaoticJob::Journal.total
|
113
113
|
end
|
@@ -124,7 +124,16 @@ end
|
|
124
124
|
> | `Journal.entries` | get all of the logged values under the default scope |
|
125
125
|
> | `Journal.entries(scope: :special)` | get all of the logged values under a particular scope |
|
126
126
|
|
127
|
-
In this example, the job being tested is defined within the test case. You can, of course, also test jobs defined in your application. The key detail is the `glitch` keyword argument. A "glitch" is simply a tuple that describes precisely where you would like the failure to occur. The first element of the tuple is the
|
127
|
+
In this example, the job being tested is defined within the test case. You can, of course, also test jobs defined in your application. The key detail is the `glitch` keyword argument. A "glitch" is simply a tuple that describes precisely where you would like the failure to occur. The first element of the tuple is the _kind_ of glitch, which can be either `:before_line`, `:before_call`, or `:before_return`. These refer to the three kinds of `TracePoint` events that the gem hooks into. The second element is the _key_ for the code that will be affected by the glitch. This _key_ is a specially formatted string that defines the specific bit of code that the glitch should be inserted before. The different kinds of glitches are identified by different kinds of keys:
|
128
|
+
|kind|key format|key example|
|
129
|
+
|---|---|---|
|
130
|
+
|`:before_line`|`"#{file_path}:#{line_number}"`|`"/Users/you/path/to/file.rb:123"`|
|
131
|
+
|`:before_call`|`"#{YourClass.name}(.|#)#{method_name}"`|`"YourClass.some_class_method"`|
|
132
|
+
|`:before_return`|`"#{YourClass.name}(.|#)#{method_name}"`|`"YourClass#some_instance_method"`|
|
133
|
+
|
134
|
+
As you can see, the `:before_call` and `:before_return` keys are formatted the same, and can identify any instance (`#`) or class (`.`) method.
|
135
|
+
|
136
|
+
What the example scenario above does is inject a glitch before the `step_3` method is called, here:
|
128
137
|
|
129
138
|
```ruby
|
130
139
|
def perform
|
@@ -135,12 +144,33 @@ def perform
|
|
135
144
|
end
|
136
145
|
```
|
137
146
|
|
147
|
+
If we wanted to inject a glitch right before the `step_3` method finishes, we could define the glitch as a `:before_return`, like this:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
run_scenario(Job.new, glitch: [:before_return, "Job#step_3"])
|
151
|
+
```
|
152
|
+
|
153
|
+
and it would inject the transient error right here:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
def step_3
|
157
|
+
ChaoticJob::Journal.log
|
158
|
+
# <-- HERE
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
Finally, if you need to inject a glitch right before a particular line of code is executed that is neither a method call nor a method return, you can use the `:before_line` key, like this:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
run_scenario(Job.new, glitch: [:before_line, "#{__FILE__}:6"])
|
166
|
+
```
|
167
|
+
|
138
168
|
If you want to simulate multiple glitches affecting a job run, you can use the plural `glitches` keyword argument instead and pass an array of tuples:
|
139
169
|
|
140
170
|
```ruby
|
141
171
|
run_scenario(Job.new, glitches: [
|
142
|
-
[
|
143
|
-
[
|
172
|
+
[:before_call, "Job#step_1"],
|
173
|
+
[:before_return, "Job#step_1"]
|
144
174
|
])
|
145
175
|
```
|
146
176
|
|
@@ -166,30 +196,45 @@ test "simulation of a simple job" do
|
|
166
196
|
end
|
167
197
|
```
|
168
198
|
|
169
|
-
|
199
|
+
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:
|
170
200
|
|
171
201
|
```ruby
|
172
202
|
[
|
173
|
-
[[:
|
174
|
-
[[:
|
175
|
-
[[:
|
176
|
-
[[:
|
177
|
-
[[:
|
178
|
-
[[:
|
179
|
-
[[:
|
180
|
-
[[:
|
181
|
-
[[:
|
182
|
-
[[:
|
183
|
-
[[:
|
184
|
-
[[:
|
203
|
+
[[:before_line, "test_chaotic_job.rb:69"]],
|
204
|
+
[[:before_line, "test_chaotic_job.rb:75"]],
|
205
|
+
[[:before_line, "test_chaotic_job.rb:74"]],
|
206
|
+
[[:before_line, "test_chaotic_job.rb:74"]],
|
207
|
+
[[:before_line, "test_chaotic_job.rb:68"]],
|
208
|
+
[[:before_line, "test_chaotic_job.rb:70"]],
|
209
|
+
[[:before_line, "test_chaotic_job.rb:68"]],
|
210
|
+
[[:before_line, "test_chaotic_job.rb:73"]],
|
211
|
+
[[:before_line, "test_chaotic_job.rb:75"]],
|
212
|
+
[[:before_line, "test_chaotic_job.rb:69"]],
|
213
|
+
[[:before_line, "test_chaotic_job.rb:70"]],
|
214
|
+
[[:before_line, "test_chaotic_job.rb:73"]]
|
185
215
|
]
|
186
216
|
```
|
187
217
|
|
188
|
-
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.
|
218
|
+
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.
|
219
|
+
|
220
|
+
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:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
job_file_path = YourJob.instance_method(:perform).source_location&.first
|
224
|
+
tracer = Tracer.new { |tp| tp.path == job_file_path || tp.defined_class == YourJob }
|
225
|
+
tracer.capture { YourJob.perform_now }
|
226
|
+
```
|
227
|
+
|
228
|
+
To capture, for example, a custom callstack that includes all events within your application, you can use the `ChaoticJob::Tracer` class as follows:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
tracer = ChaoticJob::Tracer.new { |tp| tp.path.start_with?(Rails.root.to_s) }
|
232
|
+
tracer.capture { YourJob.perform_now }
|
233
|
+
```
|
189
234
|
|
190
|
-
|
235
|
+
If you passed this callstack to your simulation, it would test what happens to your job whenever a transient glitch is injected anywhere in your application code called as a part of executing the job under test.
|
191
236
|
|
192
|
-
|
237
|
+
Remember, in your application tests, you will want to make assertions about the side-effects that your job performs, asserting that they are correctly idempotent (only occur once) and result in the correct state.
|
193
238
|
|
194
239
|
## Development
|
195
240
|
|
data/lib/chaotic_job/glitch.rb
CHANGED
@@ -1,46 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Glitch.new.
|
4
|
-
# Glitch.new.
|
3
|
+
# Glitch.new.before_line("job_crucible.rb:10") { do_anything }
|
4
|
+
# Glitch.new.before_call("Model#method", String, name: "Joel") { do_anything }
|
5
|
+
# Glitch.new.before_return("Model#method", String, name: "Joel") { do_anything }
|
5
6
|
# Glitch.new.inject! { execute code to glitch }
|
6
7
|
|
7
8
|
module ChaoticJob
|
8
9
|
class Glitch
|
9
10
|
def initialize
|
10
11
|
@breakpoints = {}
|
11
|
-
@file_contents = {}
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
set_breakpoint(
|
14
|
+
def before_line(key, &block)
|
15
|
+
set_breakpoint(key, :line, &block)
|
16
|
+
self
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
set_breakpoint(
|
19
|
+
def before_call(key, ...)
|
20
|
+
set_breakpoint(key, :call, ...)
|
21
|
+
self
|
20
22
|
end
|
21
23
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# content = @file_contents[tp.path]
|
27
|
-
# line = content[tp.lineno - 1]
|
28
|
-
# next unless line.match? key
|
24
|
+
def before_return(key, return_type = nil, &block)
|
25
|
+
set_breakpoint(key, :return, retval: return_type, &block)
|
26
|
+
self
|
27
|
+
end
|
29
28
|
|
30
|
-
|
31
|
-
|
29
|
+
def inject!(&block)
|
30
|
+
breakpoints = @breakpoints
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
trace = TracePoint.new(:line, :call, :return) do |tp|
|
33
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
34
|
+
key = derive_key(tp)
|
35
|
+
matchers = derive_matchers(tp)
|
36
|
+
|
37
|
+
next unless (defn = breakpoints.dig(key, tp.event))
|
38
|
+
next unless matches?(defn, matchers)
|
39
|
+
|
40
|
+
execute_block(defn)
|
41
|
+
# :nocov:
|
37
42
|
end
|
38
43
|
|
39
|
-
trace.enable
|
40
|
-
yield if block_given?
|
41
|
-
ensure
|
42
|
-
trace.disable
|
43
|
-
execute_block(@breakpoints[prev_key][:after]) if prev_key && @breakpoints.key?(prev_key)
|
44
|
+
trace.enable(&block)
|
44
45
|
end
|
45
46
|
|
46
47
|
def all_executed?
|
@@ -57,11 +58,51 @@ module ChaoticJob
|
|
57
58
|
|
58
59
|
private
|
59
60
|
|
60
|
-
def set_breakpoint(
|
61
|
-
@breakpoints[
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
def set_breakpoint(key, event, *args, retval: nil, **kwargs, &block)
|
62
|
+
@breakpoints[key] ||= {}
|
63
|
+
@breakpoints[key][event] = {args: args, kwargs: kwargs, retval: retval, block: block, executed: false}
|
64
|
+
end
|
65
|
+
|
66
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
67
|
+
def matches?(defn, matchers)
|
68
|
+
return true if defn.nil?
|
69
|
+
return true if matchers.nil?
|
70
|
+
return true if defn[:args].empty? && defn[:kwargs].empty? && defn[:retval].nil?
|
71
|
+
|
72
|
+
args = []
|
73
|
+
kwargs = {}
|
74
|
+
retval = nil
|
75
|
+
|
76
|
+
matchers.each do |kind, name, value|
|
77
|
+
case kind
|
78
|
+
when :req
|
79
|
+
args << value
|
80
|
+
when :opt
|
81
|
+
args << value if value
|
82
|
+
when :rest
|
83
|
+
args.concat(value) if value
|
84
|
+
when :keyreq
|
85
|
+
kwargs[name] = value
|
86
|
+
when :key
|
87
|
+
kwargs[name] = value
|
88
|
+
when :keyrest
|
89
|
+
kwargs.merge!(value) if value
|
90
|
+
when :retval
|
91
|
+
retval = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
defn[:args].each_with_index do |type, index|
|
96
|
+
return false unless type === args[index]
|
97
|
+
end
|
98
|
+
|
99
|
+
defn[:kwargs].each do |key, type|
|
100
|
+
return false unless type === kwargs[key]
|
101
|
+
end
|
102
|
+
|
103
|
+
return false unless defn[:retval] === retval
|
104
|
+
|
105
|
+
true
|
65
106
|
end
|
66
107
|
|
67
108
|
def execute_block(handler)
|
@@ -71,5 +112,33 @@ module ChaoticJob
|
|
71
112
|
handler[:executed] = true
|
72
113
|
handler[:block].call
|
73
114
|
end
|
115
|
+
|
116
|
+
def derive_key(trace)
|
117
|
+
case trace.event
|
118
|
+
when :line
|
119
|
+
"#{trace.path}:#{trace.lineno}"
|
120
|
+
when :call, :return
|
121
|
+
if Module === trace.self
|
122
|
+
"#{trace.self}.#{trace.method_id}"
|
123
|
+
else
|
124
|
+
"#{trace.defined_class}##{trace.method_id}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def derive_matchers(trace)
|
130
|
+
case trace.event
|
131
|
+
when :line
|
132
|
+
nil
|
133
|
+
when :call
|
134
|
+
trace.parameters.map do |type, name|
|
135
|
+
value = trace.binding.local_variable_get(name) rescue nil # standard:disable Style/RescueModifier
|
136
|
+
[type, name, value]
|
137
|
+
end
|
138
|
+
when :return
|
139
|
+
[[:retval, nil, trace.return_value]]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
# :nocov:
|
74
143
|
end
|
75
144
|
end
|
data/lib/chaotic_job/journal.rb
CHANGED
@@ -18,6 +18,7 @@ module ChaoticJob
|
|
18
18
|
@logs ||= {}
|
19
19
|
@logs[scope] ||= []
|
20
20
|
@logs[scope] << item
|
21
|
+
item
|
21
22
|
end
|
22
23
|
|
23
24
|
def size(scope: :default)
|
@@ -29,7 +30,7 @@ module ChaoticJob
|
|
29
30
|
end
|
30
31
|
|
31
32
|
def top(scope: :default)
|
32
|
-
entries&.first
|
33
|
+
entries(scope: scope)&.first
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
data/lib/chaotic_job/scenario.rb
CHANGED
@@ -10,7 +10,7 @@ module ChaoticJob
|
|
10
10
|
def initialize(job, glitches:, raise: RetryableError, capture: /active_job/)
|
11
11
|
@job = job
|
12
12
|
@glitches = glitches
|
13
|
-
@raise = raise
|
13
|
+
@raise = binding.local_variable_get(:raise)
|
14
14
|
@capture = capture
|
15
15
|
@glitch = nil
|
16
16
|
@events = []
|
@@ -45,8 +45,8 @@ module ChaoticJob
|
|
45
45
|
|
46
46
|
def glitch
|
47
47
|
@glitch ||= Glitch.new.tap do |glitch|
|
48
|
-
@glitches.each do |
|
49
|
-
glitch.public_send(
|
48
|
+
@glitches.each do |kind, location, _description|
|
49
|
+
glitch.public_send(kind, location) { raise @raise }
|
50
50
|
end
|
51
51
|
end
|
52
52
|
end
|
@@ -6,13 +6,16 @@
|
|
6
6
|
# Simulation.new(job).scenarios
|
7
7
|
module ChaoticJob
|
8
8
|
class Simulation
|
9
|
-
def initialize(job, depth: 1, variations: 100, test: nil, seed: nil)
|
9
|
+
def initialize(job, callstack: nil, depth: 1, variations: 100, test: nil, seed: nil)
|
10
10
|
@template = job
|
11
|
+
@callstack = callstack || capture_callstack
|
11
12
|
@depth = depth
|
12
13
|
@variations = variations
|
13
14
|
@test = test
|
14
15
|
@seed = seed || Random.new_seed
|
15
16
|
@random = Random.new(@seed)
|
17
|
+
|
18
|
+
raise Error.new("callstack must be a generated via the ChaoticJob::Tracer") unless @callstack.is_a?(Stack)
|
16
19
|
end
|
17
20
|
|
18
21
|
def run(&callback)
|
@@ -27,20 +30,9 @@ module ChaoticJob
|
|
27
30
|
end
|
28
31
|
|
29
32
|
def permutations
|
30
|
-
|
31
|
-
|
32
|
-
lpath, lno = left
|
33
|
-
rpath, rno = right
|
34
|
-
key = "#{lpath}:#{lno}"
|
35
|
-
# inject an error before and after each non-adjacent line
|
36
|
-
if lpath == rpath && rno == lno + 1
|
37
|
-
[[:before, key]]
|
38
|
-
else
|
39
|
-
[[:before, key], [:after, key]]
|
40
|
-
end
|
33
|
+
error_locations = @callstack.map do |event, key|
|
34
|
+
["before_#{event}", key]
|
41
35
|
end
|
42
|
-
final_key = callstack.last.join(":")
|
43
|
-
error_locations.push [:before, final_key], [:after, final_key]
|
44
36
|
error_locations.permutation(@depth)
|
45
37
|
end
|
46
38
|
|
@@ -62,23 +54,13 @@ module ChaoticJob
|
|
62
54
|
private
|
63
55
|
|
64
56
|
def capture_callstack
|
65
|
-
return @callstack if defined?(@callstack)
|
66
|
-
|
67
|
-
@callstack = Set.new
|
68
57
|
job_class = @template.class
|
69
58
|
job_file_path = job_class.instance_method(:perform).source_location&.first
|
59
|
+
tracer = Tracer.new { |tp| tp.path == job_file_path || tp.defined_class == job_class }
|
60
|
+
callstack = tracer.capture { @template.dup.perform_now }
|
70
61
|
|
71
|
-
trace = TracePoint.new(:line) do |tp|
|
72
|
-
next if tp.defined_class == self.class
|
73
|
-
next unless tp.path == job_file_path ||
|
74
|
-
tp.defined_class == job_class
|
75
|
-
|
76
|
-
@callstack << [tp.path, tp.lineno]
|
77
|
-
end
|
78
|
-
|
79
|
-
trace.enable { @template.dup.perform_now }
|
80
62
|
@template.class.queue_adapter.enqueued_jobs = []
|
81
|
-
|
63
|
+
callstack
|
82
64
|
end
|
83
65
|
|
84
66
|
def run_scenario(scenario, &callback)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Tracer.new { |tp| tp.path.start_with? "foo" }
|
4
|
+
# Tracer.new.capture { code_execution_to_trace_callstack }
|
5
|
+
|
6
|
+
module ChaoticJob
|
7
|
+
class Tracer
|
8
|
+
def initialize(&constraint)
|
9
|
+
@constraint = constraint
|
10
|
+
@callstack = Stack.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def capture(&block)
|
14
|
+
trace = TracePoint.new(:line, :call, :return) do |tp|
|
15
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
16
|
+
next if tp.defined_class == self.class
|
17
|
+
next unless @constraint.call(tp)
|
18
|
+
|
19
|
+
case tp.event
|
20
|
+
when :line
|
21
|
+
key = line_key(tp)
|
22
|
+
when :call, :return
|
23
|
+
key = call_key(tp)
|
24
|
+
end
|
25
|
+
|
26
|
+
@callstack << [tp.event, key]
|
27
|
+
# :nocov:
|
28
|
+
end
|
29
|
+
|
30
|
+
trace.enable(&block)
|
31
|
+
@callstack
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# :nocov: SimpleCov cannot track code executed _within_ a TracePoint
|
37
|
+
def line_key(event)
|
38
|
+
"#{event.path}:#{event.lineno}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def call_key(event)
|
42
|
+
if Module === event.self
|
43
|
+
"#{event.self}.#{event.method_id}"
|
44
|
+
else
|
45
|
+
"#{event.defined_class}##{event.method_id}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# :nocov:
|
49
|
+
end
|
50
|
+
end
|
data/lib/chaotic_job/version.rb
CHANGED
data/lib/chaotic_job.rb
CHANGED
@@ -3,13 +3,16 @@
|
|
3
3
|
require_relative "chaotic_job/version"
|
4
4
|
require_relative "chaotic_job/journal"
|
5
5
|
require_relative "chaotic_job/performer"
|
6
|
+
require_relative "chaotic_job/tracer"
|
6
7
|
require_relative "chaotic_job/glitch"
|
7
8
|
require_relative "chaotic_job/scenario"
|
8
9
|
require_relative "chaotic_job/simulation"
|
10
|
+
require "set"
|
9
11
|
|
10
12
|
module ChaoticJob
|
11
|
-
|
12
|
-
|
13
|
+
Error = Class.new(StandardError)
|
14
|
+
RetryableError = Class.new(Error)
|
15
|
+
Stack = Set
|
13
16
|
|
14
17
|
def self.log_to_journal!(item = nil, scope: nil)
|
15
18
|
if item && scope
|
@@ -63,18 +66,19 @@ module ChaoticJob
|
|
63
66
|
Performer.perform_all_after(time)
|
64
67
|
end
|
65
68
|
|
66
|
-
def run_simulation(job, depth: nil, variations: nil, &block)
|
69
|
+
def run_simulation(job, depth: nil, variations: nil, callstack: nil, &block)
|
67
70
|
seed = defined?(RSpec) ? RSpec.configuration.seed : Minitest.seed
|
68
71
|
kwargs = {test: self, seed: seed}
|
69
72
|
kwargs[:depth] = depth if depth
|
70
73
|
kwargs[:variations] = variations if variations
|
74
|
+
kwargs[:callstack] = callstack if callstack
|
71
75
|
self.simulation_scenario = nil
|
72
76
|
Simulation.new(job, **kwargs).run(&block)
|
73
77
|
end
|
74
78
|
|
75
79
|
def run_scenario(job, glitch: nil, glitches: nil, raise: nil, capture: nil, &block)
|
76
80
|
kwargs = {glitches: glitches || [glitch]}
|
77
|
-
kwargs[:raise] = raise if raise
|
81
|
+
kwargs[:raise] = binding.local_variable_get(:raise) if binding.local_variable_get(:raise)
|
78
82
|
kwargs[:capture] = capture if capture
|
79
83
|
if block
|
80
84
|
Scenario.new(job, **kwargs).run(&block)
|
@@ -89,7 +93,7 @@ module ChaoticJob
|
|
89
93
|
contextual_msg = lambda do
|
90
94
|
# copied from the original `assert` method in Minitest::Assertions
|
91
95
|
default_msg = "Expected #{mu_pp test} to be truthy."
|
92
|
-
custom_msg =
|
96
|
+
custom_msg = msg.is_a?(Proc) ? msg.call : msg
|
93
97
|
full_msg = custom_msg || default_msg
|
94
98
|
" #{@simulation_scenario}\n#{full_msg}"
|
95
99
|
end
|
metadata
CHANGED
@@ -1,14 +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.5.0
|
5
5
|
platform: ruby
|
6
|
-
original_platform: ''
|
7
6
|
authors:
|
8
7
|
- Stephen Margheim
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-06-03 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activejob
|
@@ -42,6 +41,7 @@ files:
|
|
42
41
|
- lib/chaotic_job/performer.rb
|
43
42
|
- lib/chaotic_job/scenario.rb
|
44
43
|
- lib/chaotic_job/simulation.rb
|
44
|
+
- lib/chaotic_job/tracer.rb
|
45
45
|
- lib/chaotic_job/version.rb
|
46
46
|
- sig/chaotic_job.rbs
|
47
47
|
homepage: https://github.com/fractaledmind/chaotic_job
|
@@ -65,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: '0'
|
67
67
|
requirements: []
|
68
|
-
rubygems_version: 3.6.
|
68
|
+
rubygems_version: 3.6.3
|
69
69
|
specification_version: 4
|
70
70
|
summary: Test ActiveJobs for reliability and resilience.
|
71
71
|
test_files: []
|