suture 0.5.0 → 1.0.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: 9ca2d3a0376378b6ead8f55b0f92e1ef20b73991
4
- data.tar.gz: b506cf09899bfd1e372c088aaba8203a0f0c20aa
3
+ metadata.gz: 65850d2b933384f85c0cd13ec67fe92efcb08320
4
+ data.tar.gz: ba005e2f0e87b693f89948102559dfccfba7720b
5
5
  SHA512:
6
- metadata.gz: 30813362842d2a559181b248b08904187d96f78d6feac8b3662ef4a2e7dfa35a2cdda38f5b589184e531266574f4c1e7b23fb7fd68b749bd90d36f73f3a9d652
7
- data.tar.gz: 94f1c2b749cadc2a1dc80ac962bc7bfee7d735cea0eae1940a49c09543466f167e25faa809915ae8077e70f34d93590cd1e9bee1d32278be0cd0c77c47ee1122
6
+ metadata.gz: 7132a9cfa0499d89c786c7ccea1c7ed3da896ec786d3249b1aa3d00433eb0387d3be8917e3bb589f917f888624ab4bab71dc3c2f751a599a100422c09b717f2f
7
+ data.tar.gz: cd7f1fea872d8887a51f8b65d03cd5bff5189d9d4032141c0862c02265aec1c2acd4552e24e77e611fbb10e57ebd62796d2a956edf754c6dcde57df43438f0ff
data/.travis.yml CHANGED
@@ -1,7 +1,8 @@
1
1
  language: ruby
2
- cache: bundler
3
2
  sudo: false
4
- before_install: gem install bundler
3
+ before_install:
4
+ - gem install bundler
5
+ - cd example/rails_app && ./script/setup.sh && cd ../..
5
6
  rvm:
6
7
  - 1.8.7
7
8
  - 1.9
data/CHANGELOG.md CHANGED
@@ -2,7 +2,28 @@
2
2
 
3
3
  ## [Unreleased](https://github.com/testdouble/suture/tree/HEAD)
4
4
 
