stable 1.1.1 → 1.3.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/stable/spec.rb +70 -4
  3. data/lib/stable.rb +20 -31
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6de202dcc5cfca5f9d9b7799c85f919dc55735f85a06e8b1f2f674eb71240aa1
4
- data.tar.gz: b7edd805344ecd2eead6e6cbef78abacdde918eef4c9219a252f24d0aa76f9b8
3
+ metadata.gz: 632d329e437a0d82fed98a7fd06f9f93c160363e961ea2c835bcc40cc1330066
4
+ data.tar.gz: e301162819f6e056594cc07c600c4c678d91143e371b848367be9e9abaaceaca
5
5
  SHA512:
6
- metadata.gz: 74f14d4cb35e05c31f7bbc569b6da703a54f59a3cd2f2edc3c630ef7e536ccdad9b60d069f43be2184645744e72c28d5b0aaf3fcd3638b5b1b9a039fd10f08f1
7
- data.tar.gz: 95bba0dd950234f2f1115b25cfd0197b0bec71474930d0302d113a7e2113d260c09578e1d66f2c7b59e5f6db51bd3f6b2bd48bffcb98b60d16829a0912a63e1f
6
+ metadata.gz: '036778caa60876817ab53b71c2e5c2fbedd4105e9084c1bc6a45b6c688a6172f60a0b3434cc732806426131259848ed345de71d519011263692a907282f9a02a'
7
+ data.tar.gz: f427b87ee21f105729862a77e51d06814c90a931f18ced149a6b22cfff42cce643997252e0fdc42f00b9ec6a1db7e478c635db9f45bb7b49076b77443c650804
data/lib/stable/spec.rb CHANGED
@@ -1,20 +1,83 @@
1
1
  # lib/stable/spec.rb
2
2
  require 'json'
3
+ require 'securerandom'
4
+ require 'digest'
3
5
 
4
6
  module Stable
5
7
  # a spec is a recording of a single method call, including the inputs and
6
8
  # outputs. it's a self-contained, serializable representation of a method's
7
9
  # behavior at a specific point in time.
8
10
  class Spec
9
- attr_reader :class_name, :method_name, :args, :result, :error, :timestamp
11
+ attr_reader :class_name, :method_name, :args, :result, :error, :timestamp, :actual_result, :actual_error, :status, :uuid, :signature
10
12
 
11
- def initialize(class_name:, method_name:, args:, result: nil, error: nil, timestamp: Time.now.iso8601)
13
+ def initialize(class_name:, method_name:, args:, result: nil, error: nil, timestamp: Time.now.iso8601, uuid: SecureRandom.uuid)
12
14
  @class_name = class_name
13
15
  @method_name = method_name
14
16
  @args = args
15
17
  @result = result
16
18
  @error = error
17
19
  @timestamp = timestamp
20
+ @status = :pending
21
+ @uuid = uuid
22
+ @signature = Digest::SHA256.hexdigest("#{class_name}##{method_name}:#{args.to_json}")
23
+ end
24
+
25
+ def run!
26
+ klass = Object.const_get(class_name)
27
+ instance = klass.new
28
+
29
+ begin
30
+ @actual_result = instance.public_send(method_name, *args)
31
+ if error
32
+ @status = :failed
33
+ elsif actual_result == result
34
+ @status = :passed
35
+ else
36
+ @status = :failed
37
+ end
38
+ rescue => e
39
+ @actual_error = e
40
+ if error && e.class.name == error["class"]
41
+ @status = :passed_with_error
42
+ else
43
+ @status = :failed
44
+ end
45
+ end
46
+ self
47
+ end
48
+
49
+ def to_s
50
+ desc = "#{uuid}/#{signature}"
51
+ call = "#{class_name}##{method_name}(#{args.join(', ')})"
52
+
53
+ case status
54
+ when :passed
55
+ "#{desc} P #{call}"
56
+ when :passed_with_error
57
+ "#{desc} P (error) #{call}"
58
+ when :failed
59
+ lines = ["#{desc} F #{call}"]
60
+ if actual_error
61
+ if error
62
+ lines << " Expected error: #{error['class']}"
63
+ lines << " Actual error: #{actual_error.class.name}: #{actual_error.message}"
64
+ else
65
+ lines << " Expected result: #{result.inspect}"
66
+ lines << " Actual error: #{actual_error.class.name}: #{actual_error.message}"
67
+ end
68
+ else
69
+ if error
70
+ lines << " Expected error: #{error['class']}"
71
+ lines << " Actual result: #{actual_result.inspect}"
72
+ else
73
+ lines << " Expected: #{result.inspect}"
74
+ lines << " Actual: #{actual_result.inspect}"
75
+ end
76
+ end
77
+ lines.join("\n")
78
+ else
79
+ "#{desc} ? #{call}"
80
+ end
18
81
  end
