stable 1.19.1 → 1.20.1

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: 9c37e32ad41c2706a5759ea8c71dbaa01fdbca65a68c21a0ba4f7854d6e31536
4
- data.tar.gz: 3f0dc4afac0758033537dbe4b3c7d30900135cf559c89a6a43648c4f4007d79c
3
+ metadata.gz: 6e15bc20c1e3075bbcf961acf641350f5a86f28d344c4cdf10714d1f81e5cedb
4
+ data.tar.gz: 6c7e38b8cdb11d438057c612b7be9c7c0509ec627ff1702d5bb0a829cd4108f5
5
5
  SHA512:
6
- metadata.gz: 3c7834e8ea59b1c8d9d5ef50cd0540d58fdf646009c8c5320b562637a95a032dbafe7b92e31adf0d61c1f87e253b09352eab3ac6bf2bfb6617860c36de334d96
7
- data.tar.gz: 7fb53c7055b9d1a5fb285f94aa757c91009943d1875eb183bea7a228264fa17ad0cb0147419fa57af4282b00ddc320a14bccafdcff646c3576fc204003ffd5d2
6
+ metadata.gz: 6f713c013b885c9297b227d46b8ee5195b4d38844f042a7c6df9706e605eaec397eceeac54cb40962df2f9cccef49ec617234f7abd42956026ecbd99e6d574bd
7
+ data.tar.gz: 59902723fb85cdde62aab42c994570d4f630f4373a046e90b6f955b2b6bbd296f091c63c06a0b0ce93671bfee8e1326addf6316858b48ac3a5ed850ba5fa1817
@@ -0,0 +1,25 @@
1
+ # lib/example/stateful_calculator.rb
2
+
3
+ # this class demonstrates a stateful calculator where operations rely on the
4
+ # value stored in the `@memory` instance variable.
5
+ class StatefulCalculator
6
+ def initialize
7
+ @memory = 0
8
+ end
9
+
10
+ def add(number)
11
+ @memory += number
12
+ end
13
+
14
+ def subtract(number)
15
+ @memory -= number
16
+ end
17
+
18
+ def clear
19
+ @memory = 0
20
+ end
21
+
22
+ def memory
23
+ @memory
24
+ end
25
+ end
data/lib/stable/fact.rb CHANGED
@@ -8,9 +8,9 @@ module Stable
8
8
  # outputs. it's a self-contained, serializable representation of a method's
9
9
  # behavior at a specific point in time.
10
10
  class Fact
11
- attr_reader :class_name, :method_name, :method_type, :args, :kwargs, :result, :error, :actual_result, :actual_error, :status, :uuid, :signature, :name, :source_file
11
+ attr_reader :class_name, :method_name, :method_type, :args, :kwargs, :result, :error, :actual_result, :actual_error, :status, :uuid, :signature, :name, :source_file, :prior
12
12
 
13
- def initialize(class_name:, method_name:, args:, method_type: :instance, kwargs: {}, result: nil, error: nil, uuid: SecureRandom.uuid, name: nil, source_file: nil)
13
+ def initialize(class_name:, method_name:, args:, method_type: :instance, kwargs: {}, result: nil, error: nil, uuid: SecureRandom.uuid, name: nil, source_file: nil, prior: nil)
14
14
  @class_name = class_name
15
15
  @method_name = method_name
16
16
  @method_type = method_type
@@ -23,6 +23,7 @@ module Stable
23
23
  @signature = Digest::SHA256.hexdigest("#{class_name}##{method_name}:#{args.to_json}:#{kwargs.to_json}")
24
24
  @name = name || uuid.split('-').last
25
25
  @source_file = source_file
26
+ @prior = prior
26
27
  end
27
28
 
28
29
  def name=(new_name)
@@ -31,11 +32,16 @@ module Stable
31
32
  end
32
33
 
33
34
 
34
- def run!
35
+ def verify!
35
36
  begin
36
37
  klass = Object.const_get(class_name)
37
38
  if method_type == :instance
38
39
  instance = klass.new
40
+ if prior
41
+ prior.each do |var, value|
42
+ instance.instance_variable_set(var, value)
43
+ end
44
+ end
39
45
  @actual_result = instance.public_send(method_name, *args, **kwargs)
40
46
  else
41
47
  @actual_result = klass.public_send(method_name, *args, **kwargs)
@@ -82,6 +88,7 @@ module Stable
82
88
  method_type: method_type,
83
89
  args: args,
84
90
  kwargs: kwargs,
91
+ prior: prior,
85
92
  result: result,
86
93
  error: error,
87
94
  uuid: uuid,
@@ -98,6 +105,7 @@ module Stable
98
105
  method_type: (data['method_type'] || :instance).to_sym,
99
106
  args: data['args'],
100
107
  kwargs: data['kwargs'],
108
+ prior: data['prior'],
101
109
  result: data['result'],
102
110
  error: data['error'],
103
111
  uuid: data['uuid'],
@@ -105,7 +113,5 @@ module Stable
105
113
  source_file: source_file
106
114
  )
107
115
  end
108
-
109
-
110
116
  end
111
117
  end
@@ -1,3 +1,3 @@
1
1
  module Stable
2
- VERSION = "1.19.1"
2
+ VERSION = "1.20.1"
3
3
  end
data/lib/stable.rb CHANGED
@@ -58,6 +58,34 @@ module Stable
58
58
  @storage = nil
59
59
  end
60
60
 