5
- [Full Changelog](https://github.com/testdouble/suture/compare/v0.3.3...HEAD)
5
+ [Full Changelog](https://github.com/testdouble/suture/compare/v0.4.0...HEAD)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - API to delete a seam's recordings from the database [\#32](https://github.com/testdouble/suture/issues/32)
10
+
11
+ **Closed issues:**
12
+
13
+ - Justification for building a custom Docker image instead of using the canonical Ruby one [\#57](https://github.com/testdouble/suture/issues/57)
14
+ - staging/prod: dup arguments [\#56](https://github.com/testdouble/suture/issues/56)
15
+ - Add progress bar [\#54](https://github.com/testdouble/suture/issues/54)
16
+ - warn if no calls are found for the provided seam name [\#51](https://github.com/testdouble/suture/issues/51)
17
+ - Log out an error every time a suture raises [\#50](https://github.com/testdouble/suture/issues/50)
18
+ - Validate test plans [\#28](https://github.com/testdouble/suture/issues/28)
19
+ - Support permitted error types [\#24](https://github.com/testdouble/suture/issues/24)
20
+
21
+ **Merged pull requests:**
22
+
23
+ - Remove an unnecessary space \[ci skip\] [\#52](https://github.com/testdouble/suture/pull/52) ([JuanitoFatas](https://github.com/JuanitoFatas))
24
+
25
+ ## [v0.4.0](https://github.com/testdouble/suture/tree/v0.4.0) (2016-08-29)
26
+ [Full Changelog](https://github.com/testdouble/suture/compare/v0.3.3...v0.4.0)
6
27
 
7
28
  **Closed issues:**
8
29
 
data/README.md CHANGED
@@ -262,7 +262,7 @@ unit tests around new units that shook out from the activity. Having good tests
262
262
  for well-factored code is the best guard against seeing it slip once again into
263
263
  poorly-understood "legacy" code.
264
264
 
265
- ## staging
265
+ ## Staging
266
266
 
267
267
  Once you've changed the code, you still may not be confident enough to delete it
268
268
  entirely. It's possible (even likely) that your local exploratory testing didn't
@@ -292,7 +292,7 @@ implementations, and will raise an error if they don't return the same value.
292
292
  Obviously, this setting is only helpful if the paths don't trigger major or
293
293
  destructive side effects.
294
294
 
295
- ## production
295
+ ## Production
296
296
 
297
297
  You're _almost_ ready to delete the old code path and switch production over to
298
298
  the new one, but fear lingers: maybe there's an edge case your testing to this
@@ -408,11 +408,155 @@ where you call Suture, like `Suture.create(:foo, { :comparator => my_thing })`
408
408
 
409
409
  #### Suture.create
410
410
 
411
- TODO
411
+ `Suture.create(name, [options hash])`
412
+
413
+ * _name_ (Required) - a unique name for the seam, by which any recordings will be
414
+ identified. This should match the name used for any calls to `Suture.verify` by
415
+ your automated tests
416
+
417
+ * _old_ - (Required) - something that responds to `call` for the provided `args`
418
+ of the seam and either is the legacy code path (e.g.
419
+ `OldCode.new.method(:old_path)`) or invokes it (inside an anonymous Proc or
420
+ lambda)
421
+
422
+ * _args_ - (Required) - an array of arguments to be passed to the `old` or `new`
423
+
424
+ * _new_ - like old, but either references or invokes the code path designed to
425
+ replace the `old` legacy code path. When set, Suture will default to invoking
426
+ the `new` path at the exclusion of the `old` path (unless a mode flag like
427
+ `record_calls`, `call_both`, or `fallback_on_error` suggests differently)
428
+
429
+ * _database_path_ - (Default: `"db/suture.sqlite3"`) - a path relative to the
430
+ current working directory to the Sqlite3 database Suture uses to record and
431
+ playback calls
432
+
433
+ * _record_calls_ - (Default: false) - when set to true, the `old` path is called
434
+ (regardless of whether `new` is set) and its arguments and result (be it a return
435
+ value or an expected raised error) is recorded into the Suture database for the
436
+ purpose of more coverage for calls to `Suture.verify`. [Read
437
+ more](#3-record-the-current-behavior)
438
+
439
+ * _call_both_ - (Default: false) - when set to true, the `new` path is invoked,
440
+ then the `old` path is invoked, each with the seam's `args`. The return value
441
+ from each is compared with the `comparator`, and if they are not equivalent, then
442
+ a `Suture::Error::ResultMismatch` is raised. Intended after the `new` path is
443
+ initially developed and to be run in pre-production environments. [Read
444
+ more](#staging)
445
+
446
+ * _fallback_on_error_ - (Default: false) - designed to be run in production after
447
+ the initial development of the new code path, when set to true, Suture will
448
+ invoke the `new` code path. If `new` raises an error that isn't an
449
+ `expected_error_type`, then Suture will invoke the `old` path with the same args
450
+ in an attempt to recover a working state for the user. [Read more](#production)
451
+
452
+ * _raise_on_result_mismatch_ - (Default: true) - when set to true, the
453
+ `call_both` mode will merely log incidents of result mismatches, as opposed to
454
+ raising `Suture::Error::ResultMismatch`.
455
+
456
+ * _comparator_ - (Default: `Suture::Comparator.new`) - determines how return
457
+ values from the Suture are compared when invoking `Suture.verify` or when
458
+ `call_both` mode is activated. By default, results will be considered equivalent
459
+ if `==` returns true or if they `Marshal.dump` to the same string. If this
460
+ default isn't appropriate for the return value of your seam, [read
461
+ on](#creating-a-custom-comparator)
462
+
463
+ * _expected_error_types_ - (Default: `[]`) - if the seam is expected to raise
464
+ certain types of errors, don't consider them to be exceptional cases. For
465
+ example, if your `:widget` seam is known to raise `WidgetError` objects in
466
+ certain cases, setting `:expected_error_types => [WidgetError]` will result in:
467
+ * `Suture.create` will record expected errors when `record_calls` is enabled
468
+ * `Suture.verify` will compare recorded and actual raised errors that are
469
+ `kind_of?` any recorded error type (regardless of whether `Suture.verify` is
470
+ passed a redundant list of `expected_error_types`)
471
+ * `Suture.create`, when `fallback_on_error` is enabled, will allow expected
472
+ errors raised by the `new` path to propogate, as opposed to logging &
473
+ rescuing them before calling the `old` path as a fallback
474
+ * Additionally, `Suture.verify` can be passed `expected_error_types` to squelch
475
+ warning logs that result from unexpectedly raised errors
476
+
477
+ * _disable_ - (Default: false) - when enabled, Suture will attempt to revert to
478
+ the original behavior of the `old` path and take no special action. Useful in
479
+ cases where a bug is discovered in a deployed environment and you simply want
480
+ to hit the brakes on any new code path experiments by setting
481
+ `SUTURE_DISABLE=true` globally
482
+
483
+ * _dup_args_ - (Default: false) - when enabled, Suture will call `dup` on each
484
+ of the args passed to the `old` and/or `new` code paths. Useful when the code
485
+ path(s) mutate the arguments in such a way as to prevent `call_both` or
486
+ `fallback_on_error` from being effective
487
+
488
+ * _after_new_ - a `call`-able hook that runs after `new` is invoked. If `new`
489
+ raises an error, it is not invoked
490
+
491
+ * _after_old_ - a `call`-able hook that runs after `old` is invoked. If `old`
492
+ raises an error, it is not invoked
493
+
494
+ * _on_new_error_ - a `call`-able hook that is invoked after `new` raises an
495
+ unexpected error (see `expected_error_types`).
496
+
497
+ * _on_old_error_ - a `call`-able hook that is invoked after `old` raises an
498
+ unexpected error (see `expected_error_types`).
412
499
 
413
500
  #### Suture.verify
414
501
 
415
- TODO
502
+ Many of the settings for `Suture.verify` are analogous to the same settings in
503
+ `Suture.create` and are generally expected to be configured in the same way, as
504
+ if symmetrically with the `Suture.create` call of the seam under test:
505
+
506
+ * _name_ - (Required) - should be the same name as a seam for which some number
507
+ of recorded calls exist
508
+
509
+ * _subject_ - (Required) - a `call`-able that will be invoked with each recorded
510
+ set of `args` and have its result compared to that of each recording. This is
511
+ used in lieu of `old` or `new`, since the subject of a `Suture.verify` test might
512
+ be either (or neither!)
513
+
514
+ * _verify_only_ - (Default: nil) - when set to an ID, Suture.verify` will only
515
+ run against recorded calls for the matching ID. This option is meant to be used
516
+ to focus work on resolving a single verification failure
517
+
518
+ * _fail_fast_ - (Default: false) - `Suture.verify` will, by default, run against
519
+ every single recording, aggregating and reporting on all errors (just like, say,
520
+ RSpec or Minitest would). However, if the seam is slow to invoke or if you
521
+ confidently expect all of the recordings to pass verification, `fail_fast` is an
522
+ appropriate option to set.
523
+
524
+ * _call_limit_ - (Default: nil) - when set to a number, Suture will only verify
525
+ up to the set number of recorded calls. Because Suture randomizes the order of
526
+ verifications by default, you can see this as setting Suture.verify to sample a
527
+ random smattering of `call_limit` recordings as a smell test. Potentially useful
528
+ when a seam is very slow
529
+
530
+ * _time_limit_ - (Default: nil) - when set to a number (in seconds), Suture will
531
+ stop running verifications against recordings once `time_limit` seconds has
532
+ elapsed. Useful when a seam is very slow to invoke
533
+
534
+ * _error_message_limit_ - (Default: nil) - when set to a number, Suture will only
535
+ print up to `error_message_limit` failure messages. That way, if you currently
536
+ have hundreds of verifications failing, your console isn't overwhelmed by them on
537
+ each run of `Suture.verify`
538
+
539
+ * _random_seed_ - (Default: it's random!) - a randomized seed used to shuffle
540
+ the recordings before verifying them against the `subject` code path. If set to
541
+ `nil`, the recordings will be invoked in insertion-order. If set to a specific
542
+ number, that number will be used as the random seed (useful when re-running a
543
+ particular verification failure that can't be reproduced otherwise)
544
+
545
+ * _comparator_ - (Default: `Suture::Comparator`) - If a custom comparator is used
546
+ by the seam in `Suture.create`, then the same comparator should probably be
547
+ used by `Suture.verify` to ensure the results are comparable. [Read
548
+ more](#creating-a-custom-comparator) on creating custom comparators
549
+ )
550
+
551
+ * _database_path_ - (Default: `"db/suture.sqlite3"`) - as with `Suture.create`, a
552
+ custom database path can be set for almost any invocation of Suture, and
553
+ `Suture.verify is no exception`
554
+
555
+ * _after_subject_ - a `call`-able hook that runs after `subject` is invoked. If
556
+ `subject` raises an error, it is not invoked
557
+
558
+ * _on_new_subject_ - a `call`-able hook that is invoked after `subject` raises an
559
+ unexpected error (see `expected_error_types`)
416
560
 
417
561
  ### Creating a custom comparator
418
562
 
@@ -472,6 +616,50 @@ Suture.verify(:my_type, {
472
616
  })
473
617
  ```
474
618
 
619
+ #### Comparing two ActiveRecord objects
620
+
621
+ Let's face it, a massive proportion of legacy Ruby code in the wild involves
622
+ ActiveRecord objects to some extent, and it's important that Suture be equipped
623
+ to compare them gracefully. If Suture's default comparator (`Suture::Comparator`)
624
+ detects two ActiveRecord model instances being compared, it will behave
625
+ differently, by this logic:
626
+
627
+ 1. Instead of comparing the objects with `==` (which returns true so long as the
628
+ `id` attribute matdhes), Suture will compare the objects' `attributes` hashes
629
+ instead
630
+ 2. The built-in `updated_at` and `created_at` will typically differ when code
631
+ is executed at different times and are usually not meaningful to application
632
+ logic, Suture will ignore these attributes by default
633
+
634
+ Other attributes may or may not matter (for instance, other timestamp fields,
635
+ or the `id` of the object), in those cases, you can instantiate the comparator
636
+ yourself and tell it which attributes to exclude, like so:
637
+
638
+ ``` ruby
639
+ Suture.verify :thing,
640
+ :subject => Thing.new.method(:stuff),
641
+ :comparator => Suture::Comparator.new(
642
+ :active_record_excluded_attributes => [
643
+ :id,
644
+ :quality,
645
+ :created_at,
646
+ :updated_at
647
+ ]
648
+ )
649
+ ```
650
+
651
+ If `Thing#stuff` returns an instance of an ActiveRecord model, the four
652
+ attributes listed above will be ignored when comparing with recorded results.
653
+
654
+ In all of the above cases, `:comparator` can be set on both `Suture.create` and
655
+ `Suture.verify` and typically ought to be symmetrical for most seams.
656
+
657
+ ## Examples
658
+
659
+ This repository contains these examples available for your perusal:
660
+
661
+ * [A Rails app of the Gilded Rose kata](example/rails_app)
662
+
475
663
  ## Troubleshooting
476
664
 
477
665
  Some ideas if you can't get a particular verification to work or if you keep
data/Rakefile CHANGED
@@ -26,6 +26,18 @@ Rake::TestTask.new(:test) do |t|
26
26
  ]
27
27
  end
28
28
 
29
+ task :example do
30
+ Dir.chdir("example/rails_app") do
31
+ passed = system <<-SH
32
+ BUNDLE_GEMFILE="$PWD/Gemfile" bundle install --quiet
33
+ BUNDLE_GEMFILE="$PWD/Gemfile" bundle exec rake suture
34
+ SH
35
+ if !passed
36
+ raise StandardError.new("Rails example failed!")
37
+ end
38
+ end
39
+ end
40
+
29
41
  if Gem.ruby_version >= Gem::Version.new("2.2.2")
30
42
  require 'github_changelog_generator/task'
31
43
  GitHubChangelogGenerator::RakeTask.new :changelog
@@ -39,4 +51,4 @@ if Gem.ruby_version >= Gem::Version.new("2.2.2")
39
51
  end
40
52
 
41
53
 
42
- task :default => :test
54
+ task :default => [:test, :example]
@@ -4,7 +4,7 @@ require "suture/util/env"
4
4
  module Suture::Adapter
5
5
  module Log
6
6
  def self.logger
7
- if !@setup
7
+ if !defined?(@setup) || !@setup
8
8
  @logger = Suture::Wrap::Logger.init(Suture.config.merge(Suture::Util::Env.to_map))
9
9
  @setup = true
10
10
  end
@@ -1,7 +1,53 @@
1
1
  module Suture
2
2
  class Comparator
3
+ DEFAULT_ACTIVE_RECORD_EXCLUDED_ATTRIBUTES = [:updated_at, :created_at]
4
+
5
+ def initialize(options = {})
6
+ @options = {
7
+ :active_record_excluded_attributes => (
8
+ options[:active_record_excluded_attributes] ||
9
+ DEFAULT_ACTIVE_RECORD_EXCLUDED_ATTRIBUTES
10
+ ).map(&:to_s)
11
+ }
12
+ end
13
+
3
14
  def call(recorded, actual)
4
- recorded == actual || Marshal.dump(recorded) == Marshal.dump(actual)
15
+ is_equalivalent?(recorded, actual) ||
16
+ Marshal.dump(recorded) == Marshal.dump(actual)
17
+ end
18
+
19
+ def inspect
20
+ "#{self.class}.new(#{@options.inspect})"
21
+ end
22
+
23
+ protected
24
+
25
+ def compare_active_record(recorded, actual)
26
+ actual.kind_of?(recorded.class) &&
27
+ without_excluded_attrs(recorded.attributes) ==
28
+ without_excluded_attrs(actual.attributes)
29
+ end
30
+
31
+ private
32
+
33
+ def without_excluded_attrs(hash)
34
+ hash.reject do |k, v|
35
+ @options[:active_record_excluded_attributes].include?(k.to_s)
36
+ end
37
+ end
38
+
39
+ def is_equalivalent?(recorded, actual)
40
+ if is_active_record?(recorded, actual)
41
+ compare_active_record(recorded, actual)
42
+ else
43
+ recorded == actual
44
+ end
45
+ end
46
+
47
+ def is_active_record?(recorded, actual)
48
+ defined?(ActiveRecord::Base) &&
49
+ recorded.kind_of?(ActiveRecord::Base) &&
50
+ actual.kind_of?(ActiveRecord::Base)
5
51
  end
6
52
  end
7
53
  end
@@ -0,0 +1,7 @@
1
+ module Suture::Error
2
+ class InvalidTestPlan < StandardError
3
+ def message
4
+ "Suture.verify requires a `:subject` that responds to `:call`."
5
+ end
6
+ end
7
+ end
@@ -7,7 +7,7 @@ module Suture::Error
7
7
  end
8
8
 
9
9
  def message
10
- expected_message = <<-MSG.gsub(/^ {8}/,'')
10
+ <<-MSG.gsub(/^ {8}/,'')
11
11
  The results from the old & new code paths did not match for the seam
12
12
  #{@plan.name.inspect} and Suture is raising this error because the `:call_both`
13
13
  option is enabled, because both code paths are expected to return the
@@ -52,7 +52,6 @@ module Suture::Error
52
52
  too disruptive and logging is sufficient for monitoring results, you may
53
53
  disable this error by setting `:raise_on_result_mismatch` to false.
54
54
  MSG
55
-
56
55
  end
57
56
  end
58
57
  end
@@ -1,4 +1,5 @@
1
1
  require "suture/adapter/progress_bar"
2
+ require "suture/util/numbers"
2
3
 
3
4
  module Suture::Error
4
5
  class VerificationFailed < StandardError
@@ -47,6 +48,7 @@ module Suture::Error
47
48
  results.passed.size,
48
49
  results.all.size
49
50
  )
51
+ percent = Suture::Util::Numbers.percent(results.passed.size, results.all.size)
50
52
  <<-MSG.gsub(/^ {8}/,'')
51
53
  ## Progress
52
54
 
@@ -54,7 +56,7 @@ module Suture::Error
54
56
 
55
57
  #{bar}
56
58
 
57
- Of #{results.all.size} recorded interactions, #{results.passed.size} are currently passing.
59
+ Of #{results.all.size} recorded interactions, #{results.passed.size} are currently passing. That's #{percent}%!
58
60
  MSG
59
61
  end
60
62
 
@@ -66,13 +68,13 @@ module Suture::Error
66
68
 
67
69
  ```
68
70
  {
69
- :comparator => #{describe_comparator(plan.comparator)}
70
71
  :database_path => #{plan.database_path.inspect},
71
72
  :fail_fast => #{plan.fail_fast},
72
73
  :call_limit => #{plan.call_limit.inspect},#{" # (no limit)" if plan.call_limit.nil?}
73
74
  :time_limit => #{plan.time_limit.inspect},#{plan.time_limit.nil? ? " # (no limit)" : " # (in seconds)"}
74
75
  :error_message_limit => #{plan.error_message_limit.inspect},#{" # (no limit)" if plan.error_message_limit.nil?}
75
- :random_seed => #{plan.random_seed.inspect}#{" # (insertion order)" if plan.random_seed.nil?}
76
+ :random_seed => #{plan.random_seed.inspect},#{" # (insertion order)" if plan.random_seed.nil?}
77
+ :comparator => #{describe_comparator(plan.comparator)}
76
78
  }
77
79
  ```
78
80
  MSG
@@ -80,9 +82,9 @@ module Suture::Error
80
82
 
81
83
  def describe_comparator(comparator)
82
84
  if comparator.kind_of?(Proc)
83
- "Proc, # (in: `#{describe_source_location(*comparator.source_location)}`)"
85
+ "Proc # (in: `#{describe_source_location(*comparator.source_location)}`)"
84
86
  elsif comparator.respond_to?(:method) && comparator.method(:call)
85
- "#{comparator.class}.new, # (in: `#{describe_source_location(*comparator.method(:call).source_location)}`)"
87
+ "#{comparator.inspect}.new, # (in: `#{describe_source_location(*comparator.method(:call).source_location)}`)"
86
88
  end
87
89
  end
88
90
 
@@ -0,0 +1,7 @@
1
+ module Suture::Util
2
+ module Numbers
3
+ def self.percent(x, out_of_y)
4
+ ((x.to_f / out_of_y.to_f) * 100).round
5
+ end
6
+ end
7
+ end
@@ -22,6 +22,8 @@ module Suture::Value
22
22
  other.kind_of?(self.class) && other.state == state
23
23
  end
24
24
 
25
+ alias_method :eql?, :==
26
+
25
27
  def hash
26
28
  state.hash
27
29
  end
@@ -4,6 +4,7 @@ require "suture/adapter/dictaphone"
4
4
  require "suture/value/test_results"
5
5
  require "suture/util/shuffle"
6
6
  require "suture/util/timer"
7
+ require "suture/error/invalid_test_plan"
7
8
  require "backports/1.9.2/random"
8
9
 
9
10
  module Suture
@@ -15,6 +16,7 @@ module Suture
15
16
  end
16
17
 
17
18
  def test(test_plan)
19
+ validate_test_plan!(test_plan)
18
20
  experienced_failure_in_life = false
19
21
  timer = Suture::Util::Timer.new(test_plan.time_limit) unless test_plan.time_limit.nil?
20
22
  test_cases = build_test_cases(test_plan)
@@ -35,6 +37,12 @@ module Suture
35
37
 
36
38
  private
37
39
 
40
+ def validate_test_plan!(test_plan)
41
+ if !test_plan.subject || !test_plan.subject.respond_to?(:call)
42
+ raise Suture::Error::InvalidTestPlan.new
43
+ end
44
+ end
45
+
38
46
  def should_skip?(test_plan, failed_fast, call_count, timer)
39
47
  (test_plan.fail_fast && failed_fast) ||
40
48
  (test_plan.call_limit && call_count >= test_plan.call_limit) ||
@@ -1,3 +1,3 @@
1
1
  module Suture
2
- VERSION = "0.5.0"
2
+ VERSION = "1.0.0"
3
3
  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.5.0
4
+ version: 1.0.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-09-05 00:00:00.000000000 Z
11
+ date: 2016-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3
@@ -197,6 +197,7 @@ files:
197
197
  - lib/suture/create/validates_plan.rb
198
198
  - lib/suture/delete.rb
199
199
  - lib/suture/error/invalid_plan.rb
200
+ - lib/suture/error/invalid_test_plan.rb
200
201
  - lib/suture/error/observation_conflict.rb
201
202
  - lib/suture/error/result_mismatch.rb
202
203
  - lib/suture/error/schema_version.rb
@@ -208,6 +209,7 @@ files:
208
209
  - lib/suture/surgeon/remediator.rb
209
210
  - lib/suture/util/compares_results.rb
210
211
  - lib/suture/util/env.rb
212
+ - lib/suture/util/numbers.rb
211
213
  - lib/suture/util/scalpel.rb
212
214
  - lib/suture/util/shuffle.rb
213
215
  - lib/suture/util/timer.rb