suture 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []