chaotic_job 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98b01482666de5069e40dbe4eeb828cb527e4dd31004b3900a134ceaec5cb42a
4
- data.tar.gz: f3df1be0fa3192aa842b74fa56754e56ed071f48d3c2fd027ca88d05da3f5e90
3
+ metadata.gz: 0c4da24d7fc4aa39030976d4f7249a14c87995ef5d5b05c523ad5e1e83724f37
4
+ data.tar.gz: 0a7be90ecb5381a5c9e67c6673bb41bc3ad2997c904ca42bfedbb4c701ef6bf0
5
5
  SHA512:
6
- metadata.gz: aff436f38402fddae8e62a7dbb441dc8949169abd1aa992fdf5e59f33e007f8dc3aba29787a3cea4bf3d421698ed1ac76c171388c26a3d2ab884e7880296ba84
7
- data.tar.gz: 72986fa7274aade0e1fe7f9a5b71e4911467f18bf4929b90960ba62b03721611e46da8958c0536a92f92e98f4b4a2c7315e674db73bbcc6866cc177070a6f92f
6
+ metadata.gz: 11a054f050a3e95aa40c392e281b69bf2c43dbba3b91bce3d17473d589f4e2057d26a18af5071d7a5f4cb2b055775bbdd293af6c712792fbe1d042c4c99cca85
7
+ data.tar.gz: d6750f62adecc71334dd42cfb15e35e33fb36680be62a6c9d0d5f4e2ed29eb598f9f92fac6a09ec1a4636816789f13996bc0241faa5a1ef9eb4acac0577fc06f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
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
+
3
7
  ## [0.4.0] - 2025-05-27
4
8
 
5
9
  - Allow a Glitch to be defined for a method call or method return [#4](https://github.com/fractaledmind/chaotic_job/pull/4)
data/README.md CHANGED
@@ -217,7 +217,24 @@ More specifically, it will create a scenario injecting a glitch before every lin
217
217
 
218
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
219
 
220
- 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.
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
+ ```
234
+
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.
236
+
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.
221
238
 
222
239
  ## Development
223
240
 
@@ -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,9 +30,8 @@ module ChaoticJob
27
30
  end
28
31
 
29
32
  def permutations
30
- callstack = capture_callstack.map { |path, line| "#{path}:#{line}" }
31
- error_locations = callstack.map do |path, lineno|
32
- [:before_line, "#{path}:#{lineno}"]
33
+ error_locations = @callstack.map do |event, key|
34
+ ["before_#{event}", key]
33
35
  end
34
36
  error_locations.permutation(@depth)
35
37
  end
@@ -52,23 +54,13 @@ module ChaoticJob
52
54
  private
53
55
 
54
56
  def capture_callstack
55
- return @callstack if defined?(@callstack)
56
-
57
- @callstack = Set.new
58
57
  job_class = @template.class
59
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 }
60
61
 
61
- trace = TracePoint.new(:line) do |tp|
62
- next if tp.defined_class == self.class
63
- next unless tp.path == job_file_path ||
64
- tp.defined_class == job_class
65
-
66
- @callstack << [tp.path, tp.lineno]
67
- end
68
-
69
- trace.enable { @template.dup.perform_now }
70
62
  @template.class.queue_adapter.enqueued_jobs = []
71
- @callstack
63
+ callstack
72
64
  end
73
65
 
74
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChaoticJob
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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
- class RetryableError < StandardError
12
- end
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,11 +66,12 @@ 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
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-05-27 00:00:00.000000000 Z
10
+ date: 2025-06-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activejob
@@ -41,6 +41,7 @@ files:
41
41
  - lib/chaotic_job/performer.rb
42
42
  - lib/chaotic_job/scenario.rb
43
43
  - lib/chaotic_job/simulation.rb
44
+ - lib/chaotic_job/tracer.rb
44
45
  - lib/chaotic_job/version.rb
45
46
  - sig/chaotic_job.rbs
46
47
  homepage: https://github.com/fractaledmind/chaotic_job