suture 0.1.0 → 0.2.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: bb83b84ac62cb811bdb3dcdd170aff6f8269fb3a
4
- data.tar.gz: c5388bb1365857898de1de7743d5929d1fce620c
3
+ metadata.gz: 00233003cd21f9ec984925ed6fbc2f5a03ce9671
4
+ data.tar.gz: 526640d66eed549a7e7943f3ef2c459b1bfcd315
5
5
  SHA512:
6
- metadata.gz: 3b01812aa2186bf0004864320e64308b3f40c68efa0e84e2549e4581599776a15ff3b8f2612a34bbf92790a518bc09ce96ee6df4fd56a2420e535528c535da42
7
- data.tar.gz: bed8680f9b57cf5c789ab4d03347e0e2a9fe2415db9d7c98cd4f860a03613cb8f5ea22e187d5c16bc18c079a4b759454129bcbe1ba363bb9b18f5072deedc1a4
6
+ metadata.gz: 9b19ab9fd1965a1ac8b41522b4cbd9b5cad966616c4e3518bb41ad120375e324cf341d1bb5bd735b2cc341e28a8b00ffd108e1db7c25e3eb1d934016b52233c1
7
+ data.tar.gz: 53ceca44a97179bb4a256357d6ae3c587b9c67751c7aebc63fdf0c5637c9888f081e32d8b0f371f353b0a2c987b43d54fb295512c2830b0ea6e71c9062c2b40b
data/.projections.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "*": { "make": "bundle exec rake" },
3
+ "lib/*.rb": {"alternate": "test/{}_test.rb"},
4
+ "test/*_test.rb": {"alternate": "lib/{}.rb"}
5
+ }
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ sudo: false
3
+ before_install: gem install bundler
4
+ rvm:
5
+ - 1.8.7
6
+ - 1.9
7
+ - 2.0
8
+ - 2.1
9
+ - 2.2
10
+ - 2.3.1
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016 Test Double, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Suture
2
2
 
