suture 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  sudo: false
3
4
  before_install: gem install bundler
4
5
  rvm:
@@ -8,3 +9,6 @@ rvm:
8
9
  - 2.1
9
10
  - 2.2
10
11
  - 2.3.1
12
+ addons:
13
+ code_climate:
14
+ repo_token: 05e3e31164d59aa626b730b92eb9b7418326dbf23420a4b87eab2555840b39ef
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Suture
2
2
 
3
- [![Build Status](https://travis-ci.org/testdouble/suture.svg?branch=master)](https://travis-ci.org/testdouble/suture)
3
+ [![Build Status](https://travis-ci.org/testdouble/suture.svg?branch=master)](https://travis-ci.org/testdouble/suture) [![Code Climate](https://codeclimate.com/github/testdouble/suture/badges/gpa.svg)](https://codeclimate.com/github/testdouble/suture) [![Test Coverage](https://codeclimate.com/github/testdouble/suture/badges/coverage.svg)](https://codeclimate.com/github/testdouble/suture/coverage)
4
4
 
5
5
  A refactoring tool for Ruby, designed to make it safe to change code you don't
6
6
  confidently understand. In fact, changing untrustworthy code is so fraught,
@@ -97,7 +97,7 @@ implementations behave the same way.
97
97
  First, we tell Suture to start recording calls by setting the environment
98
98
  variable `SUTURE_RECORD_CALLS` to something truthy (e.g.
99
99
  `SUTURE_RECORD_CALLS=true bundle exec rails s`). So long as this variable is set,
100
- any calls to our suture will record the arguments passed to the legacy code path
100
+ any calls to our seam will record the arguments passed to the legacy code path
101
101
  and the return value.
102
102
 
103
103
  As you use the application (whether it's a queue system, a web app, or a CLI),
@@ -304,8 +304,29 @@ you're yet unsure that your refactor or reimplementation is complete.
304
304
 
305
305
  While your application's logs aren't affected by Suture, it may be helpful for
306
306
  Suture to maintain a separate log file for any errors that are raised by the
307
- refactored code path. Setting the key `:log` to a path will prompt Suture to
308
- append any errors to a log at that location.
307
+ refactored code path.
308
+
309
+ Suture has a handful of process-wide logging settings that can be set at any
310
+ point as your app starts up (if you're using Rails, then your
311
+ environment-specific (e.g. `config/environments/production.rb`) config file
312
+ is a good choice).
313
+
314
+ ``` ruby
315
+ Suture.config({
316
+ :log_level => "WARN", #<-- defaults to "INFO"
317
+ :log_stdout => false, #<-- defaults to true
318
+ :log_file => "log/suture.log" #<-- defaults to nil
319
+ })
320
+ ```
321
+
322
+ When your new code path raises an error with the above settings, it will
323
+ propogate and log the error to the specified file.
324
+
325
+ ### Custom error handlers
326
+
327
+ Additionally, you may have some idea of what you want to do (i.e. phone home to
328
+ a reporting service) in the event that your new code path fails. To add custom
329
+ error handling before, set the `:on_error` option to a callable.
309
330
 
310
331
  ``` ruby
311
332
  class MyWorker
@@ -314,7 +335,7 @@ class MyWorker
314
335
  old: LegacyWorker.new,
315
336
  new: NewWorker.new,
316
337
  args: [id],
317
- log: 'log/my_worker_seam.log'
338
+ on_error: -> (name, args) { PhonesHome.new.phone(name, args) }
318
339
  }))
319
340
  end
320
341
  end
@@ -323,9 +344,9 @@ end
323
344
  ### Retrying failures
324
345
 
325
346
  Since the legacy code path hasn't been deleted yet, there's no reason to leave
326
- users hanging if the new code path explodes. By setting the `:retry` entry to
327
- `true`, Suture will rescue any errors raised from the new code path and attempt
328
- to invoke the legacy code path instead.
347
+ users hanging if the new code path explodes. By setting the `:fallback_to_old`
348
+ entry to `true`, Suture will rescue any errors raised from the new code path and
349
+ attempt to invoke the legacy code path instead.
329
350
 
330
351
  ``` ruby
331
352
  class MyWorker
@@ -334,7 +355,7 @@ class MyWorker
334
355
  old: LegacyWorker.new,
335
356
  new: NewWorker.new,
336
357
  args: [id],
337
- retry: true
358
+ fallback_to_old: true
338
359
  }))
339
360
  end
340
361
  end
@@ -344,3 +365,116 @@ Since this approach rescues errors, it's possible that errors in the new code
344
365
  path will go unnoticed, so it's best used in conjunction with Suture's logging
345
366
  feature. Before ultimately deciding to finally delete the legacy code path,
346
367
  double-check that the logs aren't full of rescued errors!
368
+
369
+ ## Configuration
370
+
371
+ Legacy code is, necessarily, complex and hard-to-wrangle. That's why Suture comes
372
+ with a bunch of configuration options to modify its behavior, particularly for
373
+ hard-to-compare objects.
374
+
375
+ ### Setting configuration options
376
+
377
+ In general, most configuration options can be set in several places:
378
+
379
+ * Globally, via an environment variable. The flag `record_calls` will translate
380
+ to an expected ENV var named `SUTURE_RECORD_CALLS` and can be set from the
381
+ command line like so: `SUTURE_RECORD_CALLS=true bundle exec rails server`, to
382
+ tell Suture to record all your interactions with your seams without touching the
383
+ source code.
384
+
385
+ * Globally, via the top-level `Suture.config` method. Most variables can be set
386
+ via this top-level configuration, like
387
+ `Suture.config(:database_path => 'my.db')`. Once set, this will apply to all your
388
+ interactions with Suture for the life of the process until you call
389
+ `Suture.reset!`.
390
+
391
+ * At a `Suture.create` or `Suture.verify` call-site as part of its options hash.
392
+ If you have several seams, you'll probably want to set most options locally
393
+ where you call Suture, like `Suture.create(:foo, { :comparator => my_thing })`
394
+
395
+ ### Supported options
396
+
397
+ #### Suture.create
398
+
399
+ TODO
400
+
401
+ #### Suture.verify
402
+
403
+ TODO
404
+
405
+ ### Creating a custom comparator
406
+
407
+ Out-of-the-box, Suture will do its best to compare your recorded & actual results
408
+ to ensure that things are equivalent to one another, but reality is often less
409
+ tidy than a gem can predict up-front. When the built-in equivalency comparator
410
+ fails you, you can define a custom one—globally or at each `Suture.create` or
411
+ `Suture.verify` call-site.
412
+
413
+ #### Extending the built-in comparator class
414
+
415
+ If you have a bunch of value types that require special equivalency checks, it
416
+ makes sense to invest the time to extend built-in one:
417
+
418
+ ``` ruby
419
+ class MyComparator < Suture::Comparator
420
+ def call(recorded, actual)
421
+ if recorded.kind_of?(MyType)
422
+ recorded.data_stuff == actual.data_stuff
423
+ else
424
+ super
425
+ end
426
+ end
427
+ end
428
+ ```
429
+
430
+ So long as you return `super` for non-special cases, it should be safe to set an
431
+ instance of your custom comparator globally for the life of the process with:
432
+
433
+ ``` ruby
434
+ Suture.config({
435
+ :comparator => MyComparator.new
436
+ })
437
+ ```
438
+
439
+ #### Creating a one-off comparator
440
+
441
+ If a particular seam requires a custom comparator and will always return
442
+ sufficiently homogeneous types, it may be good enough to set a custom comparator
443
+ inline at the `Suture.create` or `Suture.verify` call-site, like so:
444
+
445
+ ``` ruby
446
+ Suture.create(:my_type, {
447
+ :old => method(:old_method),
448
+ :args => [42],
449
+ :comparator => ->(recorded, actual){ recorded.data_thing == actual.data_thing }
450
+ })
451
+ ```
452
+
453
+ Just be sure to set it the same way if you want `Suture.verify` to be able to
454
+ test your recorded values!
455
+
456
+ ``` ruby
457
+ Suture.verify(:my_type, {
458
+ :subject => method(:old_method),
459
+ :comparator => ->(recorded, actual){ recorded.data_thing == actual.data_thing }
460
+ })
461
+ ```
462
+
463
+ ## Troubleshooting
464
+
465
+ Some ideas if you can't get a particular verification to work or if you keep
466
+ seeing false negatives:
467
+
468
+ * There may be a side effect in your code that you haven't found, extracted,
469
+ replicated, or controlled for. Consider contributing to [this
470
+ milestone](https://github.com/testdouble/suture/milestone/3), which specifies
471
+ a side-effect detector to be paired with Suture to make it easier to see
472
+ when observable database, network, and in-memory changes are made during a
473
+ Suture operation
474
+ * Consider writing a [custom comparator](#creating-a-custom-comparator) with
475
+ a relaxed conception of equivalence between the recorded and observed results
476
+ * If a recording was made in error, you can always delete it, either by
477
+ dropping Suture's database (which is, by default, stored in
478
+ `db/suture.sqlite3`) or by observing the ID of the recording from an error
479
+ message and invoking `Suture.delete(42)`
480
+
data/Rakefile CHANGED
@@ -13,4 +13,25 @@ Rake::TestTask.new(:safe) do |t|
13
13
  t.test_files = FileList['safe/helper.rb', 'safe/**/*_test.rb']
14
14
  end
15
15
 
16
- task :default => [:test, :safe]
16
+ Rake::TestTask.new(:everything) do |t|
17
+ t.libs << "test"
18
+ t.libs << "safe"
19
+ t.libs << "lib"
20
+ t.test_files = FileList[
21
+ 'safe/support/code_climate',
22
+ 'test/helper.rb',
23
+ 'test/**/*_test.rb',
24
+ 'safe/helper.rb',
25
+ 'safe/**/*_test.rb'
26
+ ]
27
+ end
28
+
29
+ require 'github_changelog_generator/task'
30
+
31
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
32
+ config.since_tag = '0.1.14'
33
+ config.future_release = '0.2.0'
34
+ end
35
+ task :release => :changelog
36
+
37
+ task :default => :everything
@@ -1,50 +1,68 @@
1
1
  require "suture/wrap/sqlite"
2
+ require "suture/adapter/log"
2
3
  require "suture/value/observation"
3
4
  require "suture/error/observation_conflict"
4
5
 
5
6
  module Suture::Adapter
6
7
  class Dictaphone
8
+ include Suture::Adapter::Log
9
+
7
10
  def initialize(plan)
8
11
  @db = Suture::Wrap::Sqlite.init(plan.database_path)
9
12
  @name = plan.name
10
- @args = plan.args
13
+ @comparator = plan.comparator
14
+ if plan.respond_to?(:args) # does not apply to TestPlan objects
15
+ @args_inspect = plan.args.inspect
16
+ @args_dump = Marshal.dump(plan.args)
17
+ end
11
18
  end
12
19
 
13
20
  def record(result)
14
21
  Suture::Wrap::Sqlite.insert(@db, :observations, [:name, :args, :result],
15
- [@name.to_s, Marshal.dump(@args), Marshal.dump(result)])
16
-
17
- rescue SQLite3::ConstraintException => e
18
- old_result = result_for(@name, @args)
19
- if old_result != result # TODO - use comparator
20
- raise Suture::Error::ObservationConflict.new(@name, @args, result, old_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}`")
21
28
  else
22
- # We're all good here, it was just a duplicative observation. No harm.
29
+ raise Suture::Error::ObservationConflict.new(@name, @args_inspect, result, old_observation)
23
30
  end
24
31
  end
25
32
 
26
- def play
27
- rows = Suture::Wrap::Sqlite.select(@db, :observations, "where name = ?", [@name.to_s])
28
- rows.map do |row|
29
- Suture::Value::Observation.new(
30
- row[0],
31
- row[1].to_sym,
32
- Marshal.load(row[2]),
33
- Marshal.load(row[3])
34
- )
35
- end
33
+ def play(only_id = nil)
34
+ rows = Suture::Wrap::Sqlite.select(
35
+ @db, :observations,
36
+ "where name = ? #{"and id = ?" if only_id}",
37
+ [@name.to_s, only_id].compact
38
+ )
39
+ log_debug("found #{rows.size} recorded calls for seam #{@name.inspect}.")
40
+ rows.map { |row| row_to_observation(row) }
41
+ end
42
+
43
+ def delete(id)
44
+ log_info("deleting call with ID: #{id}")
45
+ Suture::Wrap::Sqlite.delete(@db, :observations, "where id = ?", [id])
36
46
  end
37
47
 
38
48
  private
39
49
 
40
- def result_for(name, args)
41
- rows = Suture::Wrap::Sqlite.select(
50
+ def row_to_observation(row)
51
+ Suture::Value::Observation.new(
52
+ row[0],
53
+ row[1].to_sym,
54
+ Marshal.load(row[2]),
55
+ Marshal.load(row[3])
56
+ )
57
+ end
58
+
59
+ def known_observation
60
+ row_to_observation(Suture::Wrap::Sqlite.select(
42
61
  @db,
43
62
  :observations,
44
63
  "where name = ? and args = ?",
45
- [name.to_s, Marshal.dump(args)]
46
- )
47
- Marshal.load(rows.first[3])
64
+ [@name.to_s, @args_dump]
65
+ ).first)
48
66
  end
49
67
  end
50
68
  end
@@ -0,0 +1,31 @@
1
+ require "suture/wrap/logger"
2
+ require "suture/util/env"
3
+
4
+ module Suture::Adapter
5
+ module Log
6
+ def self.logger
7
+ if !@setup
8
+ @logger = Suture::Wrap::Logger.init(Suture.config.merge(Suture::Util::Env.to_map))
9
+ @setup = true
10
+ end
11
+ @logger
12
+ end
13
+
14
+ def self.reset!
15
+ @setup = nil
16
+ @logger = nil
17
+ end
18
+
19
+ def log_debug(*args, &blk)
20
+ Log.logger.debug(*args, &blk)
21
+ end
22
+
23
+ def log_info(*args, &blk)
24
+ Log.logger.info(*args, &blk)
25
+ end
26
+
27
+ def log_warn(*args, &blk)
28
+ Log.logger.warn(*args, &blk)
29
+ end
30
+ end
31
+ end
@@ -3,11 +3,11 @@ require "suture/util/env"
3
3
 
4
4
  module Suture
5
5
  class BuildsPlan
6
- UN_ENV_IABLE_OPTIONS = [:name, :old, :new, :args]
6
+ UN_ENV_IABLE_OPTIONS = [:name, :old, :new, :args, :comparator]
7
7
 
8
8
  def build(name, options = {})
9
9
  Value::Plan.new(
10
- DEFAULT_OPTIONS.
10
+ Suture.config.
11
11
  merge(options).
12
12
  merge(:name => name).
13
13
  merge(Suture::Util::Env.to_map(UN_ENV_IABLE_OPTIONS))
@@ -1,10 +1,20 @@
1
+ require "suture/adapter/log"
1
2
  require "suture/surgeon/observer"
2
3
  require "suture/surgeon/no_op"
3
4
 
4
5
  module Suture
5
6
  class ChoosesSurgeon
7
+ include Suture::Adapter::Log
8
+
6
9
  def choose(plan)
7
10
  if plan.record_calls
11
+ if plan.new
12
+ log_warn <<-MSG.gsub(/^ {12}/,'')
13
+ Seam #{plan.name.inspect} has a :new code path defined, but because
14
+ it is set to :record_calls, we will invoke the :old code path
15
+ instead. If this is not what you intend, set :record_calls to false.
16
+ MSG
17
+ end
8
18
  Surgeon::Observer.new
9
19
  else
10
20
  Surgeon::NoOp.new
@@ -0,0 +1,7 @@
1
+ module Suture
2
+ class Comparator
3
+ def call(recorded, actual)
4
+ recorded == actual || Marshal.dump(recorded) == Marshal.dump(actual)
5
+ end
6
+ end
7
+ end
@@ -1,16 +1,28 @@
1
1
  module Suture::Error
2
2
  class ObservationConflict < StandardError
3
- def initialize(name, args, new_result, old_result)
3
+ def initialize(name, args_inspect, new_result, old_observation)
4
4
  @name = name
5
- @args = args
5
+ @args_inspect = args_inspect
6
6
  @new_result = new_result
7
- @old_result = old_result
7
+ @old_id = old_observation.id
8
+ @old_result = old_observation.result
8
9
  end
9
10
 
10
11
  def message
11
- <<-MSG.gsub(/^\s{8}/,'')
12
- At suture #{@name.inspect} with inputs `#{@args.inspect}`, the newly-observed return value `#{@new_result.inspect}`
13
- conflicts with previously recorded return value `#{@old_result.inspect}`.
12
+ <<-MSG.gsub(/^ {8}/,'')
13
+ At seam #{@name.inspect}, we just recorded a duplicate call, but the same arguments
14
+ resulted in a different output. Read on for details:
15
+
16
+ Arguments: ```
17
+ #{@args_inspect}
18
+ ```
19
+ Previously-observed return value: ```
20
+ #{@old_result.inspect}
21
+ ```
22
+
23
+ Newly-observed return value: ```
24
+ #{@new_result.inspect}
25
+ ```
14
26
 
15
27
  That's not good! Here are a few ideas of what may have happened:
16
28
 
@@ -22,21 +34,22 @@ module Suture::Error
22
34
  different timestamp) or side effects (e.g. saving to a database
23
35
  resulting in a different GUID value) mean that Suture is detecting two
24
36
  different results for the same inputs. This can be worked around by
25
- providing a custom comparator for the two values nearest common
26
- ancestor type. Comparator support is tracked here:
27
- https://github.com/testdouble/suture/issues/14
37
+ providing a custom comparator to Suture. For more info, see the README:
38
+
39
+ https://github.com/testdouble/suture#creating-a-custom-comparator
28
40
 
29
41
  3. If neither of the above are true, it's possible that the old code path
30
42
  was changed while still in the early stage of recording characterization
31
43
  calls (presumably by mistake). If such a change may have occurred in
32
44
  error, check your git history. Otherwise, perhaps you `record_calls` is
33
- accidentally still enabled and should be turned off for this suture
45
+ accidentally still enabled and should be turned off for this seam
34
46
  (either with SUTURE_RECORD_CALLS=false or :record_calls => false).
35
47
 
36
- 4. If the old recording was made in error, then you may want to delete it
37
- Deletion support via the Suture API is tracked here:
38
- https://github.com/testdouble/suture/issues/10
48
+ 4. If, after exhausting the possibilities above, you're pretty sure the
49
+ recorded result is in error, you can delete it from Suture's database
50
+ with:
39
51
 
52
+ Suture.delete(#{@old_id})
40
53
  MSG
41
54
  end
42
55
  end
@@ -1,12 +1,167 @@
1
1
  module Suture::Error
2
2
  class VerificationFailed < StandardError
3
- def initialize(test_results)
4
- super
5
- @test_results = test_results
3
+ def initialize(plan, results)
4
+ @plan = plan
5
+ @results = results
6
6
  end
7
7
 
8
- # def message
9
- # end
8
+ def message
9
+ [
10
+ intro,
11
+ describe_failures(@results.failed, @plan),
12
+ configuration(@plan),
13
+ summarize(@results)
14
+ ].join("\n")
15
+ end
16
+
17
+ private
18
+
19
+ def intro
20
+ <<-MSG.gsub(/^ {8}/,'')
21
+
22
+ # Verification of your seam failed!
23
+
24
+ Descriptions of each unsuccessful verification follows:
25
+ MSG
26
+ end
27
+
28
+ def summarize(results)
29
+ <<-MSG.gsub(/^ {8}/,'')
30
+ # Result Summary
31
+
32
+ - Passed........#{results.passed_count}
33
+ - Failed........#{results.failed_count}
34
+ - with error..#{results.errored_count}
35
+ - Skipped.......#{results.skipped_count}
36
+ - Total calls...#{results.total_count}
37
+ MSG
38
+ end
39
+
40
+ def configuration(plan)
41
+ <<-MSG.gsub(/^ {8}/,'')
42
+ # Configuration
43
+
44
+ This is the configuration used by this test run:
45
+
46
+ ```
47
+ {
48
+ :comparator => #{describe_comparator(plan.comparator)}
49
+ :database_path => #{plan.database_path.inspect},
50
+ :fail_fast => #{plan.fail_fast},
51
+ :call_limit => #{plan.call_limit.inspect},#{" # (no limit)" if plan.call_limit.nil?}
52
+ :time_limit => #{plan.time_limit.inspect},#{plan.time_limit.nil? ? " # (no limit)" : " # (in seconds)"}
53
+ :error_message_limit => #{plan.error_message_limit.inspect},#{" # (no limit)" if plan.error_message_limit.nil?}
54
+ :random_seed => #{plan.random_seed.inspect}#{" # (insertion order)" if plan.random_seed.nil?}
55
+ }
56
+ ```
57
+ MSG
58
+ end
59
+
60
+ def describe_comparator(comparator)
61
+ if comparator.kind_of?(Proc)
62
+ "Proc, # (in: `#{describe_source_location(*comparator.source_location)}`)"
63
+ elsif comparator.respond_to?(:method) && comparator.method(:call)
64
+ "#{comparator.class}.new, # (in: `#{describe_source_location(*comparator.method(:call).source_location)}`)"
65
+ end
66
+ end
67
+
68
+ def describe_source_location(file, line)
69
+ root = File.join(Dir.getwd, "/")
70
+ path = file.start_with?(root) ? file.gsub(root, '') : file
71
+ "#{path}:#{line}"
72
+ end
73
+
74
+ def describe_failures(failures, plan)
75
+ return if failures.empty?
76
+ [
77
+ "## Failures\n",
78
+ failures.each_with_index.map { |failure, index|
79
+ describe_failure(failure, index) if plan.error_message_limit.nil? || index < plan.error_message_limit
80
+ },
81
+ describe_squelched_failure_messages(failures.size, plan.error_message_limit),
82
+ describe_general_failure_advice(plan)
83
+ ].flatten.compact.join("\n")
84
+ end
85
+
86
+ def describe_failure(failure, index)
87
+ expected = failure[:observation]
88
+ return <<-MSG.gsub(/^ {8}/,'')
89
+ #{index + 1}.) Recorded call for seam #{expected.name.inspect} (ID: #{expected.id}) ran and #{failure[:error] ? "raised an error" : "failed comparison"}.
90
+
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})`
109
+ MSG
110
+ end
111
+
112
+ def describe_squelched_failure_messages(failure_count, error_message_limit)
113
+ return if error_message_limit.nil? || error_message_limit >= failure_count
114
+ "(#{failure_count - error_message_limit} more failure messages were hidden because :error_message_limit was set to #{error_message_limit}.)"
115
+ end
116
+
117
+ def describe_general_failure_advice(plan)
118
+ <<-MSG.gsub(/^ {8}/,'')
119
+ ### Fixing these failures
120
+
121
+ #### Custom comparator
10
122
 
123
+ If any comparison is failing and you believe the results are
124
+ equivalent, we suggest you look into creating a custom comparator.
125
+ See more details here:
126
+
127
+ https://github.com/testdouble/suture#creating-a-custom-comparator
128
+
129
+ #### Random seed
130
+
131
+ Suture runs all verifications in random order by default. If you're
132
+ seeing an erratic failure, it's possibly due to order-dependent
133
+ behavior somewhere in your subject's code.
134
+
135
+ #{if !plan.random_seed.nil?
136
+ <<-MOAR.gsub(/^ {14}/,'')
137
+ To re-run the tests with the same random seed as was used in this run,
138
+ set the env var `SUTURE_RANDOM_SEED=#{plan.random_seed}` or the config entry
139
+ `:random_seed => #{plan.random_seed}`.
140
+
141
+ To re-run the tests without added shuffling (that is, in the order the
142
+ calls were recorded in), then set the random seed explicitly to nil
143
+ with env var `SUTURE_RANDOM_SEED=nil` or the config entry
144
+ `:random_seed => nil`.
145
+ MOAR
146
+ else
147
+ <<-MOAR.gsub(/^ {14}/,'')
148
+ This test was run in insertion order (by the primary key of the table
149
+ that stores calls, ascending). This is sometimes necessary when the
150
+ code has an order-dependent side effect, but shouldn't be set unless it's
151
+ clearly necessary, so as not to incidentally encourage _porting over_
152
+ that temporal side effect to the new code path. To restore random
153
+ ordering, unset the env var `SUTURE_RANDOM_SEED` and/or the config entry
154
+ `:random_seed`.
155
+ MOAR
156
+ end.chomp}
157
+ MSG
158
+ end
159
+
160
+
161
+ def stringify_error(error)
162
+ s = error.inspect
163
+ s += "\n" + error.backtrace.join("\n") if error.backtrace
164
+ s
165
+ end
11
166
  end
