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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8719743f9e00506715c4d1ce0246d845499618c2
4
- data.tar.gz: 95298c758bedaf269b91f309658829cb44895f2c
3
+ metadata.gz: b9c19b3ad957fa4dee235e314fcacde3aeac88c3
4
+ data.tar.gz: de6d831eaaecf0ff8b7edde1397b7c1304a486c2
5
5
  SHA512:
6
- metadata.gz: 4854f0c0472d24c6e49d6be7131e66423d638538505ff2883dd797d6554f6e2a4b8a1fd9ed1ec4707fe90f8b3a13b51053e988e4d12efe824b2a82d3fcf7988f
7
- data.tar.gz: e4b83c187c95e674576f9bc230b7c18e89289917abd82536f2cf07b37ea0839cecfa2c6ad550ea22beeb28ce53ee0c03db2c556c2e3d36341642022169cf6513
6
+ metadata.gz: e35674ad68833373e8d926437c0c272f7f9b216c821b150aec70fc74250e29a188f698d277bdca8dcf48f67baa534bbdcd355652ff56b9d5f991c38802955608
7
+ data.tar.gz: 4fa715fc19bbb2911c3e028ab360cf557b49b87d564764c4cb4d64109362d5bc00683c294a9d3e6d2ecfc72de6ebbf879540363898e0e60ee41f52f063b90efe
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
  /tmp/
10
10
  /db/
11
11
  /log/
12
+ /ignore/
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 `:run_both` to `true`:
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
- run_both: true
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 `:fallback_to_old`
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
- fallback_to_old: true
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
- require 'github_changelog_generator/task'
30
- GitHubChangelogGenerator::RakeTask.new :changelog
31
- task :changelog_commit do
32
- require "suture"
33
- cmd = "git commit -m \"Changelog for #{Suture::VERSION}\" -- CHANGELOG.md"
34
- puts "-------> #{cmd}"
35
- system cmd
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
@@ -40,7 +40,7 @@ module Suture::Adapter
40
40
  rows.map { |row| row_to_observation(row) }
41
41
  end
42
42
 
43
- def delete(id)
43
+ def delete!(id)
44
44
  log_info("deleting call with ID: #{id}")
45
45
  Suture::Wrap::Sqlite.delete(@db, :observations, "where id = ?", [id])
46
46
  end
@@ -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
@@ -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,9 @@
1
+ require "suture/create/builds_plan"
2
+ require "suture/adapter/dictaphone"
3
+
4
+ module Suture
5
+ def self.delete!(id, options = {})
6
+ plan = BuildsPlan.new.build(:name_not_used_here, options)
7
+ Adapter::Dictaphone.new(plan).delete!(id)
8
+ end
9
+ 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
 
@@ -0,0 +1,9 @@
1
+ require "suture/adapter/log"
2
+
3
+ module Suture
4
+ def self.reset!
5
+ Suture.config_reset!
6
+ Adapter::Log.reset!
7
+ end
8
+ end
9
+
@@ -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
@@ -1,8 +1,10 @@
1
+ require "suture/util/scalpel"
2
+
1
3
  module Suture::Surgeon
2
4
  class NoOp
3
5
  def operate(plan)
4
6
  return unless plan.old
5
- plan.old.call(*plan.args)
7
+ Suture::Util::Scalpel.new.cut(plan, :old)
6
8
  end
7
9
  end
8
10
  end
@@ -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
- invoke(plan).tap do |result|
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
@@ -1,14 +1,27 @@
1
1
  module Suture::Value
2
2
  class Plan
3
- attr_reader :name, :old, :new, :args, :record_calls, :comparator, :database_path
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
- @database_path = attrs[:database_path]
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
- :comparator, :database_path
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, :database_path)
9
- assign_integral_ivars(attrs, :verify_only, :call_limit, :time_limit, :error_message_limit)
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
- Hash[attrs.select {|(k,_)| names.include?(k) }.
24
- map { |(k,v)| [k, v.nil? ? nil : v.to_i]}],
25
- *names
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)
@@ -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
- DEFAULT_TEST_OPTIONS = {
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(DEFAULT_TEST_OPTIONS.
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/timer"
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] = if observation.args
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
+
@@ -1,3 +1,3 @@
1
1
  module Suture
2
- VERSION = "0.3.3"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -30,11 +30,11 @@ module Suture::Wrap
30
30
  end
31
31
 
32
32
  class NullLogger
33
- def formatter=; end
34
- def level=; end
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/builds_plan"
4
- require "suture/chooses_surgeon"
5
- require "suture/performs_surgery"
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.3.3
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-23 00:00:00.000000000 Z
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: github_changelog_generator
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: codeclimate-test-reporter
126
+ name: simplecov
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
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: '0'
138
+ version: 0.11.2
139
139
  - !ruby/object:Gem::Dependency
140
- name: simplecov
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.11.2
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.11.2
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/interprets_results.rb
183
- - lib/suture/performs_surgery.rb
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/tests_patient.rb
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