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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b9c19b3ad957fa4dee235e314fcacde3aeac88c3
4
- data.tar.gz: de6d831eaaecf0ff8b7edde1397b7c1304a486c2
3
+ metadata.gz: 9ca2d3a0376378b6ead8f55b0f92e1ef20b73991
4
+ data.tar.gz: b506cf09899bfd1e372c088aaba8203a0f0c20aa
5
5
  SHA512:
6
- metadata.gz: e35674ad68833373e8d926437c0c272f7f9b216c821b150aec70fc74250e29a188f698d277bdca8dcf48f67baa534bbdcd355652ff56b9d5f991c38802955608
7
- data.tar.gz: 4fa715fc19bbb2911c3e028ab360cf557b49b87d564764c4cb4d64109362d5bc00683c294a9d3e6d2ecfc72de6ebbf879540363898e0e60ee41f52f063b90efe
6
+ metadata.gz: 30813362842d2a559181b248b08904187d96f78d6feac8b3662ef4a2e7dfa35a2cdda38f5b589184e531266574f4c1e7b23fb7fd68b749bd90d36f73f3a9d652
7
+ data.tar.gz: 94f1c2b749cadc2a1dc80ac962bc7bfee7d735cea0eae1940a49c09543466f167e25faa809915ae8077e70f34d93590cd1e9bee1d32278be0cd0c77c47ee1122
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
  /db/
11
11
  /log/
12
12
  /ignore/
13
+ .ruby-version
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 `:call_old_on_error`
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
- call_old_on_error: true
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(:test) do |t|
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(:everything) do |t|
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 => :everything
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
- @comparator = plan.comparator
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
- def record(result)
21
- Suture::Wrap::Sqlite.insert(@db, :observations, [:name, :args, :result],
22
- [@name.to_s, @args_dump, Marshal.dump(result)])
23
- log_info("recorded call for seam #{@name.inspect} with args `#{@args_inspect}` and result `#{result.inspect}`")
24
- rescue SQLite3::ConstraintException
25
- old_observation = known_observation
26
- if @comparator.call(old_observation.result, result)
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 delete!(id)
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
 
@@ -27,5 +27,9 @@ module Suture::Adapter
27
27
  def log_warn(*args, &blk)
28
28
  Log.logger.warn(*args, &blk)
29
29
  end
30
+
31
+ def log_error(*args, &blk)
32
+ Log.logger.error(*args, &blk)
33
+ end
30
34
  end
31
35
  end
@@ -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
@@ -5,8 +5,9 @@ module Suture
5
5
  :database_path => "db/suture.sqlite3",
6
6
  :comparator => Comparator.new,
7
7
  :log_level => "INFO",
8
- :log_file => nil,
9
8
  :log_stdout => true,
9
+ :log_io => nil,
10
+ :log_file => nil,
10
11
  :raise_on_result_mismatch => true
11
12
  }
12
13
 
@@ -21,7 +21,7 @@ module Suture
21
21
  Surgeon::Observer.new
22
22
  elsif plan.call_both
23
23
  Surgeon::Auditor.new
24
- elsif plan.call_old_on_error
24
+ elsif plan.fallback_on_error
25
25
  Surgeon::Remediator.new
26
26
  else
27
27
  Surgeon::NoOp.new
@@ -35,13 +35,13 @@ module Suture
35
35
  end
36
36
  },
37
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."
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.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"
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.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."
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).delete!(id)
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
- ].join("\n")
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
- Arguments: ```
92
- #{expected.args.inspect}
93
- ```
94
- Expected result: ```
95
- #{expected.result.inspect}
96
- ```
97
- #{failure[:error] ? "Error raised" : "Actual result"}: ```
98
- #{if failure[:error]
99
- stringify_error(failure[:error])
100
- else
101
- failure[:new_result].inspect
102
- end
103
- }
104
- ```
105
-
106
- Ideas to fix this:
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})`
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
 
@@ -3,8 +3,19 @@ require "suture/util/scalpel"
3
3
  module Suture::Surgeon
4
4
  class NoOp
5
5
  def operate(plan)
6
- return unless plan.old
7
- Suture::Util::Scalpel.new.cut(plan, :old)
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
- Suture::Util::Scalpel.new.cut(plan, :old).tap do |result|
9
- dictaphone.record(result)
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
@@ -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 = args(plan, args_override)
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(plan.name, args, result)
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.expected_error_types.any? {|e| error.kind_of?(e) }
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 args(plan, args_override)
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, :result
4
- def initialize(id, name, args, result)
5
- @id = id
6
- @name = name
7
- @args = args
8
- @result = result
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
@@ -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, :call_old_on_error,
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
- @call_old_on_error = !!attrs[:call_old_on_error]
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, :fail_fast, :comparator,
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
- @scalpel = Suture::Util::Scalpel.new
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.fail_fast && experienced_failure_in_life) ||
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
- invoke(test_plan, observation).merge({
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
 
@@ -1,3 +1,3 @@
1
1
  module Suture
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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
- @logger = ::Logger.new(full_path)
10
- elsif options[:log_stdout]
11
- @logger = ::Logger.new(STDOUT)
9
+ ::Logger.new(full_path)
12
10
  else
13
- @logger = NullLogger.new
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[:log_file] && options[:log_stdout]
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 NullLogger
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
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
@@ -4,18 +4,18 @@ require "suture/error/schema_version"
4
4
 
5
5
  module Suture::Wrap
6
6
  module Sqlite
7
- SCHEMA_VERSION=1
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 schema_info (
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 schema_info values (?)", [SCHEMA_VERSION])
18
- actual_schema_version = db.execute("select * from schema_info").first[0]
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.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-08-29 00:00:00.000000000 Z
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