suture 0.3.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +7 -5
- data/Rakefile +10 -8
- data/lib/suture/adapter/dictaphone.rb +1 -1
- data/lib/suture/config.rb +21 -0
- data/lib/suture/{builds_plan.rb → create/builds_plan.rb} +3 -1
- data/lib/suture/{chooses_surgeon.rb → create/chooses_surgeon.rb} +7 -0
- data/lib/suture/{performs_surgery.rb → create/performs_surgery.rb} +0 -0
- data/lib/suture/create/validates_plan.rb +90 -0
- data/lib/suture/create.rb +12 -0
- data/lib/suture/delete.rb +9 -0
- data/lib/suture/error/invalid_plan.rb +41 -0
- data/lib/suture/error/observation_conflict.rb +1 -2
- data/lib/suture/error/result_mismatch.rb +58 -0
- data/lib/suture/error/verification_failed.rb +1 -1
- data/lib/suture/reset.rb +9 -0
- data/lib/suture/surgeon/auditor.rb +29 -0
- data/lib/suture/surgeon/no_op.rb +3 -1
- data/lib/suture/surgeon/observer.rb +2 -11
- data/lib/suture/surgeon/remediator.rb +21 -0
- data/lib/suture/util/scalpel.rb +41 -0
- data/lib/suture/value/plan.rb +15 -2
- data/lib/suture/value/test_plan.rb +19 -10
- data/lib/suture/{interprets_results.rb → verify/interprets_results.rb} +0 -0
- data/lib/suture/{prescribes_test_plan.rb → verify/prescribes_test_plan.rb} +3 -6
- data/lib/suture/{tests_patient.rb → verify/tests_patient.rb} +7 -6
- data/lib/suture/verify.rb +14 -0
- data/lib/suture/version.rb +1 -1
- data/lib/suture/wrap/logger.rb +5 -5
- data/lib/suture.rb +5 -48
- data/suture.gemspec +4 -1
- metadata +30 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9c19b3ad957fa4dee235e314fcacde3aeac88c3
|
4
|
+
data.tar.gz: de6d831eaaecf0ff8b7edde1397b7c1304a486c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e35674ad68833373e8d926437c0c272f7f9b216c821b150aec70fc74250e29a188f698d277bdca8dcf48f67baa534bbdcd355652ff56b9d5f991c38802955608
|
7
|
+
data.tar.gz: 4fa715fc19bbb2911c3e028ab360cf557b49b87d564764c4cb4d64109362d5bc00683c294a9d3e6d2ecfc72de6ebbf879540363898e0e60ee41f52f063b90efe
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -128,6 +128,7 @@ We could write a test like this:
|
|
128
128
|
``` ruby
|
129
129
|
class MyWorkerCharacterizationTest < Minitest::Test
|
130
130
|
def setup
|
131
|
+
super
|
131
132
|
# Load the test data needed to resemble the environment when recording
|
132
133
|
end
|
133
134
|
|
@@ -192,6 +193,7 @@ interactions:
|
|
192
193
|
``` ruby
|
193
194
|
class MyWorkerCharacterizationTest < Minitest::Test
|
194
195
|
def setup
|
196
|
+
super
|
195
197
|
# Load the test data needed to resemble the environment when recording
|
196
198
|
end
|
197
199
|
|
@@ -270,7 +272,7 @@ arguments and broader state.
|
|
270
272
|
Suture gives users a way to experiment with risky refactors by deploying them to
|
271
273
|
a staging environment and running both the original and new code paths
|
272
274
|
side-by-side, raising an error in the event they don't return the same value.
|
273
|
-
This is governed by the `:
|
275
|
+
This is governed by the `:call_both` to `true`:
|
274
276
|
|
275
277
|
``` ruby
|
276
278
|
class MyWorker
|
@@ -279,7 +281,7 @@ class MyWorker
|
|
279
281
|
old: LegacyWorker.new,
|
280
282
|
new: NewWorker.new,
|
281
283
|
args: [id],
|
282
|
-
|
284
|
+
call_both: true
|
283
285
|
}))
|
284
286
|
end
|
285
287
|
end
|
@@ -344,7 +346,7 @@ end
|
|
344
346
|
### Retrying failures
|
345
347
|
|
346
348
|
Since the legacy code path hasn't been deleted yet, there's no reason to leave
|
347
|
-
users hanging if the new code path explodes. By setting the `:
|
349
|
+
users hanging if the new code path explodes. By setting the `:call_old_on_error`
|
348
350
|
entry to `true`, Suture will rescue any errors raised from the new code path and
|
349
351
|
attempt to invoke the legacy code path instead.
|
350
352
|
|
@@ -355,7 +357,7 @@ class MyWorker
|
|
355
357
|
old: LegacyWorker.new,
|
356
358
|
new: NewWorker.new,
|
357
359
|
args: [id],
|
358
|
-
|
360
|
+
call_old_on_error: true
|
359
361
|
}))
|
360
362
|
end
|
361
363
|
end
|
@@ -476,5 +478,5 @@ seeing false negatives:
|
|
476
478
|
* If a recording was made in error, you can always delete it, either by
|
477
479
|
dropping Suture's database (which is, by default, stored in
|
478
480
|
`db/suture.sqlite3`) or by observing the ID of the recording from an error
|
479
|
-
message and invoking `Suture.delete(42)`
|
481
|
+
message and invoking `Suture.delete!(42)`
|
480
482
|
|
data/Rakefile
CHANGED
@@ -26,15 +26,17 @@ Rake::TestTask.new(:everything) do |t|
|
|
26
26
|
]
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
if Gem.ruby_version >= Gem::Version.new("2.2.2")
|
30
|
+
require 'github_changelog_generator/task'
|
31
|
+
GitHubChangelogGenerator::RakeTask.new :changelog
|
32
|
+
task :changelog_commit do
|
33
|
+
require "suture"
|
34
|
+
cmd = "git commit -m \"Changelog for #{Suture::VERSION}\" -- CHANGELOG.md"
|
35
|
+
puts "-------> #{cmd}"
|
36
|
+
system cmd
|
37
|
+
end
|
38
|
+
Rake::Task["release:source_control_push"].enhance([:changelog, :changelog_commit])
|
36
39
|
end
|
37
40
|
|
38
|
-
Rake::Task["release:source_control_push"].enhance([:changelog, :changelog_commit])
|
39
41
|
|
40
42
|
task :default => :everything
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "suture/comparator"
|
2
|
+
|
3
|
+
module Suture
|
4
|
+
DEFAULT_OPTIONS = {
|
5
|
+
:database_path => "db/suture.sqlite3",
|
6
|
+
:comparator => Comparator.new,
|
7
|
+
:log_level => "INFO",
|
8
|
+
:log_file => nil,
|
9
|
+
:log_stdout => true,
|
10
|
+
:raise_on_result_mismatch => true
|
11
|
+
}
|
12
|
+
|
13
|
+
def self.config(config = {})
|
14
|
+
@config ||= DEFAULT_OPTIONS.dup
|
15
|
+
@config.merge!(config)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.config_reset!
|
19
|
+
@config = DEFAULT_OPTIONS.dup
|
20
|
+
end
|
21
|
+
end
|
@@ -3,7 +3,9 @@ require "suture/util/env"
|
|
3
3
|
|
4
4
|
module Suture
|
5
5
|
class BuildsPlan
|
6
|
-
UN_ENV_IABLE_OPTIONS = [:name, :old, :new, :args, :comparator
|
6
|
+
UN_ENV_IABLE_OPTIONS = [:name, :old, :new, :args, :comparator, :after_new,
|
7
|
+
:after_old, :on_new_error, :on_old_error,
|
8
|
+
:expected_error_types]
|
7
9
|
|
8
10
|
def build(name, options = {})
|
9
11
|
Value::Plan.new(
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require "suture/adapter/log"
|
2
2
|
require "suture/surgeon/observer"
|
3
|
+
require "suture/surgeon/auditor"
|
4
|
+
require "suture/surgeon/remediator"
|
3
5
|
require "suture/surgeon/no_op"
|
4
6
|
|
5
7
|
module Suture
|
@@ -7,6 +9,7 @@ module Suture
|
|
7
9
|
include Suture::Adapter::Log
|
8
10
|
|
9
11
|
def choose(plan)
|
12
|
+
return Surgeon::NoOp.new if plan.disable
|
10
13
|
if plan.record_calls
|
11
14
|
if plan.new
|
12
15
|
log_warn <<-MSG.gsub(/^ {12}/,'')
|
@@ -16,6 +19,10 @@ module Suture
|
|
16
19
|
MSG
|
17
20
|
end
|
18
21
|
Surgeon::Observer.new
|
22
|
+
elsif plan.call_both
|
23
|
+
Surgeon::Auditor.new
|
24
|
+
elsif plan.call_old_on_error
|
25
|
+
Surgeon::Remediator.new
|
19
26
|
else
|
20
27
|
Surgeon::NoOp.new
|
21
28
|
end
|
File without changes
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "suture/error/invalid_plan"
|
2
|
+
|
3
|
+
module Suture
|
4
|
+
class ValidatesPlan
|
5
|
+
REQUIREMENTS = {
|
6
|
+
:name => "in order to identify recorded calls",
|
7
|
+
:old => "in order to call the legacy code path (must respond to `:call`)",
|
8
|
+
:args => "in order to differentiate recorded calls (if the code you're changing doesn't take arguments, you can set :args to `[]` but should probably consider creating a seam inside of it which can--consult the README for more advice)"
|
9
|
+
}
|
10
|
+
|
11
|
+
VALIDATIONS = {
|
12
|
+
:name => {
|
13
|
+
:test => lambda { |name| name.to_s.size < 256 },
|
14
|
+
:message => "must be less than 256 characters"
|
15
|
+
},
|
16
|
+
:old => CALLABLE_VALIDATION = ({
|
17
|
+
:test => lambda { |old| old.respond_to?(:call) },
|
18
|
+
:message => "must respond to `call` (e.g. `dog.method(:bark)` or `->(*args){ dog.bark(*args) }`)"
|
19
|
+
}),
|
20
|
+
:new => CALLABLE_VALIDATION,
|
21
|
+
:comparator => CALLABLE_VALIDATION.merge(
|
22
|
+
:message => "must respond to `call` (e.g. `MyComparator.new` or `->(recorded, actual) { recorded == actual }`)"
|
23
|
+
)
|
24
|
+
}
|
25
|
+
|
26
|
+
CONFLICTS = [
|
27
|
+
lambda { |plan|
|
28
|
+
if plan.record_calls && !plan.database_path
|
29
|
+
":record_calls is enabled, but :database_path is nil, so Suture doesn't know where to record calls to the seam."
|
30
|
+
end
|
31
|
+
},
|
32
|
+
lambda { |plan|
|
33
|
+
if plan.record_calls && plan.call_both
|
34
|
+
":record_calls & :call_both are both enabled and conflict with one another. :record_calls will only invoke the old code path (intended for characterization of the old code path and initial development of the new code path), whereas :call_both will invoke the new path and the old to compare their results after development of the new code path is initially complete (typically in a pre-production environment to validate the behavior of the new code path is consistent with the old). If you're still actively developing the new code path and need more recordings to feed Suture.verify, disable :call_both; otherwise, it's likely time to turn off :record_calls on this seam."
|
35
|
+
end
|
36
|
+
},
|
37
|
+
lambda { |plan|
|
38
|
+
if plan.record_calls && plan.call_old_on_error
|
39
|
+
":record_calls & :call_old_on_error are both enabled and conflict with one another. :record_calls will only invoke the old code path (intended for characterization of the old code path and initial development of the new code path), whereas :call_old_on_error will call the new code path unless an error is raised, in which case it will fall back on the old code path."
|
40
|
+
end
|
41
|
+
},
|
42
|
+
lambda { |plan|
|
43
|
+
if plan.call_both && plan.call_old_on_error
|
44
|
+
":call_both & :call_old_on_error are both enabled and conflict with one another. :call_both is designed for pre-production environments and will call both the old and new code paths to compare their results, whereas :call_old_on_error is designed for production environments where it is safe to call the old code path in the event that the new code path fails unexpectedly"
|
45
|
+
end
|
46
|
+
},
|
47
|
+
lambda { |plan|
|
48
|
+
if plan.call_both && !plan.new.respond_to?(:call)
|
49
|
+
":call_both is set but :new is either not set or is not callable. In order to call both code paths, both :old and :new must be set and callable."
|
50
|
+
end
|
51
|
+
},
|
52
|
+
lambda { |plan|
|
53
|
+
if plan.call_old_on_error && !plan.new.respond_to?(:call)
|
54
|
+
":call_old_on_error is set but :new is either not set or is not callable. This mode is designed for after the :new code path has been developed and run in production-like environments, where :old is only kept around as a fallback to retry in the event that :new raises an unexpected error. Either specify a :new code path or disable :call_old_on_error."
|
55
|
+
end
|
56
|
+
}
|
57
|
+
]
|
58
|
+
|
59
|
+
def validate(plan)
|
60
|
+
if (missing = missing_attrs(plan)).any?
|
61
|
+
raise Error::InvalidPlan.missing_requirements(missing)
|
62
|
+
elsif (invalids = invalid_attrs(plan)).any?
|
63
|
+
raise Error::InvalidPlan.invalid_options(invalids)
|
64
|
+
elsif (conflicts = conflicting_attrs(plan)).any?
|
65
|
+
raise Error::InvalidPlan.conflicting_options(conflicts)
|
66
|
+
else
|
67
|
+
plan
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def missing_attrs(plan)
|
72
|
+
REQUIREMENTS.select { |(name, _)|
|
73
|
+
!plan.send(name)
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def invalid_attrs(plan)
|
78
|
+
VALIDATIONS.select { |name, rule|
|
79
|
+
next unless attr = plan.send(name)
|
80
|
+
!rule[:test].call(attr)
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def conflicting_attrs(plan)
|
85
|
+
CONFLICTS.map { |rule|
|
86
|
+
rule.call(plan)
|
87
|
+
}.compact
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "suture/create/builds_plan"
|
2
|
+
require "suture/create/validates_plan"
|
3
|
+
require "suture/create/chooses_surgeon"
|
4
|
+
require "suture/create/performs_surgery"
|
5
|
+
|
6
|
+
module Suture
|
7
|
+
def self.create(name, options)
|
8
|
+
plan = ValidatesPlan.new.validate(BuildsPlan.new.build(name, options))
|
9
|
+
surgeon = ChoosesSurgeon.new.choose(plan)
|
10
|
+
PerformsSurgery.new.perform(plan, surgeon)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Suture::Error
|
2
|
+
class InvalidPlan < StandardError
|
3
|
+
HEADER = "Suture was unable to create your seam, because options passed to `Suture.create` were invalid."
|
4
|
+
|
5
|
+
def self.missing_requirements(requirements)
|
6
|
+
new <<-MSG.gsub(/^ {8}/,'')
|
7
|
+
#{HEADER}
|
8
|
+
|
9
|
+
The following options are required:
|
10
|
+
|
11
|
+
#{requirements.map {|(name,explanation)|
|
12
|
+
"* #{name.inspect} - #{explanation}"
|
13
|
+
}.join("\n ")}
|
14
|
+
MSG
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.invalid_options(invalids)
|
18
|
+
new <<-MSG.gsub(/^ {8}/,'')
|
19
|
+
#{HEADER}
|
20
|
+
|
21
|
+
The following options were invalid:
|
22
|
+
|
23
|
+
#{invalids.map {|(name,rule)|
|
24
|
+
"* #{name.inspect} - #{rule[:message]}"
|
25
|
+
}.join("\n ")}
|
26
|
+
MSG
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.conflicting_options(conflicts)
|
30
|
+
new <<-MSG.gsub(/^ {8}/,'')
|
31
|
+
#{HEADER}
|
32
|
+
|
33
|
+
Suture isn't sure how to best handle the combination of options passed:
|
34
|
+
|
35
|
+
#{conflicts.map {|message|
|
36
|
+
"* #{message}"
|
37
|
+
}.join("\n ")}
|
38
|
+
MSG
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -19,7 +19,6 @@ module Suture::Error
|
|
19
19
|
Previously-observed return value: ```
|
20
20
|
#{@old_result.inspect}
|
21
21
|
```
|
22
|
-
|
23
22
|
Newly-observed return value: ```
|
24
23
|
#{@new_result.inspect}
|
25
24
|
```
|
@@ -49,7 +48,7 @@ module Suture::Error
|
|
49
48
|
recorded result is in error, you can delete it from Suture's database
|
50
49
|
with:
|
51
50
|
|
52
|
-
Suture.delete(#{@old_id})
|
51
|
+
Suture.delete!(#{@old_id})
|
53
52
|
MSG
|
54
53
|
end
|
55
54
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Suture::Error
|
2
|
+
class ResultMismatch < StandardError
|
3
|
+
def initialize(plan, new_result, old_result)
|
4
|
+
@plan = plan
|
5
|
+
@new_result = new_result
|
6
|
+
@old_result = old_result
|
7
|
+
end
|
8
|
+
|
9
|
+
def message
|
10
|
+
expected_message = <<-MSG.gsub(/^ {8}/,'')
|
11
|
+
The results from the old & new code paths did not match for the seam
|
12
|
+
#{@plan.name.inspect} and Suture is raising this error because the `:call_both`
|
13
|
+
option is enabled, because both code paths are expected to return the
|
14
|
+
same result.
|
15
|
+
|
16
|
+
Arguments: ```
|
17
|
+
#{@plan.args.inspect}
|
18
|
+
```
|
19
|
+
The new code path returned: ```
|
20
|
+
#{@new_result.inspect}
|
21
|
+
```
|
22
|
+
The old code path returned: ```
|
23
|
+
#{@old_result.inspect}
|
24
|
+
```
|
25
|
+
|
26
|
+
Here's what we recommend you do next:
|
27
|
+
|
28
|
+
1. Verify that this mismatch does not represent a missed requirement in
|
29
|
+
the new code path. If it does, implement it!
|
30
|
+
|
31
|
+
2. If either (or both) code path has a side effect that impacts the
|
32
|
+
return value of the other, consider passing an `:after_old` and/or
|
33
|
+
`:after_new` hook to clean up your application's state well enough to
|
34
|
+
run both paths one-after-the-other safely.
|
35
|
+
|
36
|
+
3. If the two return values above are sufficiently similar for the
|
37
|
+
purpose of your application, consider writing your own custom
|
38
|
+
comparator that relaxes the comparison (e.g. only checks equivalence
|
39
|
+
of the attributes that matter). See the README for more info on custom
|
40
|
+
comparators.
|
41
|
+
|
42
|
+
4. If the new code path is working as desired (i.e. the old code path had
|
43
|
+
a bug for this argument and you don't want to reimplement it just to
|
44
|
+
make them perfectly in sync with one another), consider writing a
|
45
|
+
one-off comparator for this seam that will ignore the affected range
|
46
|
+
of arguments. See the README for more info on custom comparators.
|
47
|
+
|
48
|
+
By default, Suture's :call_both mode will log a warning and raise an
|
49
|
+
error when the results of each code path don't match. It is intended for
|
50
|
+
use in any pre-production environment to "try out" the new code path
|
51
|
+
before pushing it to production. If, for whatever reason, this error is
|
52
|
+
too disruptive and logging is sufficient for monitoring results, you may
|
53
|
+
disable this error by setting `:raise_on_result_mismatch` to false.
|
54
|
+
MSG
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -105,7 +105,7 @@ module Suture::Error
|
|
105
105
|
|
106
106
|
Ideas to fix this:
|
107
107
|
* Focus on this test by setting ENV var `SUTURE_VERIFY_ONLY=#{expected.id}`
|
108
|
-
* Is the recording wrong? Delete it! `Suture.delete(#{expected.id})`
|
108
|
+
* Is the recording wrong? Delete it! `Suture.delete!(#{expected.id})`
|
109
109
|
MSG
|
110
110
|
end
|
111
111
|
|
data/lib/suture/reset.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "suture/error/result_mismatch"
|
2
|
+
require "suture/util/scalpel"
|
3
|
+
|
4
|
+
module Suture::Surgeon
|
5
|
+
class Auditor
|
6
|
+
include Suture::Adapter::Log
|
7
|
+
|
8
|
+
def operate(plan)
|
9
|
+
scalpel = Suture::Util::Scalpel.new
|
10
|
+
new_result = scalpel.cut(plan, :new)
|
11
|
+
old_result = scalpel.cut(plan, :old)
|
12
|
+
if !plan.comparator.call(old_result, new_result)
|
13
|
+
log_warn <<-MSG.gsub(/^ {10}/,'')
|
14
|
+
Seam #{plan.name.inspect} is set to :call_both the :new and :old code
|
15
|
+
paths, but they did not match. The new result was: ```
|
16
|
+
#{new_result.inspect}
|
17
|
+
```
|
18
|
+
The old result was: ```
|
19
|
+
#{old_result.inspect}
|
20
|
+
```
|
21
|
+
MSG
|
22
|
+
if plan.raise_on_result_mismatch
|
23
|
+
raise Suture::Error::ResultMismatch.new(plan, new_result, old_result)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
new_result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/suture/surgeon/no_op.rb
CHANGED
@@ -1,23 +1,14 @@
|
|
1
|
+
require "suture/util/scalpel"
|
1
2
|
require "suture/adapter/dictaphone"
|
2
3
|
|
3
4
|
module Suture::Surgeon
|
4
5
|
class Observer
|
5
6
|
def operate(plan)
|
6
7
|
dictaphone = Suture::Adapter::Dictaphone.new(plan)
|
7
|
-
|
8
|
+
Suture::Util::Scalpel.new.cut(plan, :old).tap do |result|
|
8
9
|
dictaphone.record(result)
|
9
10
|
end
|
10
11
|
end
|
11
|
-
|
12
|
-
private
|
13
|
-
|
14
|
-
def invoke(plan)
|
15
|
-
if plan.args
|
16
|
-
plan.old.call(*plan.args)
|
17
|
-
else
|
18
|
-
plan.old.call
|
19
|
-
end
|
20
|
-
end
|
21
12
|
end
|
22
13
|
end
|
23
14
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "suture/util/scalpel"
|
2
|
+
|
3
|
+
module Suture::Surgeon
|
4
|
+
class Remediator
|
5
|
+
def initialize
|
6
|
+
@scalpel = Suture::Util::Scalpel.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def operate(plan)
|
10
|
+
begin
|
11
|
+
@scalpel.cut(plan ,:new)
|
12
|
+
rescue StandardError => actual_error
|
13
|
+
if plan.expected_error_types.any? { |e| actual_error.is_a?(e) }
|
14
|
+
raise actual_error
|
15
|
+
else
|
16
|
+
@scalpel.cut(plan, :old)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Suture::Util
|
2
|
+
class Scalpel
|
3
|
+
def cut(plan, location, args_override = nil)
|
4
|
+
args = args(plan, args_override)
|
5
|
+
begin
|
6
|
+
plan.send(location).call(*args).tap do |result|
|
7
|
+
call_after_hook(plan, location, args, result)
|
8
|
+
end
|
9
|
+
rescue StandardError => error
|
10
|
+
call_error_hook(plan, location, args, error)
|
11
|
+
raise error
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def call_after_hook(plan, location, args, result)
|
18
|
+
return unless after_hook = try(plan, "after_#{location}")
|
19
|
+
after_hook.call(plan.name, args, result)
|
20
|
+
end
|
21
|
+
|
22
|
+
def call_error_hook(plan, location, args, error)
|
23
|
+
return if plan.expected_error_types.any? {|e| error.kind_of?(e) }
|
24
|
+
return unless error_hook = try(plan, "on_#{location}_error")
|
25
|
+
error_hook.call(plan.name, args, error)
|
26
|
+
end
|
27
|
+
|
28
|
+
def args(plan, args_override)
|
29
|
+
return args_override if args_override
|
30
|
+
if plan.respond_to?(:args)
|
31
|
+
plan.args
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def try(plan, method)
|
36
|
+
return unless plan.respond_to?(method)
|
37
|
+
plan.send(method)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/lib/suture/value/plan.rb
CHANGED
@@ -1,14 +1,27 @@
|
|
1
1
|
module Suture::Value
|
2
2
|
class Plan
|
3
|
-
attr_reader :name, :old, :new, :args, :
|
3
|
+
attr_reader :name, :old, :new, :args, :after_new, :after_old, :on_new_error,
|
4
|
+
:on_old_error, :database_path, :record_calls, :comparator,
|
5
|
+
:call_both, :raise_on_result_mismatch, :call_old_on_error,
|
6
|
+
:expected_error_types, :disable
|
7
|
+
|
4
8
|
def initialize(attrs = {})
|
5
9
|
@name = attrs[:name]
|
6
10
|
@old = attrs[:old]
|
7
11
|
@new = attrs[:new]
|
8
12
|
@args = attrs[:args]
|
13
|
+
@after_new = attrs[:after_new]
|
14
|
+
@after_old = attrs[:after_old]
|
15
|
+
@on_new_error = attrs[:on_new_error]
|
16
|
+
@on_old_error = attrs[:on_old_error]
|
17
|
+
@database_path = attrs[:database_path]
|
9
18
|
@record_calls = !!attrs[:record_calls]
|
10
19
|
@comparator = attrs[:comparator]
|
11
|
-
@
|
20
|
+
@call_both = !!attrs[:call_both]
|
21
|
+
@raise_on_result_mismatch = !!attrs[:raise_on_result_mismatch]
|
22
|
+
@call_old_on_error = !!attrs[:call_old_on_error]
|
23
|
+
@expected_error_types = attrs[:expected_error_types] || []
|
24
|
+
@disable = !!attrs[:disable]
|
12
25
|
end
|
13
26
|
end
|
14
27
|
end
|
@@ -2,11 +2,17 @@ module Suture::Value
|
|
2
2
|
class TestPlan
|
3
3
|
attr_accessor :name, :subject,
|
4
4
|
:verify_only, :fail_fast, :call_limit, :time_limit,
|
5
|
-
:error_message_limit, :random_seed,
|
6
|
-
:
|
5
|
+
:error_message_limit, :random_seed, :comparator,
|
6
|
+
:database_path, :after_subject, :on_subject_error,
|
7
|
+
:expected_error_types
|
8
|
+
|
7
9
|
def initialize(attrs = {})
|
8
|
-
assign_simple_ivars!(attrs, :name, :subject, :fail_fast, :comparator,
|
9
|
-
|
10
|
+
assign_simple_ivars!(attrs, :name, :subject, :fail_fast, :comparator,
|
11
|
+
:database_path, :after_subject,
|
12
|
+
:on_subject_error)
|
13
|
+
assign_integral_ivars!(attrs, :verify_only, :call_limit, :time_limit,
|
14
|
+
:error_message_limit)
|
15
|
+
@expected_error_types = attrs[:expected_error_types] || []
|
10
16
|
@random_seed = determine_random_seed(attrs)
|
11
17
|
end
|
12
18
|
|
@@ -18,12 +24,15 @@ module Suture::Value
|
|
18
24
|
end
|
19
25
|
end
|
20
26
|
|
21
|
-
def assign_integral_ivars(attrs, *names)
|
22
|
-
assign_simple_ivars!(
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
+
def assign_integral_ivars!(attrs, *names)
|
28
|
+
assign_simple_ivars!(massage_values(attrs, names) {|v| v.to_i }, *names)
|
29
|
+
end
|
30
|
+
|
31
|
+
def massage_values(attrs, names, &blk)
|
32
|
+
Hash[
|
33
|
+
attrs.select {|(k,_)| names.include?(k) }.
|
34
|
+
map { |(k,v)| [k, v.nil? ? nil : blk.call(v) ]}
|
35
|
+
]
|
27
36
|
end
|
28
37
|
|
29
38
|
def determine_random_seed(attrs)
|
File without changes
|
@@ -3,14 +3,11 @@ require "suture/util/env"
|
|
3
3
|
|
4
4
|
module Suture
|
5
5
|
class PrescribesTestPlan
|
6
|
-
UN_ENV_IABLE_OPTIONS = [:name, :subject, :comparator
|
7
|
-
|
8
|
-
:fail_fast => false
|
9
|
-
}
|
6
|
+
UN_ENV_IABLE_OPTIONS = [:name, :subject, :comparator, :after_subject,
|
7
|
+
:on_subject_error, :expected_error_types]
|
10
8
|
|
11
9
|
def prescribe(name, options = {})
|
12
|
-
Value::TestPlan.new(
|
13
|
-
merge(Suture.config).
|
10
|
+
Value::TestPlan.new(Suture.config.
|
14
11
|
merge(options).
|
15
12
|
merge(:name => name).
|
16
13
|
merge(Suture::Util::Env.to_map(UN_ENV_IABLE_OPTIONS)))
|
@@ -1,11 +1,16 @@
|
|
1
1
|
require "suture/adapter/dictaphone"
|
2
2
|
require "suture/value/test_results"
|
3
|
-
require "suture/util/
|
3
|
+
require "suture/util/scalpel"
|
4
4
|
require "suture/util/shuffle"
|
5
|
+
require "suture/util/timer"
|
5
6
|
require "backports/1.9.2/random"
|
6
7
|
|
7
8
|
module Suture
|
8
9
|
class TestsPatient
|
10
|
+
def initialize
|
11
|
+
@scalpel = Suture::Util::Scalpel.new
|
12
|
+
end
|
13
|
+
|
9
14
|
def test(test_plan)
|
10
15
|
experienced_failure_in_life = false
|
11
16
|
timer = Suture::Util::Timer.new(test_plan.time_limit) unless test_plan.time_limit.nil?
|
@@ -45,11 +50,7 @@ module Suture
|
|
45
50
|
def invoke(test_plan, observation)
|
46
51
|
{}.tap do |result|
|
47
52
|
begin
|
48
|
-
result[:new_result] =
|
49
|
-
test_plan.subject.call(*observation.args)
|
50
|
-
else
|
51
|
-
test_plan.subject.call
|
52
|
-
end
|
53
|
+
result[:new_result] = @scalpel.cut(test_plan, :subject, observation.args)
|
53
54
|
result[:passed] = test_plan.comparator.call(observation.result, result[:new_result])
|
54
55
|
rescue StandardError => e
|
55
56
|
result[:passed] = false
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "suture/verify/prescribes_test_plan"
|
2
|
+
require "suture/verify/tests_patient"
|
3
|
+
require "suture/verify/interprets_results"
|
4
|
+
|
5
|
+
module Suture
|
6
|
+
def self.verify(name, options)
|
7
|
+
test_plan = Suture::PrescribesTestPlan.new.prescribe(name, options)
|
8
|
+
test_results = Suture::TestsPatient.new.test(test_plan)
|
9
|
+
if test_results.failed?
|
10
|
+
Suture::InterpretsResults.new.interpret(test_plan, test_results)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
data/lib/suture/version.rb
CHANGED
data/lib/suture/wrap/logger.rb
CHANGED
@@ -30,11 +30,11 @@ module Suture::Wrap
|
|
30
30
|
end
|
31
31
|
|
32
32
|
class NullLogger
|
33
|
-
def formatter
|
34
|
-
def level
|
35
|
-
def debug; end
|
36
|
-
def info; end
|
37
|
-
def warn; end
|
33
|
+
def formatter=(*args); end
|
34
|
+
def level=(*args); end
|
35
|
+
def debug(*args); end
|
36
|
+
def info(*args); end
|
37
|
+
def warn(*args); end
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
data/lib/suture.rb
CHANGED
@@ -1,51 +1,8 @@
|
|
1
1
|
require "suture/version"
|
2
2
|
|
3
|
-
require "suture/
|
4
|
-
require "suture/
|
5
|
-
require "suture/
|
3
|
+
require "suture/create"
|
4
|
+
require "suture/verify"
|
5
|
+
require "suture/delete"
|
6
|
+
require "suture/config"
|
7
|
+
require "suture/reset"
|
6
8
|
|
7
|
-
require "suture/prescribes_test_plan"
|
8
|
-
require "suture/tests_patient"
|
9
|
-
require "suture/interprets_results"
|
10
|
-
|
11
|
-
require "suture/adapter/log"
|
12
|
-
require "suture/comparator"
|
13
|
-
|
14
|
-
module Suture
|
15
|
-
DEFAULT_OPTIONS = {
|
16
|
-
:database_path => "db/suture.sqlite3",
|
17
|
-
:comparator => Comparator.new,
|
18
|
-
:log_level => "INFO",
|
19
|
-
:log_file => nil,
|
20
|
-
:log_stdout => true
|
21
|
-
}
|
22
|
-
|
23
|
-
def self.create(name, options)
|
24
|
-
plan = BuildsPlan.new.build(name, options)
|
25
|
-
surgeon = ChoosesSurgeon.new.choose(plan)
|
26
|
-
PerformsSurgery.new.perform(plan, surgeon)
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.verify(name, options)
|
30
|
-
test_plan = Suture::PrescribesTestPlan.new.prescribe(name, options)
|
31
|
-
test_results = Suture::TestsPatient.new.test(test_plan)
|
32
|
-
if test_results.failed?
|
33
|
-
Suture::InterpretsResults.new.interpret(test_plan, test_results)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def self.delete(id, options = {})
|
38
|
-
plan = BuildsPlan.new.build(name, options)
|
39
|
-
Suture::Adapter::Dictaphone.new(plan).delete(id)
|
40
|
-
end
|
41
|
-
|
42
|
-
def self.config(config = {})
|
43
|
-
@config ||= DEFAULT_OPTIONS.dup
|
44
|
-
@config.merge!(config)
|
45
|
-
end
|
46
|
-
|
47
|
-
def self.reset!
|
48
|
-
@config = nil
|
49
|
-
Adapter::Log.reset!
|
50
|
-
end
|
51
|
-
end
|
data/suture.gemspec
CHANGED
@@ -26,10 +26,13 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency "pry", "~> 0.9.12.6"
|
27
27
|
spec.add_development_dependency "minitest", "~> 5.9"
|
28
28
|
spec.add_development_dependency "gimme", "~> 0.5"
|
29
|
-
spec.add_development_dependency "github_changelog_generator"
|
30
29
|
|
31
30
|
if Gem.ruby_version >= Gem::Version.new("1.9.3")
|
32
31
|
spec.add_development_dependency "codeclimate-test-reporter"
|
33
32
|
spec.add_development_dependency "simplecov", "~> 0.11.2" #<--only here to lock
|
34
33
|
end
|
34
|
+
|
35
|
+
if Gem.ruby_version >= Gem::Version.new("2.2.2")
|
36
|
+
spec.add_development_dependency "github_changelog_generator"
|
37
|
+
end
|
35
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: suture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Searls
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|
@@ -109,7 +109,7 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0.5'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: codeclimate-test-reporter
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - ">="
|
@@ -123,33 +123,33 @@ dependencies:
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: simplecov
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
|
-
- - "
|
129
|
+
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version:
|
131
|
+
version: 0.11.2
|
132
132
|
type: :development
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
|
-
- - "
|
136
|
+
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version:
|
138
|
+
version: 0.11.2
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
140
|
+
name: github_changelog_generator
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
|
-
- - "
|
143
|
+
- - ">="
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: 0
|
145
|
+
version: '0'
|
146
146
|
type: :development
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
|
-
- - "
|
150
|
+
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: 0
|
152
|
+
version: '0'
|
153
153
|
description: Provides tools to record calls to legacy code and verify new implementations
|
154
154
|
still work
|
155
155
|
email:
|
@@ -173,25 +173,36 @@ files:
|
|
173
173
|
- lib/suture.rb
|
174
174
|
- lib/suture/adapter/dictaphone.rb
|
175
175
|
- lib/suture/adapter/log.rb
|
176
|
-
- lib/suture/builds_plan.rb
|
177
|
-
- lib/suture/chooses_surgeon.rb
|
178
176
|
- lib/suture/comparator.rb
|
177
|
+
- lib/suture/config.rb
|
178
|
+
- lib/suture/create.rb
|
179
|
+
- lib/suture/create/builds_plan.rb
|
180
|
+
- lib/suture/create/chooses_surgeon.rb
|
181
|
+
- lib/suture/create/performs_surgery.rb
|
182
|
+
- lib/suture/create/validates_plan.rb
|
183
|
+
- lib/suture/delete.rb
|
184
|
+
- lib/suture/error/invalid_plan.rb
|
179
185
|
- lib/suture/error/observation_conflict.rb
|
186
|
+
- lib/suture/error/result_mismatch.rb
|
180
187
|
- lib/suture/error/schema_version.rb
|
181
188
|
- lib/suture/error/verification_failed.rb
|
182
|
-
- lib/suture/
|
183
|
-
- lib/suture/
|
184
|
-
- lib/suture/prescribes_test_plan.rb
|
189
|
+
- lib/suture/reset.rb
|
190
|
+
- lib/suture/surgeon/auditor.rb
|
185
191
|
- lib/suture/surgeon/no_op.rb
|
186
192
|
- lib/suture/surgeon/observer.rb
|
187
|
-
- lib/suture/
|
193
|
+
- lib/suture/surgeon/remediator.rb
|
188
194
|
- lib/suture/util/env.rb
|
195
|
+
- lib/suture/util/scalpel.rb
|
189
196
|
- lib/suture/util/shuffle.rb
|
190
197
|
- lib/suture/util/timer.rb
|
191
198
|
- lib/suture/value/observation.rb
|
192
199
|
- lib/suture/value/plan.rb
|
193
200
|
- lib/suture/value/test_plan.rb
|
194
201
|
- lib/suture/value/test_results.rb
|
202
|
+
- lib/suture/verify.rb
|
203
|
+
- lib/suture/verify/interprets_results.rb
|
204
|
+
- lib/suture/verify/prescribes_test_plan.rb
|
205
|
+
- lib/suture/verify/tests_patient.rb
|
195
206
|
- lib/suture/version.rb
|
196
207
|
- lib/suture/wrap/logger.rb
|
197
208
|
- lib/suture/wrap/sqlite.rb
|