suture 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bb83b84ac62cb811bdb3dcdd170aff6f8269fb3a
4
+ data.tar.gz: c5388bb1365857898de1de7743d5929d1fce620c
5
+ SHA512:
6
+ metadata.gz: 3b01812aa2186bf0004864320e64308b3f40c68efa0e84e2549e4581599776a15ff3b8f2612a34bbf92790a518bc09ce96ee6df4fd56a2420e535528c535da42
7
+ data.tar.gz: bed8680f9b57cf5c789ab4d03347e0e2a9fe2415db9d7c98cd4f860a03613cb8f5ea22e187d5c16bc18c079a4b759454129bcbe1ba363bb9b18f5072deedc1a4
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /db/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in suture.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,344 @@
1
+ # Suture
2
+
3
+ A refactoring tool for Ruby, designed to make it safe to change code you don't
4
+ confidently understand. In fact, changing untrustworthy code is so fraught,
5
+ Suture hopes to make it safer to completely reimplement a code path.
6
+
7
+ Suture provides help to the entire lifecycle of refactoring poorly-understood
8
+ code, from local development, to a staging environment, and even in production.
9
+
10
+ Refactoring or reimplementing important code is an involved process! Instead of
11
+ listing out Suture's API without sufficient exposition, here is an example that
12
+ we'll take you through each stage of the lifecycle.
13
+
14
+ ## development
15
+
16
+ Suppose you have a really nasty worker method:
17
+
18
+ ``` ruby
19
+ class MyWorker
20
+ def do_work(id)
21
+ thing = Thing.find(id)
22
+ # … 99 lines of terribleness …
23
+ MyMailer.send(thing.result)
24
+ end
25
+ end
26
+ ```
27
+
28
+ ### 1. Identify a seam
29
+
30
+ A seam serves as an artificial entry point that sets a boundary around the code
31
+ you'd like to change. A good seam is:
32
+
33
+ * easy to invoke in isolation
34
+ * takes arguments, returns a value
35
+ * eliminates (or at least minimizes) side effects
36
+
37
+ Then, to create a seam, typically we create a new unit to house the code that we
38
+ excise from its original site, and then we call it. This adds a level of
39
+ indirection, which gives us the flexibility we'll need later.
40
+
41
+ In this case, to create a seam, we might start with this:
42
+
43
+ ``` ruby
44
+ class MyWorker
45
+ def do_work(id)
46
+ MyMailer.send(LegacyWorker.new.call(id))
47
+ end
48
+ end
49
+
50
+ class LegacyWorker
51
+ def call(id)
52
+ thing = Thing.find(id)
53
+ # … Still 99 lines. Still terrible …
54
+ thing.result
55
+ end
56
+ end
57
+ ```
58
+
59
+ As you can see, the call to `MyMailer.send` is left at the original call site,
60
+ since its effectively a void method being invoked for its side effect, and its
61
+ much easier to verify return values.
62
+
63
+ Since any changes to the code while it's untested are very dangerous, it's
64
+ important to minimize changes made for the sake of creating a clear seam.
65
+
66
+ ### 2. Create our suture
67
+
68
+ Next, we introduce Suture to the call site so we can start analyzing its
69
+ behavior:
70
+
71
+ ``` ruby
72
+ class MyWorker
73
+ def do_work(id)
74
+ MyMailer.send(Suture.create(:worker, {
75
+ old: LegacyWorker.new,
76
+ args: [id]
77
+ }))
78
+ end
79
+ end
80
+ ```
81
+
82
+ Where `old` can be anything callable with `call` (like the class above, a
83
+ method, or a Proc/lambda) and `args` is an array of the args to pass to it.
84
+
85
+ At this point, running this code will result in Suture just delegating to
86
+ LegacyWorker without taking any other meaningful action.
87
+
88
+ ### 3. Record the current behavior
89
+
90
+ Next, we want to start observing how the legacy worker is actually called: what
91
+ arguments are sent to it and what values does it return? By recording the calls
92
+ as we use our app locally, we can later test that the old and new
93
+ implementations behave the same way.
94
+
95
+ First, we tell Suture to start recording calls by setting the environment
96
+ variable `SUTURE_RECORD_CALLS` to something truthy (e.g.
97
+ `SUTURE_RECORD_CALLS=true bundle exec rails s`). So long as this variable is set,
98
+ any calls to our suture will record the arguments passed to the legacy code path
99
+ and the return value.
100
+
101
+ As you use the application (whether it's a queue system, a web app, or a CLI),
102
+ the calls will be saved to a sqlite database. If the legacy code path relies on
103
+ external data sources or services, keep in mind that your recorded inputs and
104
+ outputs will rely on them as well. You may want to narrow the scope of your
105
+ seam accordingly (e.g. to receive an object as an argument instead of a database
106
+ id).
107
+
108
+ #### Hard to exploratory test the code locally?
109
+
110
+ If it's difficult to generate realistic usage locally, then consider running
111
+ this step in production and fetching the sqlite DB after you've generated enough
112
+ inputs and outputs to be confident you've covered most realistic uses. Keep in
113
+ mind that this approach means your test environment will probably need access to
114
+ the same data stores as the environment that made the recording, which may not
115
+ be feasible or appropriate in many cases.
116
+
117
+ ### 4. Ensure current behavior with a test
118
+
119
+ Next, we should probably write a test that will ensure our new implementation
120
+ will continue to behave like the old one. We can use these recordings to help us
121
+ automate some of the drudgery typically associated with writing
122
+ [characterization tests](https://en.wikipedia.org/wiki/Characterization_test).
123
+
124
+ We could write a test like this:
125
+
126
+ ``` ruby
127
+ class MyWorkerCharacterizationTest < Minitest::Test
128
+ def setup
129
+ # Load the test data needed to resemble the environment when recording
130
+ end
131
+
132
+ def test_that_it_still_works
133
+ Suture.verify(:worker, {
134
+ :subject => LegacyWorker.new
135
+ :fail_fast => true
136
+ })
137
+ end
138
+ end
139
+ ```
140
+
141
+ `Suture.verify` will fail if any of the recorded arguments don't return the
142
+ expected value. It's a good idea to run this against the legacy code first,
143
+ for two reasons:
144
+
145
+ * running the characterization tests against the legacy code path will ensure
146
+ the test environment has the data needed to behave the same way as when it was
147
+ recorded (it may be appropriate to take a snapshot of the database before you
148
+ start recording and load it before you run your tests)
149
+
150
+ * by generating a code coverage report (
151
+ [simplecov](https://github.com/colszowka/simplecov) is a good one to start
152
+ with) from running this test in isolation, we can see what `LegacyWorker` is
153
+ actually calling, in an attempt to do two things:
154
+ * maximize coverage for code in the LegacyWorker (and for code that's
155
+ subordinate to it) to make sure our characterization test sufficiently
156
+ exercises it
157
+ * identify incidental coverage of code paths that are outside the scope of
158
+ what we hope to refactor, and in turn analyzing whether `LegacyWorker` has
159
+ side effects we didn't anticipate and should additionally write tests for
160
+
161
+ ### 5. Specify and test a path for new code
162
+
163
+ Once our automated characterization test of our recordings is passing, then we
164
+ can start work on a `NewWorker`. To get started, we can update our Suture
165
+ configuration:
166
+
167
+ ``` ruby
168
+ class MyWorker
169
+ def do_work(id)
170
+ MyMailer.send(Suture.create(:worker, {
171
+ old: LegacyWorker.new,
172
+ new: NewWorker.new,
173
+ args: [id]
174
+ }))
175
+ end
176
+ end
177
+
178
+ class NewWorker
179
+ def call(id)
180
+ end
181
+ end
182
+ ```
183
+
184
+ Next, we specify a `NewWorker` under the `:new` key. For now,
185
+ Suture will start sending all of its calls to `NewWorker#call`.
186
+
187
+ Next, let's write a test to verify the new code path also passes the recorded
188
+ interactions:
189
+
190
+ ``` ruby
191
+ class MyWorkerCharacterizationTest < Minitest::Test
192
+ def setup
193
+ # Load the test data needed to resemble the environment when recording
194
+ end
195
+
196
+ def test_that_it_still_works
197
+ Suture.verify(:worker, {
198
+ subject: LegacyWorker.new,
199
+ fail_fast: true
200
+ })
201
+ end
202
+
203
+ def test_new_thing_also_works
204
+ Suture.verify(:worker, {
205
+ subject: NewWorker.new,
206
+ fail_fast: false
207
+ })
208
+ end
209
+ end
210
+ ```
211
+
212
+ Obviously, this should fail until `NewWorker`'s implementation covers all the
213
+ cases we recorded from `LegacyWorker`.
214
+
215
+ Remember, characterization tests aren't designed to be kept around forever. Once
216
+ you're confident that the new implementation is sufficient, it's typically better
217
+ to discard them and design focused, intention-revealing tests for the new
218
+ implementation and its component parts.
219
+
220
+ ### 6. Refactor or reimplement the legacy code.
221
+
222
+ This step is the hardest part and there's not much Suture can do to make it
223
+ any easier. How you go about implementing your improvements depends on whether
224
+ you intend to rewrite the legacy code path or refactor it. Some comment on each
225
+ approach follows:
226
+
227
+ #### Reimplementing
228
+
229
+ The best time to rewrite a piece of software is when you have a better
230
+ understanding of the real-world process it models than the original authors did
231
+ when they first wrote it. If that's the case, it's likely you'll think of more
232
+ reliable names and abstractions than they did.
233
+
234
+ As for workflow, consider writing the new implementation like you would any other
235
+ new part of the system, with the added benefit of being able to run the
236
+ characterization tests as a progress indicator and a backstop for any missed edge
237
+ cases. The ultimate goal of this workflow should be to incrementally arrive at a
238
+ clean design that completely passes the characterization test run by
239
+ `Suture.verify`.
240
+
241
+ #### Refactoring
242
+
243
+ If you choose to refactor the working implementation, though, you should start
244
+ by copying it (and all of its subordinate types) into the new, separate code
245
+ path. The goal should be to keep the legacy code path in a working state, so
246
+ that `Suture` can run it when needed until we're supremely confident that it can
247
+ be safely discarded. (It's also nice to be able to perform side-by-side
248
+ comparisons without having to check out a different git reference.)
249
+
250
+ The workflow when refactoring should be to take small, safe steps using well
251
+ understood [refactoring patterns](https://www.amazon.com/Refactoring-Ruby-Addison-Wesley-Professional/dp/0321984137)
252
+ and running the characterization test suite frequently to ensure nothing was
253
+ accidentally broken.
254
+
255
+ Once the code is factored well enough to work with (i.e. it is clear enough to
256
+ incorporate future anticipated changes), consider writing some clear and clean
257
+ unit tests around new units that shook out from the activity. Having good tests
258
+ for well-factored code is the best guard against seeing it slip once again into
259
+ poorly-understood "legacy" code.
260
+
261
+ ## staging
262
+
263
+ Once you've changed the code, you still may not be confident enough to delete it
264
+ entirely. It's possible (even likely) that your local exploratory testing didn't
265
+ exercise every branch in the original code with the full range of potential
266
+ arguments and broader state.
267
+
268
+ Suture gives users a way to experiment with risky refactors by deploying them to
269
+ a staging environment and running both the original and new code paths
270
+ side-by-side, raising an error in the event they don't return the same value.
271
+ This is governed by the `:run_both` to `true`:
272
+
273
+ ``` ruby
274
+ class MyWorker
275
+ def do_work(id)
276
+ MyMailer.send(Suture.create(:worker, {
277
+ old: LegacyWorker.new,
278
+ new: NewWorker.new,
279
+ args: [id],
280
+ run_both: true
281
+ }))
282
+ end
283
+ end
284
+ ```
285
+
286
+ With this setting, the seam will call through to **both** legacy and refactored
287
+ implementations, and will raise an error if they don't return the same value.
288
+ Obviously, this setting is only helpful if the paths don't trigger major or
289
+ destructive side effects.
290
+
291
+ ## production
292
+
293
+ You're _almost_ ready to delete the old code path and switch production over to
294
+ the new one, but fear lingers: maybe there's an edge case your testing to this
295
+ point hasn't caught.
296
+
297
+ Suture was written to minimize the inhibition to moving forward with changing
298
+ code, so it provides a couple features designed to be run in production when
299
+ you're yet unsure that your refactor or reimplementation is complete.
300
+
301
+ ### Logging errors
302
+
303
+ While your application's logs aren't affected by Suture, it may be helpful for
304
+ Suture to maintain a separate log file for any errors that are raised by the
305
+ refactored code path. Setting the key `:log` to a path will prompt Suture to
306
+ append any errors to a log at that location.
307
+
308
+ ``` ruby
309
+ class MyWorker
310
+ def do_work(id)
311
+ MyMailer.send(Suture.create(:worker, {
312
+ old: LegacyWorker.new,
313
+ new: NewWorker.new,
314
+ args: [id],
315
+ log: 'log/my_worker_seam.log'
316
+ }))
317
+ end
318
+ end
319
+ ```
320
+
321
+ ### Retrying failures
322
+
323
+ Since the legacy code path hasn't been deleted yet, there's no reason to leave
324
+ users hanging if the new code path explodes. By setting the `:retry` entry to
325
+ `true`, Suture will rescue any errors raised from the new code path and attempt
326
+ to invoke the legacy code path instead.
327
+
328
+ ``` ruby
329
+ class MyWorker
330
+ def do_work(id)
331
+ MyMailer.send(Suture.create(:worker, {
332
+ old: LegacyWorker.new,
333
+ new: NewWorker.new,
334
+ args: [id],
335
+ retry: true
336
+ }))
337
+ end
338
+ end
339
+ ```
340
+
341
+ Since this approach rescues errors, it's possible that errors in the new code
342
+ path will go unnoticed, so it's best used in conjunction with Suture's logging
343
+ feature. Before ultimately deciding to finally delete the legacy code path,
344
+ double-check that the logs aren't full of rescued errors!
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/helper.rb', 'test/**/*_test.rb']
8
+ end
9
+
10
+ Rake::TestTask.new(:safe) do |t|
11
+ t.libs << "safe"
12
+ t.libs << "lib"
13
+ t.test_files = FileList['safe/helper.rb', 'safe/**/*_test.rb']
14
+ end
15
+
16
+ task :default => [:test, :safe]
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "suture"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/suture.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "suture/version"
2
+
3
+ require "suture/surgeon"
4
+ require "suture/value"
5
+
6
+ require "suture/builds_plan"
7
+ require "suture/chooses_surgeon"
8
+ require "suture/performs_surgery"
9
+
10
+ module Suture
11
+ def self.create(name, options)
12
+ plan = BuildsPlan.new.build(name, options)
13
+ surgeon = ChoosesSurgeon.new.choose(plan)
14
+ PerformsSurgery.new.perform(plan, surgeon)
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ require "suture/wrap/sqlite"
2
+
3
+ module Suture::Adapter
4
+ class Dictaphone
5
+ def initialize
6
+ @db = Suture::Wrap::Sqlite.init
7
+ end
8
+
9
+ def record(name, args, result)
10
+ Suture::Wrap::Sqlite.insert(@db, :observations, [:name, :args, :result],
11
+ [name.to_s, Marshal.dump(args), Marshal.dump(result)])
12
+ end
13
+
14
+ def play(name)
15
+ rows = Suture::Wrap::Sqlite.select(@db, :observations, "where name = ?", [name.to_s])
16
+ rows.map do |row|
17
+ Suture::Value::Observation.new(
18
+ row[0],
19
+ row[1].to_sym,
20
+ Marshal.load(row[2]),
21
+ Marshal.load(row[3])
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ module Suture
2
+ class BuildsPlan
3
+ UN_ENV_IABLE_OPTIONS = [:name, :old, :new, :args]
4
+
5
+ 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
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module Suture
2
+ class ChoosesSurgeon
3
+ def choose(plan)
4
+ if plan.record_calls
5
+ Surgeon::Observer.new
6
+ else
7
+ Surgeon::NoOp.new
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Suture
2
+ class PerformsSurgery
3
+ def perform(plan, surgeon)
4
+ surgeon.operate(plan)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ require "suture/surgeon/no_op"
2
+ require "suture/surgeon/observer"
@@ -0,0 +1,8 @@
1
+ module Suture::Surgeon
2
+ class NoOp
3
+ def operate(plan)
4
+ return unless plan.old
5
+ plan.old.call(*plan.args)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ require "suture/adapter/dictaphone"
2
+
3
+ module Suture::Surgeon
4
+ class Observer
5
+ def initialize
6
+ @dictaphone = Suture::Adapter::Dictaphone.new
7
+ end
8
+
9
+ def operate(plan)
10
+ plan.old.call(*plan.args).tap do |result|
11
+ @dictaphone.record(plan.name, plan.args, result)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,2 @@
1
+ require "suture/value/observation"
2
+ require "suture/value/plan"
@@ -0,0 +1,11 @@
1
+ module Suture::Value
2
+ 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
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module Suture::Value
2
+ class Plan
3
+ attr_reader :name, :old, :new, :args, :record_calls
4
+ def initialize(attrs = {})
5
+ @name = attrs[:name]
6
+ @old = attrs[:old]
7
+ @new = attrs[:new]
8
+ @args = attrs[:args]
9
+ @record_calls = !!attrs[:record_calls]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Suture
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ require "fileutils"
2
+ require "SQLite3"
3
+
4
+ module Suture::Wrap
5
+ 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|
9
+ db.execute <<-SQL
10
+ create table if not exists observations (
11
+ id integer primary key,
12
+ name varchar(255),
13
+ args clob,
14
+ result clob
15
+ );
16
+ SQL
17
+ end
18
+ end
19
+
20
+ def self.insert(db, table, cols, vals)
21
+ db.execute("insert into #{table} (#{cols.join(", ")}) values (?,?,?)", vals)
22
+ end
23
+
24
+ def self.select(db, table, where_clause, bind_params)
25
+ db.execute("select * from #{table} #{where_clause}", bind_params)
26
+ end
27
+ end
28
+ end
data/suture.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'suture/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "suture"
8
+ spec.version = Suture::VERSION
9
+ spec.authors = ["Justin Searls"]
10
+ spec.email = ["searls@gmail.com"]
11
+
12
+ spec.summary = %q{A gem that helps people refactor or reimplement legacy code}
13
+ spec.description = %q{Provides tools to record calls to legacy code and verify new implementations still work}
14
+ spec.homepage = "https://github.com/testdouble/suture"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|db|safe|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "sqlite3"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.12"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "pry", "~> 0.10"
26
+ spec.add_development_dependency "minitest", "~> 5.9"
27
+ spec.add_development_dependency "gimme", "~> 0.5"
28
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: suture
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Justin Searls
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-08-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sqlite3
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.10'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.10'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: gimme
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.5'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.5'
97
+ description: Provides tools to record calls to legacy code and verify new implementations
98
+ still work
99
+ email:
100
+ - searls@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - Gemfile
107
+ - README.md
108
+ - Rakefile
109
+ - bin/console
110
+ - bin/setup
111
+ - lib/suture.rb
112
+ - lib/suture/adapter/dictaphone.rb
113
+ - lib/suture/builds_plan.rb
114
+ - lib/suture/chooses_surgeon.rb
115
+ - lib/suture/performs_surgery.rb
116
+ - lib/suture/surgeon.rb
117
+ - lib/suture/surgeon/no_op.rb
118
+ - lib/suture/surgeon/observer.rb
119
+ - lib/suture/value.rb
120
+ - lib/suture/value/observation.rb
121
+ - lib/suture/value/plan.rb
122
+ - lib/suture/version.rb
123
+ - lib/suture/wrap/sqlite.rb
124
+ - suture.gemspec
125
+ homepage: https://github.com/testdouble/suture
126
+ licenses: []
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.5.1
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: A gem that helps people refactor or reimplement legacy code
148
+ test_files: []