12
167
  end
@@ -2,9 +2,9 @@ require "suture/error/verification_failed"
2
2
 
3
3
  module Suture
4
4
  class InterpretsResults
5
- def interpret(test_results)
5
+ def interpret(test_plan, test_results)
6
6
  return unless test_results.failed?
7
- raise Suture::Error::VerificationFailed.new(test_results)
7
+ raise Suture::Error::VerificationFailed.new(test_plan, test_results)
8
8
  end
9
9
  end
10
10
  end
@@ -3,14 +3,14 @@ require "suture/util/env"
3
3
 
4
4
  module Suture
5
5
  class PrescribesTestPlan
6
- UN_ENV_IABLE_OPTIONS = [:name, :subject, :args]
6
+ UN_ENV_IABLE_OPTIONS = [:name, :subject, :comparator]
7
7
  DEFAULT_TEST_OPTIONS = {
8
- :fail_fast => true
8
+ :fail_fast => false
9
9
  }
10
10
 
11
11
  def prescribe(name, options = {})
12
- Value::TestPlan.new(DEFAULT_OPTIONS.
13
- merge(DEFAULT_TEST_OPTIONS).
12
+ Value::TestPlan.new(DEFAULT_TEST_OPTIONS.
13
+ merge(Suture.config).
14
14
  merge(options).
15
15
  merge(:name => name).
16
16
  merge(Suture::Util::Env.to_map(UN_ENV_IABLE_OPTIONS)))