19
82
 
20
83
  def to_jsonl
@@ -24,7 +87,9 @@ module Stable
24
87
  args: args,
25
88
  result: result,
26
89
  error: error,
27
- timestamp: timestamp
90
+ timestamp: timestamp,
91
+ uuid: uuid,
92
+ signature: signature
28
93
  }.compact.to_json
29
94
  end
30
95
 
@@ -36,7 +101,8 @@ module Stable
36
101
  args: data['args'],
37
102
  result: data['result'],
38
103
  error: data['error'],
39
- timestamp: data['timestamp']
104
+ timestamp: data['timestamp'],
105
+ uuid: data['uuid']
40
106
  )
41
107
  end
42
108
  end
data/lib/stable.rb CHANGED
@@ -37,7 +37,10 @@ module Stable
37
37
  args: args,
38
38
  result: result
39
39
  )
40
- Stable.storage.puts(spec.to_jsonl)
40
+ unless Stable.send(:_spec_exists?, spec.signature)
41
+ Stable.storage.puts(spec.to_jsonl)
42
+ Stable.send(:_recorded_specs) << spec
43
+ end
41
44
  result
42
45
  rescue => e
43
46
  spec = Spec.new(
@@ -50,7 +53,10 @@ module Stable
50
53
  backtrace: e.backtrace
51
54
  }
52
55
  )
53
- Stable.storage.puts(spec.to_jsonl)
56
+ unless Stable.send(:_spec_exists?, spec.signature)
57
+ Stable.storage.puts(spec.to_jsonl)
58
+ Stable.send(:_recorded_specs) << spec
59
+ end
54
60
  raise e
55
61
  end
56
62
  else
@@ -62,37 +68,20 @@ module Stable
62
68
  end
63
69
 
64
70
  def verify(record_hash)
65
- spec = Spec.from_jsonl(record_hash.to_json)
66
- klass = Object.const_get(spec.class_name)
67
- instance = klass.new
68
- description = "#{spec.class_name}##{spec.method_name}(#{spec.args.join(', ')})"
71
+ Spec.from_jsonl(record_hash.to_json).run!
72
+ end
69
73
 
70
- begin
71
- actual_result = instance.public_send(spec.method_name, *spec.args)
72
- if spec.error
73
- puts "FAILED: #{description}"
74
- puts " Expected error: #{spec.error['class']}"
75
- puts " Actual result: #{actual_result.inspect}"
76
- elsif actual_result == spec.result
77
- puts "PASSED: #{description}"
78
- else
79
- puts "FAILED: #{description}"
80
- puts " Expected: #{spec.result.inspect}"
81
- puts " Actual: #{actual_result.inspect}"
82
- end
83
- rescue => e
84
- if spec.error && e.class.name == spec.error["class"]
85
- puts "PASSED: #{description} (error)"
86
- elsif spec.error
87
- puts "FAILED: #{description}"
88
- puts " Expected error: #{spec.error['class']}"
89
- puts " Actual error: #{e.class.name}: #{e.message}"
90
- else
91
- puts "FAILED: #{description}"
92
- puts " Expected result: #{spec.result.inspect}"
93
- puts " Actual error: #{e.class.name}: #{e.message}"
94
- end
74
+ private
75
+
76
+ def _recorded_specs
77
+ @_recorded_specs ||= begin
78
+ return [] unless storage.respond_to?(:path) && File.exist?(storage.path)
79
+ File.foreach(storage.path).map { |line| Spec.from_jsonl(line) }
95
80
  end
96
81
  end
82
+
83
+ def _spec_exists?(signature)
84
+ _recorded_specs.any? { |spec| spec.signature == signature }
85
+ end
97
86
  end
98
87
  end
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.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Lunt