suture 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +28 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +4 -0
- data/README.md +143 -9
- data/Rakefile +22 -1
- data/lib/suture/adapter/dictaphone.rb +41 -23
- data/lib/suture/adapter/log.rb +31 -0
- data/lib/suture/builds_plan.rb +2 -2
- data/lib/suture/chooses_surgeon.rb +10 -0
- data/lib/suture/comparator.rb +7 -0
- data/lib/suture/error/observation_conflict.rb +26 -13
- data/lib/suture/error/verification_failed.rb +160 -5
- data/lib/suture/interprets_results.rb +2 -2
- data/lib/suture/prescribes_test_plan.rb +4 -4
- data/lib/suture/surgeon/observer.rb +11 -1
- data/lib/suture/tests_patient.rb +31 -7
- data/lib/suture/util/env.rb +9 -4
- data/lib/suture/util/shuffle.rb +13 -0
- data/lib/suture/util/timer.rb +14 -0
- data/lib/suture/value/plan.rb +2 -1
- data/lib/suture/value/test_plan.rb +35 -6
- data/lib/suture/value/test_results.rb +5 -1
- data/lib/suture/version.rb +1 -1
- data/lib/suture/wrap/logger.rb +40 -0
- data/lib/suture/wrap/sqlite.rb +8 -1
- data/lib/suture.rb +24 -2
- data/suture.gemspec +8 -1
- metadata +65 -2
@@ -4,10 +4,20 @@ module Suture::Surgeon
|
|
4
4
|
class Observer
|
5
5
|
def operate(plan)
|
6
6
|
dictaphone = Suture::Adapter::Dictaphone.new(plan)
|
7
|
-
|
7
|
+
invoke(plan).tap do |result|
|
8
8
|
dictaphone.record(result)
|
9
9
|
end
|
10
10
|
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
|
11
21
|
end
|
12
22
|
end
|
13
23
|
|
data/lib/suture/tests_patient.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
require "suture/adapter/dictaphone"
|
2
2
|
require "suture/value/test_results"
|
3
|
+
require "suture/util/timer"
|
4
|
+
require "suture/util/shuffle"
|
5
|
+
require "backports/1.9.2/random"
|
3
6
|
|
4
7
|
module Suture
|
5
8
|
class TestsPatient
|
6
9
|
def test(test_plan)
|
7
|
-
dictaphone = Suture::Adapter::Dictaphone.new(test_plan)
|
8
10
|
experienced_failure_in_life = false
|
9
|
-
|
10
|
-
|
11
|
+
timer = Suture::Util::Timer.new(test_plan.time_limit) unless test_plan.time_limit.nil?
|
12
|
+
test_cases = build_test_cases(test_plan)
|
13
|
+
Value::TestResults.new(test_cases.each_with_index.map { |observation, i|
|
14
|
+
if (test_plan.fail_fast && experienced_failure_in_life) ||
|
15
|
+
(test_plan.call_limit && i >= test_plan.call_limit) ||
|
16
|
+
(timer && timer.time_up?)
|
11
17
|
{
|
12
18
|
:observation => observation,
|
13
19
|
:ran => false
|
@@ -16,17 +22,35 @@ module Suture
|
|
16
22
|
invoke(test_plan, observation).merge({
|
17
23
|
:observation => observation,
|
18
24
|
:ran => true
|
19
|
-
}).tap { |r| experienced_failure_in_life = true unless r[:passed]}
|
25
|
+
}).tap { |r| experienced_failure_in_life = true unless r[:passed] }
|
20
26
|
end
|
21
27
|
})
|
22
28
|
end
|
23
29
|
|
30
|
+
private
|
31
|
+
|
32
|
+
def build_test_cases(test_plan)
|
33
|
+
dictaphone = Suture::Adapter::Dictaphone.new(test_plan)
|
34
|
+
shuffle(
|
35
|
+
dictaphone.play(test_plan.verify_only),
|
36
|
+
test_plan.random_seed
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def shuffle(rows, random_seed)
|
41
|
+
return rows unless random_seed
|
42
|
+
Suture::Util::Shuffle.shuffle(Random.new(random_seed), rows)
|
43
|
+
end
|
44
|
+
|
24
45
|
def invoke(test_plan, observation)
|
25
46
|
{}.tap do |result|
|
26
47
|
begin
|
27
|
-
result[:new_result] =
|
28
|
-
|
29
|
-
|
48
|
+
result[:new_result] = if observation.args
|
49
|
+
test_plan.subject.call(*observation.args)
|
50
|
+
else
|
51
|
+
test_plan.subject.call
|
52
|
+
end
|
53
|
+
result[:passed] = test_plan.comparator.call(observation.result, result[:new_result])
|
30
54
|
rescue StandardError => e
|
31
55
|
result[:passed] = false
|
32
56
|
result[:error] = e
|
data/lib/suture/util/env.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
module Suture::Util
|
2
2
|
module Env
|
3
|
-
def self.to_map(excludes)
|
3
|
+
def self.to_map(excludes = {})
|
4
4
|
Hash[
|
5
5
|
ENV.keys.
|
6
6
|
select { |k| k.start_with?("SUTURE_") }.
|
7
7
|
map { |k| [to_sym(k), sanitize_value(ENV[k])] }
|
8
|
-
].reject { |(k,
|
8
|
+
].reject { |(k,_)| excludes.include?(k) }
|
9
9
|
end
|
10
10
|
|
11
11
|
# private
|
@@ -15,8 +15,13 @@ module Suture::Util
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.sanitize_value(value)
|
18
|
-
|
19
|
-
|
18
|
+
if value == "false"
|
19
|
+
false
|
20
|
+
elsif value == "true"
|
21
|
+
true
|
22
|
+
else
|
23
|
+
value
|
24
|
+
end
|
20
25
|
end
|
21
26
|
end
|
22
27
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Suture::Util
|
2
|
+
module Shuffle
|
3
|
+
def self.shuffle(random, unshuffled_array)
|
4
|
+
old_array = unshuffled_array.dup
|
5
|
+
new_array = []
|
6
|
+
while new_array.size < unshuffled_array.size
|
7
|
+
index = random.rand(old_array.size)
|
8
|
+
new_array << old_array.delete_at(index)
|
9
|
+
end
|
10
|
+
return new_array
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/suture/value/plan.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
module Suture::Value
|
2
2
|
class Plan
|
3
|
-
attr_reader :name, :old, :new, :args, :record_calls, :database_path
|
3
|
+
attr_reader :name, :old, :new, :args, :record_calls, :comparator, :database_path
|
4
4
|
def initialize(attrs = {})
|
5
5
|
@name = attrs[:name]
|
6
6
|
@old = attrs[:old]
|
7
7
|
@new = attrs[:new]
|
8
8
|
@args = attrs[:args]
|
9
9
|
@record_calls = !!attrs[:record_calls]
|
10
|
+
@comparator = attrs[:comparator]
|
10
11
|
@database_path = attrs[:database_path]
|
11
12
|
end
|
12
13
|
end
|
@@ -1,12 +1,41 @@
|
|
1
1
|
module Suture::Value
|
2
2
|
class TestPlan
|
3
|
-
attr_accessor :name, :subject,
|
3
|
+
attr_accessor :name, :subject,
|
4
|
+
:verify_only, :fail_fast, :call_limit, :time_limit,
|
5
|
+
:error_message_limit, :random_seed,
|
6
|
+
:comparator, :database_path
|
4
7
|
def initialize(attrs = {})
|
5
|
-
|
6
|
-
|
7
|
-
@
|
8
|
-
|
9
|
-
|
8
|
+
assign_simple_ivars!(attrs, :name, :subject, :fail_fast, :comparator, :database_path)
|
9
|
+
assign_integral_ivars(attrs, :verify_only, :call_limit, :time_limit, :error_message_limit)
|
10
|
+
@random_seed = determine_random_seed(attrs)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def assign_simple_ivars!(attrs, *names)
|
16
|
+
names.each do |name|
|
17
|
+
instance_variable_set("@#{name}", attrs[name])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def assign_integral_ivars(attrs, *names)
|
22
|
+
assign_simple_ivars!(
|
23
|
+
Hash[attrs.select {|(k,_)| names.include?(k) }.
|
24
|
+
map { |(k,v)| [k, v.nil? ? nil : v.to_i]}],
|
25
|
+
*names
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def determine_random_seed(attrs)
|
30
|
+
if attrs.has_key?(:random_seed)
|
31
|
+
if attrs[:random_seed].nil? || attrs[:random_seed] == "nil"
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
attrs[:random_seed].to_i
|
35
|
+
end
|
36
|
+
else
|
37
|
+
rand(99999)
|
38
|
+
end
|
10
39
|
end
|
11
40
|
end
|
12
41
|
end
|
@@ -22,8 +22,12 @@ module Suture::Value
|
|
22
22
|
@results.count { |r| r[:passed] }
|
23
23
|
end
|
24
24
|
|
25
|
+
def failed
|
26
|
+
@results.select { |r| !r[:passed] && r[:ran] }
|
27
|
+
end
|
28
|
+
|
25
29
|
def failed_count
|
26
|
-
|
30
|
+
failed.size
|
27
31
|
end
|
28
32
|
|
29
33
|
def errored_count
|
data/lib/suture/version.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Suture::Wrap
|
4
|
+
module Logger
|
5
|
+
def self.init(options)
|
6
|
+
if options[:log_file]
|
7
|
+
full_path = File.join(Dir.getwd, options[:log_file])
|
8
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
9
|
+
@logger = ::Logger.new(full_path)
|
10
|
+
elsif options[:log_stdout]
|
11
|
+
@logger = ::Logger.new(STDOUT)
|
12
|
+
else
|
13
|
+
@logger = NullLogger.new
|
14
|
+
end
|
15
|
+
|
16
|
+
@logger.level = if options[:log_level]
|
17
|
+
::Logger.const_get(options[:log_level])
|
18
|
+
else
|
19
|
+
::Logger::INFO
|
20
|
+
end
|
21
|
+
|
22
|
+
@logger.formatter = proc { |_, time , _, msg|
|
23
|
+
formatted_time = time.strftime("%Y-%m-%dT%H:%M:%S.%6N")
|
24
|
+
"[#{formatted_time}] Suture: #{msg}\n".tap { |out|
|
25
|
+
puts out if options[:log_file] && options[:log_stdout]
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
return @logger
|
30
|
+
end
|
31
|
+
|
32
|
+
class NullLogger
|
33
|
+
def formatter=; end
|
34
|
+
def level=; end
|
35
|
+
def debug; end
|
36
|
+
def info; end
|
37
|
+
def warn; end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/suture/wrap/sqlite.rb
CHANGED
@@ -43,7 +43,14 @@ module Suture::Wrap
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def self.select(db, table, where_clause, bind_params)
|
46
|
-
db.execute(
|
46
|
+
db.execute(
|
47
|
+
"select * from #{table} #{where_clause} order by id asc",
|
48
|
+
bind_params
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.delete(db, table, where_clause, bind_params)
|
53
|
+
db.execute("delete from #{table} #{where_clause}", bind_params)
|
47
54
|
end
|
48
55
|
end
|
49
56
|
end
|
data/lib/suture.rb
CHANGED
@@ -8,9 +8,16 @@ require "suture/prescribes_test_plan"
|
|
8
8
|
require "suture/tests_patient"
|
9
9
|
require "suture/interprets_results"
|
10
10
|
|
11
|
+
require "suture/adapter/log"
|
12
|
+
require "suture/comparator"
|
13
|
+
|
11
14
|
module Suture
|
12
15
|
DEFAULT_OPTIONS = {
|
13
|
-
:database_path => "db/suture.sqlite3"
|
16
|
+
:database_path => "db/suture.sqlite3",
|
17
|
+
:comparator => Comparator.new,
|
18
|
+
:log_level => "INFO",
|
19
|
+
:log_file => nil,
|
20
|
+
:log_stdout => true
|
14
21
|
}
|
15
22
|
|
16
23
|
def self.create(name, options)
|
@@ -23,7 +30,22 @@ module Suture
|
|
23
30
|
test_plan = Suture::PrescribesTestPlan.new.prescribe(name, options)
|
24
31
|
test_results = Suture::TestsPatient.new.test(test_plan)
|
25
32
|
if test_results.failed?
|
26
|
-
Suture::InterpretsResults.new.interpret(test_results)
|
33
|
+
Suture::InterpretsResults.new.interpret(test_plan, test_results)
|
27
34
|
end
|
28
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
|
29
51
|
end
|
data/suture.gemspec
CHANGED
@@ -13,16 +13,23 @@ 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|safe|spec|features)/}) }
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(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
|
+
spec.add_dependency "backports"
|
22
23
|
|
23
24
|
spec.add_development_dependency "bundler", "~> 1.12"
|
24
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
25
26
|
spec.add_development_dependency "pry", "~> 0.9.12.6"
|
26
27
|
spec.add_development_dependency "minitest", "~> 5.9"
|
27
28
|
spec.add_development_dependency "gimme", "~> 0.5"
|
29
|
+
spec.add_development_dependency "github_changelog_generator"
|
30
|
+
|
31
|
+
if Gem.ruby_version >= Gem::Version.new("1.9.3")
|
32
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
33
|
+
spec.add_development_dependency "simplecov", "~> 0.11.2" #<--only here to lock
|
34
|
+
end
|
28
35
|
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.3.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-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: backports
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +108,48 @@ dependencies:
|
|
94
108
|
- - "~>"
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '0.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: github_changelog_generator
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: codeclimate-test-reporter
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: simplecov
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.11.2
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.11.2
|
97
153
|
description: Provides tools to record calls to legacy code and verify new implementations
|
98
154
|
still work
|
99
155
|
email:
|
@@ -102,8 +158,10 @@ executables: []
|
|
102
158
|
extensions: []
|
103
159
|
extra_rdoc_files: []
|
104
160
|
files:
|
161
|
+
- ".codeclimate.yml"
|
105
162
|
- ".gitignore"
|
106
163
|
- ".projections.json"
|
164
|
+
- ".rubocop.yml"
|
107
165
|
- ".travis.yml"
|
108
166
|
- Gemfile
|
109
167
|
- LICENSE.txt
|
@@ -113,8 +171,10 @@ files:
|
|
113
171
|
- bin/setup
|
114
172
|
- lib/suture.rb
|
115
173
|
- lib/suture/adapter/dictaphone.rb
|
174
|
+
- lib/suture/adapter/log.rb
|
116
175
|
- lib/suture/builds_plan.rb
|
117
176
|
- lib/suture/chooses_surgeon.rb
|
177
|
+
- lib/suture/comparator.rb
|
118
178
|
- lib/suture/error/observation_conflict.rb
|
119
179
|
- lib/suture/error/schema_version.rb
|
120
180
|
- lib/suture/error/verification_failed.rb
|
@@ -125,11 +185,14 @@ files:
|
|
125
185
|
- lib/suture/surgeon/observer.rb
|
126
186
|
- lib/suture/tests_patient.rb
|
127
187
|
- lib/suture/util/env.rb
|
188
|
+
- lib/suture/util/shuffle.rb
|
189
|
+
- lib/suture/util/timer.rb
|
128
190
|
- lib/suture/value/observation.rb
|
129
191
|
- lib/suture/value/plan.rb
|
130
192
|
- lib/suture/value/test_plan.rb
|
131
193
|
- lib/suture/value/test_results.rb
|
132
194
|
- lib/suture/version.rb
|
195
|
+
- lib/suture/wrap/logger.rb
|
133
196
|
- lib/suture/wrap/sqlite.rb
|
134
197
|
- suture.gemspec
|
135
198
|
homepage: https://github.com/testdouble/suture
|