3
+ [![Build Status](https://travis-ci.org/testdouble/suture.svg?branch=master)](https://travis-ci.org/testdouble/suture)
4
+
3
5
  A refactoring tool for Ruby, designed to make it safe to change code you don't
4
6
  confidently understand. In fact, changing untrustworthy code is so fraught,
5
7
  Suture hopes to make it safer to completely reimplement a code path.
@@ -1,18 +1,30 @@
1
1
  require "suture/wrap/sqlite"
2
+ require "suture/value/observation"
3
+ require "suture/error/observation_conflict"
2
4
 
3
5
  module Suture::Adapter
4
6
  class Dictaphone
5
- def initialize
6
- @db = Suture::Wrap::Sqlite.init
7
+ def initialize(plan)
8
+ @db = Suture::Wrap::Sqlite.init(plan.database_path)
9
+ @name = plan.name
10
+ @args = plan.args
7
11
  end
8
12
 
9
- def record(name, args, result)
13
+ def record(result)
10
14
  Suture::Wrap::Sqlite.insert(@db, :observations, [:name, :args, :result],
11
- [name.to_s, Marshal.dump(args), Marshal.dump(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)
21
+ else
22
+ # We're all good here, it was just a duplicative observation. No harm.
23
+ end
12
24
  end
13
25
 
14
- def play(name)
15
- rows = Suture::Wrap::Sqlite.select(@db, :observations, "where name = ?", [name.to_s])
26
+ def play
27
+ rows = Suture::Wrap::Sqlite.select(@db, :observations, "where name = ?", [@name.to_s])
16
28
  rows.map do |row|
17
29
  Suture::Value::Observation.new(
18
30
  row[0],
@@ -22,5 +34,17 @@ module Suture::Adapter
22
34
  )
23
35
  end
24
36
  end
37
+
38
+ private
39
+
40
+ def result_for(name, args)
41
+ rows = Suture::Wrap::Sqlite.select(
42
+ @db,
43
+ :observations,
44
+ "where name = ? and args = ?",
45
+ [name.to_s, Marshal.dump(args)]
46
+ )
47
+ Marshal.load(rows.first[3])
48
+ end
25
49
  end
26
50
  end
@@ -1,22 +1,17 @@
1
+ require "suture/value/plan"
2
+ require "suture/util/env"
3
+
1
4
  module Suture
2
5
  class BuildsPlan
3
6
  UN_ENV_IABLE_OPTIONS = [:name, :old, :new, :args]
4
7
 
5
8
  def build(name, options = {})
6
- Value::Plan.new(options.merge(:name => name).merge(env))
7
- end
8
-
9
- private
10
-
11
- def env
12
- Hash[ENV.keys.
13
- select { |k| k.start_with?("SUTURE_") }.
14
- map { |k| [env_var_name_to_option_name(k), ENV[k]] }].
15
- reject { |(k,v)| UN_ENV_IABLE_OPTIONS.include?(k) }
16
- end
17
-
18
- def env_var_name_to_option_name(name)
19
- name.gsub(/^SUTURE\_/,'').downcase.to_sym
9
+ Value::Plan.new(
10
+ DEFAULT_OPTIONS.
11
+ merge(options).
12
+ merge(:name => name).
13
+ merge(Suture::Util::Env.to_map(UN_ENV_IABLE_OPTIONS))
14
+ )
20
15
  end
21
16
  end
22
17
  end
@@ -1,3 +1,6 @@
1
+ require "suture/surgeon/observer"
2
+ require "suture/surgeon/no_op"
3
+
1
4
  module Suture
2
5
  class ChoosesSurgeon
3
6
  def choose(plan)
@@ -0,0 +1,43 @@
1
+ module Suture::Error
2
+ class ObservationConflict < StandardError
3
+ def initialize(name, args, new_result, old_result)
4
+ @name = name
5
+ @args = args
6
+ @new_result = new_result
7
+ @old_result = old_result
8
+ end
9
+
10
+ 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}`.
14
+
15
+ That's not good! Here are a few ideas of what may have happened:
16
+
17
+ 1. The old code path may have a side effect that results in different
18
+ return values. If it's possible, to create the suture at a point after
19
+ this side effect. Otherwise, read on.
20
+
21
+ 2. Either environmental differents (e.g. system time resulting in a
22
+ different timestamp) or side effects (e.g. saving to a database
23
+ resulting in a different GUID value) mean that Suture is detecting two
24
+ 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
28
+
29
+ 3. If neither of the above are true, it's possible that the old code path
30
+ was changed while still in the early stage of recording characterization
31
+ calls (presumably by mistake). If such a change may have occurred in
32
+ error, check your git history. Otherwise, perhaps you `record_calls` is
33
+ accidentally still enabled and should be turned off for this suture
34
+ (either with SUTURE_RECORD_CALLS=false or :record_calls => false).
35
+
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
39
+
40
+ MSG
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,12 @@
1
+ module Suture::Error
2
+ class SchemaVersion < StandardError
3
+ def initialize(expected, actual)
4
+ @expected = expected
5
+ @actual = actual
6
+ end
7
+
8
+ def message
9
+ "Your suture gem is too #{@expected > @actual ? "new" : "old"} for this schema. Either delete your database or #{@expected > @actual ? "upgrade" : "downgrade"} the gem (expected schema version #{@expected}, was #{@actual})"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Suture::Error
2
+ class VerificationFailed < StandardError
3
+ def initialize(test_results)
4
+ super
5
+ @test_results = test_results
6
+ end
7
+
8
+ # def message
9
+ # end
10
+
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ require "suture/error/verification_failed"
2
+
3
+ module Suture
4
+ class InterpretsResults
5
+ def interpret(test_results)
6
+ return unless test_results.failed?
7
+ raise Suture::Error::VerificationFailed.new(test_results)
8
+ end
9
+ end
10
+ end
11
+
12
+
@@ -0,0 +1,19 @@
1
+ require "suture/value/test_plan"
2
+ require "suture/util/env"
3
+
4
+ module Suture
5
+ class PrescribesTestPlan
6
+ UN_ENV_IABLE_OPTIONS = [:name, :subject, :args]
7
+ DEFAULT_TEST_OPTIONS = {
8
+ :fail_fast => true
9
+ }
10
+
11
+ def prescribe(name, options = {})
12
+ Value::TestPlan.new(DEFAULT_OPTIONS.
13
+ merge(DEFAULT_TEST_OPTIONS).
14
+ merge(options).
15
+ merge(:name => name).
16
+ merge(Suture::Util::Env.to_map(UN_ENV_IABLE_OPTIONS)))
17
+ end
18
+ end
19
+ end
@@ -2,13 +2,10 @@ require "suture/adapter/dictaphone"
2
2
 
3
3
  module Suture::Surgeon
4
4
  class Observer
5
- def initialize
6
- @dictaphone = Suture::Adapter::Dictaphone.new
7
- end
8
-
9
5
  def operate(plan)
6
+ dictaphone = Suture::Adapter::Dictaphone.new(plan)
10
7
  plan.old.call(*plan.args).tap do |result|
11
- @dictaphone.record(plan.name, plan.args, result)
8
+ dictaphone.record(result)
12
9
  end
13
10
  end
14
11
  end
@@ -0,0 +1,38 @@
1
+ require "suture/adapter/dictaphone"
2
+ require "suture/value/test_results"
3
+
4
+ module Suture
5
+ class TestsPatient
6
+ def test(test_plan)
7
+ dictaphone = Suture::Adapter::Dictaphone.new(test_plan)
8
+ experienced_failure_in_life = false
9
+ Value::TestResults.new(dictaphone.play.map { |observation|
10
+ if test_plan.fail_fast && experienced_failure_in_life
11
+ {
12
+ :observation => observation,
13
+ :ran => false
14
+ }
15
+ else
16
+ invoke(test_plan, observation).merge({
17
+ :observation => observation,
18
+ :ran => true
19
+ }).tap { |r| experienced_failure_in_life = true unless r[:passed]}
20
+ end
21
+ })
22
+ end
23
+
24
+ def invoke(test_plan, observation)
25
+ {}.tap do |result|
26
+ begin
27
+ result[:new_result] = test_plan.subject.call(*observation.args)
28
+ # TODO: Comparators go here:
29
+ result[:passed] = result[:new_result] == observation.result
30
+ rescue StandardError => e
31
+ result[:passed] = false
32
+ result[:error] = e
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,22 @@
1
+ module Suture::Util
2
+ module Env
3
+ def self.to_map(excludes)
4
+ Hash[
5
+ ENV.keys.
6
+ select { |k| k.start_with?("SUTURE_") }.
7
+ map { |k| [to_sym(k), sanitize_value(ENV[k])] }
8
+ ].reject { |(k,v)| excludes.include?(k) }
9
+ end
10
+
11
+ # private
12
+
13
+ def self.to_sym(name)
14
+ name.gsub(/^SUTURE\_/,'').downcase.to_sym
15
+ end
16
+
17
+ def self.sanitize_value(value)
18
+ return false if value == "false"
19
+ value
20
+ end
21
+ end
22
+ end
@@ -1,12 +1,13 @@
1
1
  module Suture::Value
2
2
  class Plan
3
- attr_reader :name, :old, :new, :args, :record_calls
3
+ attr_reader :name, :old, :new, :args, :record_calls, :database_path
4
4
  def initialize(attrs = {})
5
5
  @name = attrs[:name]
6
6
  @old = attrs[:old]
7
7
  @new = attrs[:new]
8
8
  @args = attrs[:args]
9
9
  @record_calls = !!attrs[:record_calls]
10
+ @database_path = attrs[:database_path]
10
11
  end
11
12
  end
12
13
  end
@@ -0,0 +1,12 @@
1
+ module Suture::Value
2
+ class TestPlan
3
+ attr_accessor :name, :subject, :args, :fail_fast, :database_path
4
+ def initialize(attrs = {})
5
+ @name = attrs[:name]
6
+ @subject = attrs[:subject]
7
+ @args = attrs[:args]
8
+ @fail_fast = attrs[:fail_fast]
9
+ @database_path = attrs[:database_path]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module Suture::Value
2
+ class TestResults
3
+ attr_reader :results
4
+
5
+ def initialize(results)
6
+ @results = results
7
+ end
8
+
9
+ def failed?
10
+ @results.any? { |r| !r[:passed] }
11
+ end
12
+
13
+ def ran_all_tests?
14
+ @results.all? { |r| r[:ran] }
15
+ end
16
+
17
+ def total_count
18
+ @results.size
19
+ end
20
+
21
+ def passed_count
22
+ @results.count { |r| r[:passed] }
23
+ end
24
+
25
+ def failed_count
26
+ @results.count { |r| !r[:passed] && r[:ran] }
27
+ end
28
+
29
+ def errored_count
30
+ @results.count { |r| r[:error] }
31
+ end
32
+
33
+ def skipped_count
34
+ @results.count { |r| !r[:ran] }
35
+ end
36
+ end
37
+ end
38
+
@@ -1,3 +1,3 @@
1
1
  module Suture
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,24 +1,45 @@
1
1
  require "fileutils"
2
- require "SQLite3"
2
+ require "sqlite3"
3
+ require "suture/error/schema_version"
3
4
 
4
5
  module Suture::Wrap
5
6
  module Sqlite
6
- def self.init
7
- FileUtils.mkdir_p(File.join(Dir.getwd, "db"))
8
- SQLite3::Database.new("db/suture.sqlite3").tap do |db|
7
+ SCHEMA_VERSION=1
8
+ def self.init(location)
9
+ full_path = File.join(Dir.getwd, location)
10
+ FileUtils.mkdir_p(File.dirname(full_path))
11
+ SQLite3::Database.new(full_path).tap do |db|
12
+ db.execute <<-SQL
13
+ create table if not exists schema_info (
14
+ version integer unique
15
+ );
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]
19
+ if SCHEMA_VERSION != actual_schema_version
20
+ raise Suture::Error::SchemaVersion.new(SCHEMA_VERSION, actual_schema_version)
21
+ end
22
+
9
23
  db.execute <<-SQL
10
24
  create table if not exists observations (
11
25
  id integer primary key,
12
26
  name varchar(255),
13
27
  args clob,
14
- result clob
28
+ result clob,
29
+ unique(name, args)
15
30
  );
16
31
  SQL
17
32
  end
18
33
  end
19
34
 
20
35
  def self.insert(db, table, cols, vals)
21
- db.execute("insert into #{table} (#{cols.join(", ")}) values (?,?,?)", vals)
36
+ sql = <<-SQL
37
+ insert into #{table}
38
+ (#{cols.join(", ")})
39
+ values
40
+ (#{vals.size.times.map { "?" }.join(", ")})
41
+ SQL
42
+ db.execute(sql, vals)
22
43
  end
23
44
 
24
45
  def self.select(db, table, where_clause, bind_params)
data/lib/suture.rb CHANGED
@@ -1,16 +1,29 @@
1
1
  require "suture/version"
2
2
 
3
- require "suture/surgeon"
4
- require "suture/value"
5
-
6
3
  require "suture/builds_plan"
7
4
  require "suture/chooses_surgeon"
8
5
  require "suture/performs_surgery"
9
6
 
7
+ require "suture/prescribes_test_plan"
8
+ require "suture/tests_patient"
9
+ require "suture/interprets_results"
10
+
10
11
  module Suture
12
+ DEFAULT_OPTIONS = {
13
+ :database_path => "db/suture.sqlite3"
14
+ }
15
+
11
16
  def self.create(name, options)
12
17
  plan = BuildsPlan.new.build(name, options)
13
18
  surgeon = ChoosesSurgeon.new.choose(plan)
14
19
  PerformsSurgery.new.perform(plan, surgeon)
15
20
  end
21
+
22
+ def self.verify(name, options)
23
+ test_plan = Suture::PrescribesTestPlan.new.prescribe(name, options)
24
+ test_results = Suture::TestsPatient.new.test(test_plan)
25
+ if test_results.failed?
26
+ Suture::InterpretsResults.new.interpret(test_results)
27
+ end
28
+ end
16
29
  end
data/suture.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.12"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "pry", "~> 0.10"
25
+ spec.add_development_dependency "pry", "~> 0.9.12.6"
26
26
  spec.add_development_dependency "minitest", "~> 5.9"
27
27
  spec.add_development_dependency "gimme", "~> 0.5"
28
28
  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.1.0
4
+ version: 0.2.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-19 00:00:00.000000000 Z
11
+ date: 2016-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.10'
61
+ version: 0.9.12.6
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.10'
68
+ version: 0.9.12.6
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: minitest
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -103,7 +103,10 @@ extensions: []
103
103
  extra_rdoc_files: []
104
104
  files:
105
105
  - ".gitignore"
106
+ - ".projections.json"
107
+ - ".travis.yml"
106
108
  - Gemfile
109
+ - LICENSE.txt
107
110
  - README.md
108
111
  - Rakefile
109
112
  - bin/console
@@ -112,13 +115,20 @@ files:
112
115
  - lib/suture/adapter/dictaphone.rb
113
116
  - lib/suture/builds_plan.rb
114
117
  - lib/suture/chooses_surgeon.rb
118
+ - lib/suture/error/observation_conflict.rb
119
+ - lib/suture/error/schema_version.rb
120
+ - lib/suture/error/verification_failed.rb
121
+ - lib/suture/interprets_results.rb
115
122
  - lib/suture/performs_surgery.rb
116
- - lib/suture/surgeon.rb
123
+ - lib/suture/prescribes_test_plan.rb
117
124
  - lib/suture/surgeon/no_op.rb
118
125
  - lib/suture/surgeon/observer.rb
119
- - lib/suture/value.rb
126
+ - lib/suture/tests_patient.rb
127
+ - lib/suture/util/env.rb
120
128
  - lib/suture/value/observation.rb
121
129
  - lib/suture/value/plan.rb
130
+ - lib/suture/value/test_plan.rb
131
+ - lib/suture/value/test_results.rb
122
132
  - lib/suture/version.rb
123
133
  - lib/suture/wrap/sqlite.rb
124
134
  - suture.gemspec
@@ -1,2 +0,0 @@
1
- require "suture/surgeon/no_op"
2
- require "suture/surgeon/observer"
data/lib/suture/value.rb DELETED
@@ -1,2 +0,0 @@
1
- require "suture/value/observation"
2
- require "suture/value/plan"