suture 0.4.0 → 0.5.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/CHANGELOG.md +22 -0
- data/README.md +15 -6
- data/Rakefile +3 -3
- data/lib/suture/adapter/dictaphone.rb +38 -17
- data/lib/suture/adapter/log.rb +4 -0
- data/lib/suture/adapter/progress_bar.rb +13 -0
- data/lib/suture/config.rb +2 -1
- data/lib/suture/create/chooses_surgeon.rb +1 -1
- data/lib/suture/create/validates_plan.rb +6 -6
- data/lib/suture/delete.rb +6 -1
- data/lib/suture/error/observation_conflict.rb +2 -2
- data/lib/suture/error/verification_failed.rb +41 -20
- data/lib/suture/surgeon/no_op.rb +13 -2
- data/lib/suture/surgeon/observer.rb +9 -2
- data/lib/suture/util/compares_results.rb +18 -0
- data/lib/suture/util/scalpel.rb +30 -4
- data/lib/suture/value/observation.rb +14 -6
- data/lib/suture/value/plan.rb +4 -3
- data/lib/suture/value/result.rb +35 -0
- data/lib/suture/value/test_plan.rb +2 -1
- data/lib/suture/value/test_results.rb +8 -2
- data/lib/suture/verify/administers_test.rb +32 -0
- data/lib/suture/verify/tests_patient.rb +21 -19
- data/lib/suture/version.rb +1 -1
- data/lib/suture/wrap/logger.rb +15 -12
- data/lib/suture/wrap/sqlite.rb +7 -6
- data/suture.gemspec +2 -1
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ca2d3a0376378b6ead8f55b0f92e1ef20b73991
|
4
|
+
data.tar.gz: b506cf09899bfd1e372c088aaba8203a0f0c20aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30813362842d2a559181b248b08904187d96f78d6feac8b3662ef4a2e7dfa35a2cdda38f5b589184e531266574f4c1e7b23fb7fd68b749bd90d36f73f3a9d652
|
7
|
+
data.tar.gz: 94f1c2b749cadc2a1dc80ac962bc7bfee7d735cea0eae1940a49c09543466f167e25faa809915ae8077e70f34d93590cd1e9bee1d32278be0cd0c77c47ee1122
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [Unreleased](https://github.com/testdouble/suture/tree/HEAD)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/testdouble/suture/compare/v0.3.3...HEAD)
|
6
|
+
|
7
|
+
**Closed issues:**
|
8
|
+
|
9
|
+
- common test parent for unit tests [\#49](https://github.com/testdouble/suture/issues/49)
|
10
|
+
- rake test -\> rake unit w/ test as default [\#48](https://github.com/testdouble/suture/issues/48)
|
11
|
+
- Override flag to disable the seam / revert to old path [\#47](https://github.com/testdouble/suture/issues/47)
|
12
|
+
- Suture.verify :after\_subject hook [\#46](https://github.com/testdouble/suture/issues/46)
|
13
|
+
- Suture.create :call\_old\_on\_error flag [\#43](https://github.com/testdouble/suture/issues/43)
|
14
|
+
- Suture.create :on\_{new,old,subject}\_error handler [\#42](https://github.com/testdouble/suture/issues/42)
|
15
|
+
- Implement Suture.create :call\_both [\#41](https://github.com/testdouble/suture/issues/41)
|
16
|
+
- Suture.create :after\_old and :after\_new hooks [\#40](https://github.com/testdouble/suture/issues/40)
|
17
|
+
- Suture.create :raise\_on\_result\_mismatch [\#39](https://github.com/testdouble/suture/issues/39)
|
18
|
+
- Add code coverage / code climate [\#38](https://github.com/testdouble/suture/issues/38)
|
19
|
+
- implement changelog generator [\#37](https://github.com/testdouble/suture/issues/37)
|
20
|
+
- Validate plans [\#15](https://github.com/testdouble/suture/issues/15)
|
21
|
+
|
22
|
+
## [v0.3.3](https://github.com/testdouble/suture/tree/v0.3.3) (2016-08-23)
|
23
|
+
[Full Changelog](https://github.com/testdouble/suture/compare/v0.3.2...v0.3.3)
|
24
|
+
|
3
25
|
## [v0.3.2](https://github.com/testdouble/suture/tree/v0.3.2) (2016-08-23)
|
4
26
|
[Full Changelog](https://github.com/testdouble/suture/compare/v0.3.1...v0.3.2)
|
5
27
|
|
data/README.md
CHANGED
@@ -34,7 +34,7 @@ you'd like to change. A good seam is:
|
|
34
34
|
|
35
35
|
* easy to invoke in isolation
|
36
36
|
* takes arguments, returns a value
|
37
|
-
* eliminates (or at least minimizes) side effects
|
37
|
+
* eliminates (or at least minimizes) side effects (for more on side effects, see [this tutorial](https://semaphoreci.com/community/tutorials/isolate-side-effects-in-ruby))
|
38
38
|
|
39
39
|
Then, to create a seam, typically we create a new unit to house the code that we
|
40
40
|
excise from its original site, and then we call it. This adds a level of
|
@@ -150,8 +150,8 @@ the test environment has the data needed to behave the same way as when it was
|
|
150
150
|
recorded (it may be appropriate to take a snapshot of the database before you
|
151
151
|
start recording and load it before you run your tests)
|
152
152
|
|
153
|
-
* by generating a code coverage report
|
154
|
-
[simplecov](https://github.com/colszowka/simplecov) is a good one to start
|
153
|
+
* by generating a code coverage report
|
154
|
+
([simplecov](https://github.com/colszowka/simplecov) is a good one to start
|
155
155
|
with) from running this test in isolation, we can see what `LegacyWorker` is
|
156
156
|
actually calling, in an attempt to do two things:
|
157
157
|
* maximize coverage for code in the LegacyWorker (and for code that's
|
@@ -317,6 +317,7 @@ is a good choice).
|
|
317
317
|
Suture.config({
|
318
318
|
:log_level => "WARN", #<-- defaults to "INFO"
|
319
319
|
:log_stdout => false, #<-- defaults to true
|
320
|
+
:log_io => StringIO.new, #<-- defaults to nil
|
320
321
|
:log_file => "log/suture.log" #<-- defaults to nil
|
321
322
|
})
|
322
323
|
```
|
@@ -346,7 +347,7 @@ end
|
|
346
347
|
### Retrying failures
|
347
348
|
|
348
349
|
Since the legacy code path hasn't been deleted yet, there's no reason to leave
|
349
|
-
users hanging if the new code path explodes. By setting the `:
|
350
|
+
users hanging if the new code path explodes. By setting the `:fallback_on_error`
|
350
351
|
entry to `true`, Suture will rescue any errors raised from the new code path and
|
351
352
|
attempt to invoke the legacy code path instead.
|
352
353
|
|
@@ -357,7 +358,7 @@ class MyWorker
|
|
357
358
|
old: LegacyWorker.new,
|
358
359
|
new: NewWorker.new,
|
359
360
|
args: [id],
|
360
|
-
|
361
|
+
fallback_on_error: true
|
361
362
|
}))
|
362
363
|
end
|
363
364
|
end
|
@@ -368,6 +369,15 @@ path will go unnoticed, so it's best used in conjunction with Suture's logging
|
|
368
369
|
feature. Before ultimately deciding to finally delete the legacy code path,
|
369
370
|
double-check that the logs aren't full of rescued errors!
|
370
371
|
|
372
|
+
## Public API Summary
|
373
|
+
|
374
|
+
* `Suture.create(name, opts)` - Creates a seam in your production source code
|
375
|
+
* `Suture.verify(name, opts)` - Verifies a callable subject can recreate recorded calls
|
376
|
+
* `Suture.config(config)` - Sets logging options, as well global defaults for other properties
|
377
|
+
* `Suture.reset!` - Resets all Suture configuration
|
378
|
+
* `Suture.delete!(id)` - Deletes a recorded call by `id`
|
379
|
+
* `Suture.delete_all!(name)` - Deletes all recorded calls for a given seam `name`
|
380
|
+
|
371
381
|
## Configuration
|
372
382
|
|
373
383
|
Legacy code is, necessarily, complex and hard-to-wrangle. That's why Suture comes
|
@@ -479,4 +489,3 @@ seeing false negatives:
|
|
479
489
|
dropping Suture's database (which is, by default, stored in
|
480
490
|
`db/suture.sqlite3`) or by observing the ID of the recording from an error
|
481
491
|
message and invoking `Suture.delete!(42)`
|
482
|
-
|
data/Rakefile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
3
|
|
4
|
-
Rake::TestTask.new(:
|
4
|
+
Rake::TestTask.new(:unit) do |t|
|
5
5
|
t.libs << "test"
|
6
6
|
t.libs << "lib"
|
7
7
|
t.test_files = FileList['test/helper.rb', 'test/**/*_test.rb']
|
@@ -13,7 +13,7 @@ Rake::TestTask.new(:safe) do |t|
|
|
13
13
|
t.test_files = FileList['safe/helper.rb', 'safe/**/*_test.rb']
|
14
14
|
end
|
15
15
|
|
16
|
-
Rake::TestTask.new(:
|
16
|
+
Rake::TestTask.new(:test) do |t|
|
17
17
|
t.libs << "test"
|
18
18
|
t.libs << "safe"
|
19
19
|
t.libs << "lib"
|
@@ -39,4 +39,4 @@ if Gem.ruby_version >= Gem::Version.new("2.2.2")
|
|
39
39
|
end
|
40
40
|
|
41
41
|
|
42
|
-
task :default => :
|
42
|
+
task :default => :test
|
@@ -1,7 +1,9 @@
|
|
1
1
|
require "suture/wrap/sqlite"
|
2
2
|
require "suture/adapter/log"
|
3
3
|
require "suture/value/observation"
|
4
|
+
require "suture/value/result"
|
4
5
|
require "suture/error/observation_conflict"
|
6
|
+
require "suture/util/compares_results"
|
5
7
|
|
6
8
|
module Suture::Adapter
|
7
9
|
class Dictaphone
|
@@ -10,24 +12,20 @@ module Suture::Adapter
|
|
10
12
|
def initialize(plan)
|
11
13
|
@db = Suture::Wrap::Sqlite.init(plan.database_path)
|
12
14
|
@name = plan.name
|
13
|
-
@
|
15
|
+
@compares_results = Suture::Util::ComparesResults.new(plan.comparator)
|
14
16
|
if plan.respond_to?(:args) # does not apply to TestPlan objects
|
15
17
|
@args_inspect = plan.args.inspect
|
16
18
|
@args_dump = Marshal.dump(plan.args)
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
log_debug("skipped recording of duplicate call for seam #{@name.inspect} with args `#{@args_inspect}` and result `#{result.inspect}`")
|
28
|
-
else
|
29
|
-
raise Suture::Error::ObservationConflict.new(@name, @args_inspect, result, old_observation)
|
30
|
-
end
|
22
|
+
|
23
|
+
def record(returned_value)
|
24
|
+
record_result(Suture::Value::Result.returned(returned_value))
|
25
|
+
end
|
26
|
+
|
27
|
+
def record_error(error)
|
28
|
+
record_result(Suture::Value::Result.errored(error))
|
31
29
|
end
|
32
30
|
|
33
31
|
def play(only_id = nil)
|
@@ -40,19 +38,42 @@ module Suture::Adapter
|
|
40
38
|
rows.map { |row| row_to_observation(row) }
|
41
39
|
end
|
42
40
|
|
43
|
-
def
|
41
|
+
def delete_by_id!(id)
|
44
42
|
log_info("deleting call with ID: #{id}")
|
45
43
|
Suture::Wrap::Sqlite.delete(@db, :observations, "where id = ?", [id])
|
46
44
|
end
|
47
45
|
|
46
|
+
def delete_by_name!(name)
|
47
|
+
log_info("deleting calls for seam named: #{name}")
|
48
|
+
Suture::Wrap::Sqlite.delete(@db, :observations, "where name = ?", [name.to_s])
|
49
|
+
end
|
50
|
+
|
48
51
|
private
|
49
52
|
|
53
|
+
def record_result(result)
|
54
|
+
Suture::Wrap::Sqlite.insert(
|
55
|
+
@db,
|
56
|
+
:observations,
|
57
|
+
[:name, :args, result.errored? ? :error : :result],
|
58
|
+
[@name.to_s, @args_dump, Marshal.dump(result.value)]
|
59
|
+
)
|
60
|
+
log_info("recorded call for seam #{@name.inspect} with args `#{@args_inspect}` and result `#{result.value.inspect}`")
|
61
|
+
rescue SQLite3::ConstraintException
|
62
|
+
old_observation = known_observation
|
63
|
+
if @compares_results.compare(old_observation.result, result)
|
64
|
+
log_debug("skipped recording of duplicate call for seam #{@name.inspect} with args `#{@args_inspect}` and result `#{result.value.inspect}`")
|
65
|
+
else
|
66
|
+
raise Suture::Error::ObservationConflict.new(@name, @args_inspect, result, old_observation)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
50
70
|
def row_to_observation(row)
|
51
71
|
Suture::Value::Observation.new(
|
52
|
-
row[0],
|
53
|
-
row[1].to_sym,
|
54
|
-
Marshal.load(row[2]),
|
55
|
-
Marshal.load(row[3])
|
72
|
+
:id => row[0],
|
73
|
+
:name => row[1].to_sym,
|
74
|
+
:args => Marshal.load(row[2]),
|
75
|
+
:return => row[3] ? Marshal.load(row[3]) : nil,
|
76
|
+
:error => row[4] ? Marshal.load(row[4]) : nil
|
56
77
|
)
|
57
78
|
end
|
58
79
|
|
data/lib/suture/adapter/log.rb
CHANGED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bar-of-progress"
|
2
|
+
|
3
|
+
module Suture::Adapter
|
4
|
+
class ProgressBar
|
5
|
+
def initialize(attrs = {})
|
6
|
+
@options = {:length => 60}.merge(attrs)
|
7
|
+
end
|
8
|
+
|
9
|
+
def progress(numerator, denominator)
|
10
|
+
BarOfProgress.new(@options.merge(:total => denominator)).progress(numerator)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/suture/config.rb
CHANGED
@@ -35,13 +35,13 @@ module Suture
|
|
35
35
|
end
|
36
36
|
},
|
37
37
|
lambda { |plan|
|
38
|
-
if plan.record_calls && plan.
|
39
|
-
":record_calls & :
|
38
|
+
if plan.record_calls && plan.fallback_on_error
|
39
|
+
":record_calls & :fallback_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 :fallback_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
40
|
end
|
41
41
|
},
|
42
42
|
lambda { |plan|
|
43
|
-
if plan.call_both && plan.
|
44
|
-
":call_both & :
|
43
|
+
if plan.call_both && plan.fallback_on_error
|
44
|
+
":call_both & :fallback_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 :fallback_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
45
|
end
|
46
46
|
},
|
47
47
|
lambda { |plan|
|
@@ -50,8 +50,8 @@ module Suture
|
|
50
50
|
end
|
51
51
|
},
|
52
52
|
lambda { |plan|
|
53
|
-
if plan.
|
54
|
-
":
|
53
|
+
if plan.fallback_on_error && !plan.new.respond_to?(:call)
|
54
|
+
":fallback_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 :fallback_on_error."
|
55
55
|
end
|
56
56
|
}
|
57
57
|
]
|
data/lib/suture/delete.rb
CHANGED
@@ -4,6 +4,11 @@ require "suture/adapter/dictaphone"
|
|
4
4
|
module Suture
|
5
5
|
def self.delete!(id, options = {})
|
6
6
|
plan = BuildsPlan.new.build(:name_not_used_here, options)
|
7
|
-
Adapter::Dictaphone.new(plan).
|
7
|
+
Adapter::Dictaphone.new(plan).delete_by_id!(id)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.delete_all!(name, options = {})
|
11
|
+
plan = BuildsPlan.new.build(:name_not_used_here, options)
|
12
|
+
Adapter::Dictaphone.new(plan).delete_by_name!(name)
|
8
13
|
end
|
9
14
|
end
|
@@ -17,10 +17,10 @@ module Suture::Error
|
|
17
17
|
#{@args_inspect}
|
18
18
|
```
|
19
19
|
Previously-observed return value: ```
|
20
|
-
#{@old_result.inspect}
|
20
|
+
#{@old_result.value.inspect}
|
21
21
|
```
|
22
22
|
Newly-observed return value: ```
|
23
|
-
#{@new_result.inspect}
|
23
|
+
#{@new_result.value.inspect}
|
24
24
|
```
|
25
25
|
|
26
26
|
That's not good! Here are a few ideas of what may have happened:
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "suture/adapter/progress_bar"
|
2
|
+
|
1
3
|
module Suture::Error
|
2
4
|
class VerificationFailed < StandardError
|
3
5
|
def initialize(plan, results)
|
@@ -10,8 +12,10 @@ module Suture::Error
|
|
10
12
|
intro,
|
11
13
|
describe_failures(@results.failed, @plan),
|
12
14
|
configuration(@plan),
|
13
|
-
summarize(@results)
|
14
|
-
|
15
|
+
summarize(@results),
|
16
|
+
progress(@plan, @results)
|
17
|
+
].compact.join("\n").tap do
|
18
|
+
end
|
15
19
|
end
|
16
20
|
|
17
21
|
private
|
@@ -37,6 +41,23 @@ module Suture::Error
|
|
37
41
|
MSG
|
38
42
|
end
|
39
43
|
|
44
|
+
def progress(plan, results)
|
45
|
+
return if plan.fail_fast
|
46
|
+
bar = Suture::Adapter::ProgressBar.new.progress(
|
47
|
+
results.passed.size,
|
48
|
+
results.all.size
|
49
|
+
)
|
50
|
+
<<-MSG.gsub(/^ {8}/,'')
|
51
|
+
## Progress
|
52
|
+
|
53
|
+
Here's what your progress to initial completion looks like so far:
|
54
|
+
|
55
|
+
#{bar}
|
56
|
+
|
57
|
+
Of #{results.all.size} recorded interactions, #{results.passed.size} are currently passing.
|
58
|
+
MSG
|
59
|
+
end
|
60
|
+
|
40
61
|
def configuration(plan)
|
41
62
|
<<-MSG.gsub(/^ {8}/,'')
|
42
63
|
# Configuration
|
@@ -88,24 +109,24 @@ module Suture::Error
|
|
88
109
|
return <<-MSG.gsub(/^ {8}/,'')
|
89
110
|
#{index + 1}.) Recorded call for seam #{expected.name.inspect} (ID: #{expected.id}) ran and #{failure[:error] ? "raised an error" : "failed comparison"}.
|
90
111
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
112
|
+
Arguments: ```
|
113
|
+
#{expected.args.inspect}
|
114
|
+
```
|
115
|
+
Expected #{expected.result.errored? ? "error raised" : "returned value"}: ```
|
116
|
+
#{expected.result.value.inspect}
|
117
|
+
```
|
118
|
+
Actual #{(failure[:error] || failure[:new_result].errored?) ? "error raised" : "returned value"}: ```
|
119
|
+
#{if failure[:error]
|
120
|
+
stringify_error(failure[:error])
|
121
|
+
else
|
122
|
+
failure[:new_result].value.inspect
|
123
|
+
end
|
124
|
+
}
|
125
|
+
```
|
126
|
+
|
127
|
+
Ideas to fix this:
|
128
|
+
* Focus on this test by setting ENV var `SUTURE_VERIFY_ONLY=#{expected.id}`
|
129
|
+
* Is the recording wrong? Delete it! `Suture.delete!(#{expected.id})`
|
109
130
|
MSG
|
110
131
|
end
|
111
132
|
|
data/lib/suture/surgeon/no_op.rb
CHANGED
@@ -3,8 +3,19 @@ require "suture/util/scalpel"
|
|
3
3
|
module Suture::Surgeon
|
4
4
|
class NoOp
|
5
5
|
def operate(plan)
|
6
|
-
|
7
|
-
|
6
|
+
if code_path = code_path_for(plan)
|
7
|
+
Suture::Util::Scalpel.new.cut(plan, code_path)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def code_path_for(plan)
|
14
|
+
if plan.new && !plan.disable
|
15
|
+
:new
|
16
|
+
elsif plan.old
|
17
|
+
:old
|
18
|
+
end
|
8
19
|
end
|
9
20
|
end
|
10
21
|
end
|
@@ -5,8 +5,15 @@ module Suture::Surgeon
|
|
5
5
|
class Observer
|
6
6
|
def operate(plan)
|
7
7
|
dictaphone = Suture::Adapter::Dictaphone.new(plan)
|
8
|
-
|
9
|
-
|
8
|
+
begin
|
9
|
+
Suture::Util::Scalpel.new.cut(plan, :old).tap do |result|
|
10
|
+
dictaphone.record(result)
|
11
|
+
end
|
12
|
+
rescue StandardError => error
|
13
|
+
if plan.expected_error_types.any? {|e| error.kind_of?(e) }
|
14
|
+
dictaphone.record_error(error)
|
15
|
+
end
|
16
|
+
raise error
|
10
17
|
end
|
11
18
|
end
|
12
19
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Suture::Util
|
2
|
+
class ComparesResults
|
3
|
+
def initialize(comparator)
|
4
|
+
@comparator = comparator
|
5
|
+
end
|
6
|
+
|
7
|
+
def compare(expected, actual)
|
8
|
+
if expected.errored? != actual.errored?
|
9
|
+
false
|
10
|
+
elsif expected.errored?
|
11
|
+
actual.value.kind_of?(expected.value.class) &&
|
12
|
+
expected.value.message == actual.value.message
|
13
|
+
else
|
14
|
+
@comparator.call(expected.value, actual.value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/suture/util/scalpel.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
+
require "suture/adapter/log"
|
2
|
+
|
1
3
|
module Suture::Util
|
2
4
|
class Scalpel
|
5
|
+
include Suture::Adapter::Log
|
6
|
+
|
3
7
|
def cut(plan, location, args_override = nil)
|
4
|
-
args =
|
8
|
+
args = args_for(plan, args_override)
|
5
9
|
begin
|
6
10
|
plan.send(location).call(*args).tap do |result|
|
7
11
|
call_after_hook(plan, location, args, result)
|
8
12
|
end
|
9
13
|
rescue StandardError => error
|
14
|
+
log_error_details(plan, location, args, error)
|
10
15
|
call_error_hook(plan, location, args, error)
|
11
16
|
raise error
|
12
17
|
end
|
@@ -16,16 +21,37 @@ module Suture::Util
|
|
16
21
|
|
17
22
|
def call_after_hook(plan, location, args, result)
|
18
23
|
return unless after_hook = try(plan, "after_#{location}")
|
19
|
-
after_hook.call(
|
24
|
+
after_hook.call(args, result)
|
25
|
+
end
|
26
|
+
|
27
|
+
def log_error_details(plan, location, args, error)
|
28
|
+
return if expected_error?(plan, error)
|
29
|
+
log_error <<-MSG.gsub(/^ {8}/,'')
|
30
|
+
Suture invoked the #{plan.name.inspect} seam's #{location.inspect} code path with args: ```
|
31
|
+
#{args.inspect}
|
32
|
+
```
|
33
|
+
which raised a #{error.class} with message: ```
|
34
|
+
#{error.message}
|
35
|
+
```
|
36
|
+
MSG
|
20
37
|
end
|
21
38
|
|
22
39
|
def call_error_hook(plan, location, args, error)
|
23
|
-
return if plan
|
40
|
+
return if expected_error?(plan, error)
|
24
41
|
return unless error_hook = try(plan, "on_#{location}_error")
|
25
42
|
error_hook.call(plan.name, args, error)
|
26
43
|
end
|
27
44
|
|
28
|
-
def
|
45
|
+
def expected_error?(plan, error)
|
46
|
+
plan.expected_error_types.any? {|e| error.kind_of?(e) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def args_for(plan, args_override)
|
50
|
+
args = pick_args(plan, args_override)
|
51
|
+
try(plan, :dup_args) ? Marshal.load(Marshal.dump(args)) : args
|
52
|
+
end
|
53
|
+
|
54
|
+
def pick_args(plan, args_override)
|
29
55
|
return args_override if args_override
|
30
56
|
if plan.respond_to?(:args)
|
31
57
|
plan.args
|
@@ -1,11 +1,19 @@
|
|
1
|
+
require "suture/value/result"
|
2
|
+
|
1
3
|
module Suture::Value
|
2
4
|
class Observation
|
3
|
-
attr_reader :id, :name, :args
|
4
|
-
|
5
|
-
|
6
|
-
@
|
7
|
-
@
|
8
|
-
@
|
5
|
+
attr_reader :id, :name, :args
|
6
|
+
|
7
|
+
def initialize(attrs)
|
8
|
+
@id = attrs[:id]
|
9
|
+
@name = attrs[:name]
|
10
|
+
@args = attrs[:args]
|
11
|
+
@return_value = attrs[:return]
|
12
|
+
@error = attrs[:error]
|
13
|
+
end
|
14
|
+
|
15
|
+
def result
|
16
|
+
@expectation ||= @error ? Result.errored(@error) : Result.returned(@return_value)
|
9
17
|
end
|
10
18
|
end
|
11
19
|
end
|
data/lib/suture/value/plan.rb
CHANGED
@@ -2,8 +2,8 @@ module Suture::Value
|
|
2
2
|
class Plan
|
3
3
|
attr_reader :name, :old, :new, :args, :after_new, :after_old, :on_new_error,
|
4
4
|
:on_old_error, :database_path, :record_calls, :comparator,
|
5
|
-
:call_both, :raise_on_result_mismatch, :
|
6
|
-
:expected_error_types, :disable
|
5
|
+
:call_both, :raise_on_result_mismatch, :fallback_on_error,
|
6
|
+
:expected_error_types, :disable, :dup_args
|
7
7
|
|
8
8
|
def initialize(attrs = {})
|
9
9
|
@name = attrs[:name]
|
@@ -19,9 +19,10 @@ module Suture::Value
|
|
19
19
|
@comparator = attrs[:comparator]
|
20
20
|
@call_both = !!attrs[:call_both]
|
21
21
|
@raise_on_result_mismatch = !!attrs[:raise_on_result_mismatch]
|
22
|
-
@
|
22
|
+
@fallback_on_error = !!attrs[:fallback_on_error]
|
23
23
|
@expected_error_types = attrs[:expected_error_types] || []
|
24
24
|
@disable = !!attrs[:disable]
|
25
|
+
@dup_args = !!attrs[:dup_args]
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Suture::Value
|
2
|
+
class Result
|
3
|
+
def self.errored(error)
|
4
|
+
new(:error, error)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.returned(return_value)
|
8
|
+
new(:return_value, return_value)
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :value
|
12
|
+
def initialize(result_type, value)
|
13
|
+
@value = value
|
14
|
+
@errored = result_type == :error
|
15
|
+
end
|
16
|
+
|
17
|
+
def errored?
|
18
|
+
@errored
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
other.kind_of?(self.class) && other.state == state
|
23
|
+
end
|
24
|
+
|
25
|
+
def hash
|
26
|
+
state.hash
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def state
|
32
|
+
[@value, @errored]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -7,11 +7,12 @@ module Suture::Value
|
|
7
7
|
:expected_error_types
|
8
8
|
|
9
9
|
def initialize(attrs = {})
|
10
|
-
assign_simple_ivars!(attrs, :name, :subject, :
|
10
|
+
assign_simple_ivars!(attrs, :name, :subject, :comparator,
|
11
11
|
:database_path, :after_subject,
|
12
12
|
:on_subject_error)
|
13
13
|
assign_integral_ivars!(attrs, :verify_only, :call_limit, :time_limit,
|
14
14
|
:error_message_limit)
|
15
|
+
@fail_fast = !!attrs[:fail_fast]
|
15
16
|
@expected_error_types = attrs[:expected_error_types] || []
|
16
17
|
@random_seed = determine_random_seed(attrs)
|
17
18
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
module Suture::Value
|
2
2
|
class TestResults
|
3
|
-
attr_reader :results
|
4
|
-
|
5
3
|
def initialize(results)
|
6
4
|
@results = results
|
7
5
|
end
|
@@ -22,6 +20,14 @@ module Suture::Value
|
|
22
20
|
@results.count { |r| r[:passed] }
|
23
21
|
end
|
24
22
|
|
23
|
+
def all
|
24
|
+
@results
|
25
|
+
end
|
26
|
+
|
27
|
+
def passed
|
28
|
+
@results.select { |r| r[:passed] }
|
29
|
+
end
|
30
|
+
|
25
31
|
def failed
|
26
32
|
@results.select { |r| !r[:passed] && r[:ran] }
|
27
33
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "suture/util/compares_results"
|
2
|
+
require "suture/util/scalpel"
|
3
|
+
require "suture/value/result"
|
4
|
+
|
5
|
+
module Suture
|
6
|
+
class AdministersTest
|
7
|
+
def initialize
|
8
|
+
@scalpel = Util::Scalpel.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def administer(test_plan, observation)
|
12
|
+
compares_results = Util::ComparesResults.new(test_plan.comparator)
|
13
|
+
begin
|
14
|
+
result = Value::Result.returned(@scalpel.cut(test_plan, :subject, observation.args))
|
15
|
+
{
|
16
|
+
:new_result => result,
|
17
|
+
:passed => compares_results.compare(observation.result, result)
|
18
|
+
}
|
19
|
+
rescue StandardError => error
|
20
|
+
if observation.result.errored?
|
21
|
+
result = Value::Result.errored(error)
|
22
|
+
{
|
23
|
+
:new_result => result,
|
24
|
+
:passed => compares_results.compare(observation.result, result)
|
25
|
+
}
|
26
|
+
else
|
27
|
+
{ :error => error, :passed => false }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,14 +1,17 @@
|
|
1
|
+
require "suture/verify/administers_test"
|
2
|
+
require "suture/adapter/log"
|
1
3
|
require "suture/adapter/dictaphone"
|
2
4
|
require "suture/value/test_results"
|
3
|
-
require "suture/util/scalpel"
|
4
5
|
require "suture/util/shuffle"
|
5
6
|
require "suture/util/timer"
|
6
7
|
require "backports/1.9.2/random"
|
7
8
|
|
8
9
|
module Suture
|
9
10
|
class TestsPatient
|
11
|
+
include Suture::Adapter::Log
|
12
|
+
|
10
13
|
def initialize
|
11
|
-
@
|
14
|
+
@administers_test = AdministersTest.new
|
12
15
|
end
|
13
16
|
|
14
17
|
def test(test_plan)
|
@@ -16,15 +19,13 @@ module Suture
|
|
16
19
|
timer = Suture::Util::Timer.new(test_plan.time_limit) unless test_plan.time_limit.nil?
|
17
20
|
test_cases = build_test_cases(test_plan)
|
18
21
|
Value::TestResults.new(test_cases.each_with_index.map { |observation, i|
|
19
|
-
if (test_plan
|
20
|
-
(test_plan.call_limit && i >= test_plan.call_limit) ||
|
21
|
-
(timer && timer.time_up?)
|
22
|
+
if should_skip?(test_plan, experienced_failure_in_life, i, timer)
|
22
23
|
{
|
23
24
|
:observation => observation,
|
24
25
|
:ran => false
|
25
26
|
}
|
26
27
|
else
|
27
|
-
|
28
|
+
@administers_test.administer(test_plan, observation).merge({
|
28
29
|
:observation => observation,
|
29
30
|
:ran => true
|
30
31
|
}).tap { |r| experienced_failure_in_life = true unless r[:passed] }
|
@@ -34,30 +35,31 @@ module Suture
|
|
34
35
|
|
35
36
|
private
|
36
37
|
|
38
|
+
def should_skip?(test_plan, failed_fast, call_count, timer)
|
39
|
+
(test_plan.fail_fast && failed_fast) ||
|
40
|
+
(test_plan.call_limit && call_count >= test_plan.call_limit) ||
|
41
|
+
(timer && timer.time_up?)
|
42
|
+
end
|
43
|
+
|
37
44
|
def build_test_cases(test_plan)
|
38
45
|
dictaphone = Suture::Adapter::Dictaphone.new(test_plan)
|
39
46
|
shuffle(
|
40
47
|
dictaphone.play(test_plan.verify_only),
|
41
48
|
test_plan.random_seed
|
42
|
-
)
|
49
|
+
).tap do |test_cases|
|
50
|
+
next if test_cases.size > 0
|
51
|
+
log_warn <<-MSG.gsub(/^ {10}/,'')
|
52
|
+
Suture.verify found no recorded calls for seam #{test_plan.name.inspect}.
|
53
|
+
As a result, verify will have no effect and cannot provide any assurance
|
54
|
+
that the subject is working as expected.
|
55
|
+
MSG
|
56
|
+
end
|
43
57
|
end
|
44
58
|
|
45
59
|
def shuffle(rows, random_seed)
|
46
60
|
return rows unless random_seed
|
47
61
|
Suture::Util::Shuffle.shuffle(Random.new(random_seed), rows)
|
48
62
|
end
|
49
|
-
|
50
|
-
def invoke(test_plan, observation)
|
51
|
-
{}.tap do |result|
|
52
|
-
begin
|
53
|
-
result[:new_result] = @scalpel.cut(test_plan, :subject, observation.args)
|
54
|
-
result[:passed] = test_plan.comparator.call(observation.result, result[:new_result])
|
55
|
-
rescue StandardError => e
|
56
|
-
result[:passed] = false
|
57
|
-
result[:error] = e
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
data/lib/suture/version.rb
CHANGED
data/lib/suture/wrap/logger.rb
CHANGED
@@ -3,14 +3,12 @@ require "logger"
|
|
3
3
|
module Suture::Wrap
|
4
4
|
module Logger
|
5
5
|
def self.init(options)
|
6
|
-
if options[:log_file]
|
6
|
+
@logger = if options[:log_file]
|
7
7
|
full_path = File.join(Dir.getwd, options[:log_file])
|
8
8
|
FileUtils.mkdir_p(File.dirname(full_path))
|
9
|
-
|
10
|
-
elsif options[:log_stdout]
|
11
|
-
@logger = ::Logger.new(STDOUT)
|
9
|
+
::Logger.new(full_path)
|
12
10
|
else
|
13
|
-
|
11
|
+
::Logger.new(NullIO.new)
|
14
12
|
end
|
15
13
|
|
16
14
|
@logger.level = if options[:log_level]
|
@@ -22,19 +20,24 @@ module Suture::Wrap
|
|
22
20
|
@logger.formatter = proc { |_, time , _, msg|
|
23
21
|
formatted_time = time.strftime("%Y-%m-%dT%H:%M:%S.%6N")
|
24
22
|
"[#{formatted_time}] Suture: #{msg}\n".tap { |out|
|
25
|
-
puts out if options[:
|
23
|
+
puts out if options[:log_stdout]
|
24
|
+
options[:log_io].write(out) if options[:log_io]
|
26
25
|
}
|
27
26
|
}
|
28
27
|
|
29
28
|
return @logger
|
30
29
|
end
|
31
30
|
|
32
|
-
class
|
33
|
-
def
|
34
|
-
def
|
35
|
-
def
|
36
|
-
def
|
37
|
-
def
|
31
|
+
class NullIO
|
32
|
+
def gets; end
|
33
|
+
def each; end
|
34
|
+
def read(count=nil,buffer=nil); (count && count > 0) ? nil : ""; end
|
35
|
+
def rewind; 0; end
|
36
|
+
def close; end
|
37
|
+
def size; 0; end
|
38
|
+
def sync=(*args); end
|
39
|
+
def puts(*args); end
|
40
|
+
def write(*args); end
|
38
41
|
end
|
39
42
|
end
|
40
43
|
end
|
data/lib/suture/wrap/sqlite.rb
CHANGED
@@ -4,18 +4,18 @@ require "suture/error/schema_version"
|
|
4
4
|
|
5
5
|
module Suture::Wrap
|
6
6
|
module Sqlite
|
7
|
-
SCHEMA_VERSION=
|
7
|
+
SCHEMA_VERSION=2
|
8
8
|
def self.init(location)
|
9
9
|
full_path = File.join(Dir.getwd, location)
|
10
10
|
FileUtils.mkdir_p(File.dirname(full_path))
|
11
11
|
SQLite3::Database.new(full_path).tap do |db|
|
12
12
|
db.execute <<-SQL
|
13
|
-
create table if not exists
|
13
|
+
create table if not exists suture_schema_info (
|
14
14
|
version integer unique
|
15
15
|
);
|
16
16
|
SQL
|
17
|
-
db.execute("insert or ignore into
|
18
|
-
actual_schema_version = db.execute("select * from
|
17
|
+
db.execute("insert or ignore into suture_schema_info values (?)", [SCHEMA_VERSION])
|
18
|
+
actual_schema_version = db.execute("select * from suture_schema_info").first[0]
|
19
19
|
if SCHEMA_VERSION != actual_schema_version
|
20
20
|
raise Suture::Error::SchemaVersion.new(SCHEMA_VERSION, actual_schema_version)
|
21
21
|
end
|
@@ -23,9 +23,10 @@ module Suture::Wrap
|
|
23
23
|
db.execute <<-SQL
|
24
24
|
create table if not exists observations (
|
25
25
|
id integer primary key,
|
26
|
-
name varchar(255),
|
27
|
-
args clob,
|
26
|
+
name varchar(255) not null,
|
27
|
+
args clob not null,
|
28
28
|
result clob,
|
29
|
+
error clob,
|
29
30
|
unique(name, args)
|
30
31
|
);
|
31
32
|
SQL
|
data/suture.gemspec
CHANGED
@@ -13,13 +13,14 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.description = %q{Provides tools to record calls to legacy code and verify new implementations still work}
|
14
14
|
spec.homepage = "https://github.com/testdouble/suture"
|
15
15
|
|
16
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|db|log|safe|spec|features)/}) }
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(example|test|db|log|safe|spec|features)/}) }
|
17
17
|
spec.bindir = "exe"
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_dependency "sqlite3"
|
22
22
|
spec.add_dependency "backports"
|
23
|
+
spec.add_dependency "bar-of-progress", ">= 0.1.3"
|
23
24
|
|
24
25
|
spec.add_development_dependency "bundler", "~> 1.12"
|
25
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
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.5.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-
|
11
|
+
date: 2016-09-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bar-of-progress
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.3
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.1.3
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -173,6 +187,7 @@ files:
|
|
173
187
|
- lib/suture.rb
|
174
188
|
- lib/suture/adapter/dictaphone.rb
|
175
189
|
- lib/suture/adapter/log.rb
|
190
|
+
- lib/suture/adapter/progress_bar.rb
|
176
191
|
- lib/suture/comparator.rb
|
177
192
|
- lib/suture/config.rb
|
178
193
|
- lib/suture/create.rb
|
@@ -191,15 +206,18 @@ files:
|
|
191
206
|
- lib/suture/surgeon/no_op.rb
|
192
207
|
- lib/suture/surgeon/observer.rb
|
193
208
|
- lib/suture/surgeon/remediator.rb
|
209
|
+
- lib/suture/util/compares_results.rb
|
194
210
|
- lib/suture/util/env.rb
|
195
211
|
- lib/suture/util/scalpel.rb
|
196
212
|
- lib/suture/util/shuffle.rb
|
197
213
|
- lib/suture/util/timer.rb
|
198
214
|
- lib/suture/value/observation.rb
|
199
215
|
- lib/suture/value/plan.rb
|
216
|
+
- lib/suture/value/result.rb
|
200
217
|
- lib/suture/value/test_plan.rb
|
201
218
|
- lib/suture/value/test_results.rb
|
202
219
|
- lib/suture/verify.rb
|
220
|
+
- lib/suture/verify/administers_test.rb
|
203
221
|
- lib/suture/verify/interprets_results.rb
|
204
222
|
- lib/suture/verify/prescribes_test_plan.rb
|
205
223
|
- lib/suture/verify/tests_patient.rb
|