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 +7 -0
- data/.gitignore +10 -0
- data/Gemfile +4 -0
- data/README.md +344 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/suture.rb +16 -0
- data/lib/suture/adapter/dictaphone.rb +26 -0
- data/lib/suture/builds_plan.rb +22 -0
- data/lib/suture/chooses_surgeon.rb +11 -0
- data/lib/suture/performs_surgery.rb +7 -0
- data/lib/suture/surgeon.rb +2 -0
- data/lib/suture/surgeon/no_op.rb +8 -0
- data/lib/suture/surgeon/observer.rb +16 -0
- data/lib/suture/value.rb +2 -0
- data/lib/suture/value/observation.rb +11 -0
- data/lib/suture/value/plan.rb +12 -0
- data/lib/suture/version.rb +3 -0
- data/lib/suture/wrap/sqlite.rb +28 -0
- data/suture.gemspec +28 -0
- metadata +148 -0
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
data/Gemfile
ADDED
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
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,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
|
+
|
data/lib/suture/value.rb
ADDED
@@ -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,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: []
|