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