61
+ # This is the core method for observing a method on a class or module. It
62
+ # uses a dynamic module and `prepend` to intercept method calls without
63
+ # altering the original method.
64
+ #
65
+ # The design handles several complexities:
66
+ #
67
+ # 1. **Instance vs. Class Methods:** It accepts a `type` parameter to
68
+ # differentiate between instance and class methods. For class methods,
69
+ # it targets the singleton class (`klass.singleton_class`) to inject
70
+ # the wrapper.
71
+ #
72
+ # 2. **State Capture:** For instance methods, it captures the object's state
73
+ # (instance variables) *before* the method is called. This `prior` state
74
+ # is crucial for rehydrating the object during verification. State is not
75
+ # captured for class methods to prevent infinite loops, as the recording
76
+ # process itself may call class methods (e.g., `.name`).
77
+ #
78
+ # 3. **Method Binding:** It correctly handles both bound (`Method`) and
79
+ # unbound (`UnboundMethod`) method objects, ensuring `self` is correctly
80
+ # bound when the original method is eventually called.
81
+ #
82
+ # 4. **Fact Creation:** It gathers all relevant data—class name, method name,
83
+ # arguments, prior state, and the result or error—into a `Fact` object.
84
+ #
85
+ # 5. **Duplicate Prevention:** It generates a signature for each potential
86
+ # fact and checks if a fact with the same signature has already been
87
+ # recorded to prevent creating duplicate entries.
88
+ #
61
89
  def watch(klass, method_name, type: :instance)
62
90
  original_method = type == :instance ? klass.instance_method(method_name) : klass.method(method_name)
63
91
  target = type == :instance ? klass : klass.singleton_class
@@ -66,6 +94,7 @@ module Stable
66
94
  define_method(method_name) do |*args, **kwargs, &block|
67
95
  if Stable.enabled?
68
96
  begin
97
+ prior = type == :instance ? Stable.send(:_capture_state, self) : nil
69
98
  result = original_method.is_a?(UnboundMethod) ? original_method.bind(self).call(*args, **kwargs, &block) : original_method.call(*args, **kwargs, &block)
70
99
  fact = Fact.new(
71
100
  class_name: klass.name,
@@ -73,6 +102,7 @@ module Stable
73
102
  method_type: type,
74
103
  args: args,
75
104
  kwargs: kwargs,
105
+ prior: prior,
76
106
  result: result
77
107
  )
78
108
  unless Stable.send(:_fact_exists?, fact.signature)
@@ -82,12 +112,14 @@ module Stable
82
112
  end
83
113
  result
84
114
  rescue => e
115
+ prior = type == :instance ? Stable.send(:_capture_state, self) : nil
85
116
  fact = Fact.new(
86
117
  class_name: klass.name,
87
118
  method_name: method_name,
88
119
  method_type: type,
89
120
  args: args,
90
121
  kwargs: kwargs,
122
+ prior: prior,
91
123
  error: {
92
124
  class: e.class.name,
93
125
  message: e.message,
@@ -122,7 +154,7 @@ module Stable
122
154
  end
123
155
 
124
156
  def verify(record_hash)
125
- Fact.from_jsonl(record_hash.to_json).run!
157
+ Fact.from_jsonl(record_hash.to_json).verify!
126
158
  end
127
159
 
128
160
  private
@@ -140,5 +172,11 @@ module Stable
140
172
  def _fact_exists?(signature)
141
173
  _recorded_facts.any? { |fact| fact.signature == signature }
142
174
  end
175
+
176
+ def _capture_state(obj)
177
+ obj.instance_variables.each_with_object({}) do |var, hash|
178
+ hash[var] = obj.instance_variable_get(var)
179
+ end
180
+ end
143
181
  end
144
182
  end
@@ -8,6 +8,7 @@ namespace :stable do
8
8
  require_relative '../../lib/example/calculator'
9
9
  require_relative '../../lib/example/kw_calculator'
10
10
  require_relative '../../lib/example/class_methods'
11
+ require_relative '../../lib/example/stateful_calculator'
11
12
 
12
13
  fact_path = File.expand_path('../../../facts/calculator.fact.example', __FILE__)
13
14
  Stable.configure do |config|
@@ -23,7 +24,7 @@ namespace :stable do
23
24
  puts formatter.header
24
25
 
25
26
  _filter_facts(facts, args[:filter].to_s.strip.downcase).each do |fact|
26
- fact.run!
27
+ fact.verify!
27
28
  puts formatter.to_s(fact)
28
29
  end
29
30
 
@@ -43,7 +44,7 @@ namespace :stable do
43
44
  puts formatter.header
44
45
 
45
46
  facts.each do |fact|
46
- fact.run!
47
+ fact.verify!
47
48
  puts formatter.to_s(fact)
48
49
  end
49
50
 
@@ -96,7 +97,7 @@ namespace :stable do
96
97
 
97
98
  updated_facts = []
98
99
  _filter_facts(facts, _clean_filter(args[:filter])).each do |fact|
99
- fact.run!
100
+ fact.verify!
100
101
  if fact.status == :failed
101
102
  puts formatter.to_s(fact)
102
103
  print " update this fact? (y/n): "
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.1
4
+ version: 1.20.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Lunt
@@ -32,6 +32,7 @@ files:
32
32
  - lib/example/calculator.rb
33
33
  - lib/example/class_methods.rb
34
34
  - lib/example/kw_calculator.rb
35
+ - lib/example/stateful_calculator.rb
35
36
  - lib/stable.rb
36
37
  - lib/stable/configuration.rb
37
38
  - lib/stable/fact.rb