scripterator 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 +15 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +123 -0
- data/lib/scripterator.rb +33 -0
- data/lib/scripterator/configuration.rb +21 -0
- data/lib/scripterator/runner.rb +130 -0
- data/lib/scripterator/script_redis.rb +46 -0
- data/lib/scripterator/version.rb +3 -0
- data/scripterator.gemspec +24 -0
- data/spec/runner_spec.rb +138 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/widget.rb +14 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
N2ViZDdmMmFjMWUxYzIyMTlmMDg4OTYzZmFlZWNkZDkyNWExMzEyYQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MDEzMzJkNjdkNjRhZWVhODE2YTIwMjY2NWY2NDdiNGRhM2VmYmM0ZA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MGQxNmIzOTBlMjg5MjE5ZTJlZGVkNjM3ZTNjNzg0MGI1NjFmODZhMDBjNzgy
|
10
|
+
NjE3OGNlNmY3MGRjNDg5MjA1ODc2ODgwOTNmMzY0MmZlMWMzN2MzZjk5M2M5
|
11
|
+
ZWYzZjVhYWFjZTU2MDQ1YjFiMzg5NWZhMGJhYzkyOWMxNThmZmI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZGYxYzQyZWUwMGFjZTJmZWE5MmMyMjRhMDZiMmRjMWU0NTgyYjZkNTU2Y2Qx
|
14
|
+
YmM3YTE0OWZmNDJhYmMyOWE2ZTNmZTkwMDA3YTg4NDU4MTA1MjI0M2YwMzYx
|
15
|
+
ZDk4ZTViYzc2NThmNWYwMjZlYWU3ZTYzZDI0MTQwZjA3ZWRhNWY=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 t ddddddd
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# Scripterator
|
2
|
+
"Helping you scripterate over all the things"
|
3
|
+
|
4
|
+
A lightweight script harness and DSL for iterating over and running operations on ActiveRecord model records, with Redis hooks for managing subsets, failures, and retries.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'scripterator'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install scripterator
|
19
|
+
|
20
|
+
Works with ActiveRecord 3.* (Rails 4 support coming).
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
Create a .rb file with your script code:
|
25
|
+
```ruby
|
26
|
+
Scripterator.run "Convert users from legacy auth data" do
|
27
|
+
|
28
|
+
before do
|
29
|
+
User.skip_some_callbacks_we_want_to_avoid_during_script_running
|
30
|
+
end
|
31
|
+
|
32
|
+
for_each_user do |user|
|
33
|
+
user.do_legacy_conversion
|
34
|
+
end
|
35
|
+
|
36
|
+
after do
|
37
|
+
# some code to run after everything's finished
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
Run your script for a given set of IDs:
|
44
|
+
|
45
|
+
$ START=10000 END=19999 bundle exec rails runner my_script.rb >out.txt
|
46
|
+
|
47
|
+
This will produce output of the form:
|
48
|
+
|
49
|
+
```
|
50
|
+
Starting at 2013-09-24 14:53:39 -0700...
|
51
|
+
2013-09-24 14:53:40 -0700: Checked 0 rows, 0 migrated.
|
52
|
+
2013-09-24 14:53:41 -0700: Checked 10000 rows, 10000 migrated.
|
53
|
+
2013-09-24 14:53:41 -0700: Checked 20000 rows, 20000 migrated.
|
54
|
+
2013-09-24 14:53:42 -0700: Checked 30000 rows, 30000 migrated.
|
55
|
+
done
|
56
|
+
Finished at 2013-09-24 14:53:43 -0700...
|
57
|
+
|
58
|
+
Total rows migrated: 34903 / 34903
|
59
|
+
0 rows previously migrated and skipped
|
60
|
+
0 errors
|
61
|
+
```
|
62
|
+
|
63
|
+
Retrieve set information about checked and failed records:
|
64
|
+
|
65
|
+
```
|
66
|
+
> Scripterator.failed_ids_for "Convert users from legacy auth data"
|
67
|
+
=> [14011, 15634, 17301, 17302]
|
68
|
+
|
69
|
+
> Scripterator.already_run_for?("Convert users from legacy auth data", 15000)
|
70
|
+
=> true
|
71
|
+
```
|
72
|
+
|
73
|
+
User-definable blocks:
|
74
|
+
|
75
|
+
Required:
|
76
|
+
|
77
|
+
- `for_each_(.+)`: code to run for every record. This block should return `true` (or a truthy value) if the operation ran successfully, or `false` (or a falsy value) if the record was skipped/ineligible. Errors and Exceptions will be caught by Scripterator and tabulated/output.
|
78
|
+
|
79
|
+
Optional:
|
80
|
+
|
81
|
+
- `model`: code with which model should be loaded, e.g., `model { User.includes(:profile, :roles) }`; if this block is not supplied, the model class is inferred from the `for_each_*` block, e.g., `for_each_post_comment` will cause the model `PostComment` to be loaded
|
82
|
+
- `before`: code to run before iteration begins
|
83
|
+
- `after`: code to run after iteration finishes
|
84
|
+
|
85
|
+
Environment variable options:
|
86
|
+
|
87
|
+
- `START`: first model ID to scripterate
|
88
|
+
- `END`: last model ID to scripterate
|
89
|
+
- `REDIS_EXPIRATION`: amount of time (in seconds) before Redis result sets (checked IDs and failed IDs) are expired
|
90
|
+
|
91
|
+
Either a starting or an ending ID must be provided.
|
92
|
+
|
93
|
+
## Configuration
|
94
|
+
|
95
|
+
Within an optional Rails initializer, configure Scripterator further as follows (`config/initializers/scripterator.rb`):
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
Scripterator.configure do |config|
|
99
|
+
# alternate Redis instance
|
100
|
+
config.redis = MyRedis.new
|
101
|
+
|
102
|
+
# turn off Redis
|
103
|
+
config.redis = nil
|
104
|
+
|
105
|
+
# change default Redis set expiration time
|
106
|
+
config.redis_expiration = 5.days
|
107
|
+
|
108
|
+
# set redis_expiration to 0 to turn off expiration
|
109
|
+
config.redis_expiration = 0
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
## Running tests
|
114
|
+
|
115
|
+
$ bundle exec rspec
|
116
|
+
|
117
|
+
## Contributing
|
118
|
+
|
119
|
+
1. Fork it
|
120
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
121
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
122
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
123
|
+
5. Create new Pull Request
|
data/lib/scripterator.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "redis" unless defined? Redis
|
2
|
+
|
3
|
+
require "scripterator/version"
|
4
|
+
require "scripterator/configuration"
|
5
|
+
require "scripterator/runner"
|
6
|
+
|
7
|
+
module Scripterator
|
8
|
+
class << self
|
9
|
+
def configure
|
10
|
+
yield config
|
11
|
+
end
|
12
|
+
|
13
|
+
def config
|
14
|
+
@config ||= Scripterator::Configuration.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def run(description, &block)
|
18
|
+
options = {}.tap do |o|
|
19
|
+
o[:start_id] = ENV['START'].try(:to_i)
|
20
|
+
o[:end_id] = ENV['END'].try(:to_i)
|
21
|
+
o[:redis_expiration] = ENV['REDIS_EXPIRATION'].try(:to_i) || config.redis_expiration
|
22
|
+
end
|
23
|
+
|
24
|
+
Runner.new(description, &block).run(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
%w(already_run_for? checked_ids failed_ids).each do |runner_method|
|
28
|
+
define_method(runner_method) do |description, *args|
|
29
|
+
Runner.new(description).send(runner_method, *args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Scripterator
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :redis_expiration
|
4
|
+
attr_reader :redis
|
5
|
+
|
6
|
+
# set config.redis = nil to use NilRedis implementation
|
7
|
+
def redis=(r)
|
8
|
+
@redis = r || Scripterator::NilRedis.new
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class NilRedis
|
13
|
+
def smembers(*args)
|
14
|
+
[]
|
15
|
+
end
|
16
|
+
|
17
|
+
%w(expire sadd sismember).each do |redis_method|
|
18
|
+
define_method(redis_method) { |*args| nil }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require_relative 'script_redis'
|
3
|
+
|
4
|
+
module Scripterator
|
5
|
+
class Runner
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :script_redis,
|
8
|
+
:already_run_for?, :checked_ids, :expire_redis_sets, :failed_ids,
|
9
|
+
:mark_as_failed_for, :mark_as_run_for, :script_key
|
10
|
+
|
11
|
+
def initialize(description, &config_block)
|
12
|
+
@script_description = description
|
13
|
+
|
14
|
+
self.instance_eval(&config_block) if config_block
|
15
|
+
|
16
|
+
@model ||= Proc.new { eval("#{@inferred_model_name}") } # constantize
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(options = {})
|
20
|
+
unless options[:start_id] || options[:end_id]
|
21
|
+
raise 'You must provide either a start ID or end ID'
|
22
|
+
end
|
23
|
+
@start_id = options[:start_id] || 1
|
24
|
+
@end_id = options[:end_id]
|
25
|
+
@redis_expiration = options[:redis_expiration]
|
26
|
+
@output_stream = options[:output_stream] || $stdout
|
27
|
+
|
28
|
+
raise 'No per_record code defined' unless @per_record
|
29
|
+
|
30
|
+
init_vars
|
31
|
+
run_blocks
|
32
|
+
output_stats
|
33
|
+
end
|
34
|
+
|
35
|
+
%w(model before per_record after).each do |callback|
|
36
|
+
define_method callback do |&block|
|
37
|
+
instance_variable_set "@#{callback}", block
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(method_name, *args, &block)
|
42
|
+
if model_name = /for_each_(.+)/.match(method_name)[1]
|
43
|
+
@per_record = block
|
44
|
+
@inferred_model_name = model_name.split('_').map(&:capitalize).join
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def init_vars
|
53
|
+
@success_count = 0
|
54
|
+
@total_checked = 0
|
55
|
+
@already_done = 0
|
56
|
+
@errors = []
|
57
|
+
end
|
58
|
+
|
59
|
+
def fetch_record(id)
|
60
|
+
model_finder.find_by_id(id)
|
61
|
+
end
|
62
|
+
|
63
|
+
def model_finder
|
64
|
+
@model_finder ||= self.instance_eval(&@model)
|
65
|
+
end
|
66
|
+
|
67
|
+
def output_progress
|
68
|
+
if (@total_checked + @already_done) % 10000 == 0
|
69
|
+
output "#{Time.now}: Checked #{@total_checked} rows, #{@success_count} migrated."
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def output_stats
|
74
|
+
output "Total rows migrated: #{@success_count} / #{@total_checked}"
|
75
|
+
output "#{@already_done} rows previously migrated and skipped"
|
76
|
+
output "#{@errors.count} errors"
|
77
|
+
if @errors.count > 0 && !failed_ids.empty?
|
78
|
+
output " Retrieve failed IDs with redis: SMEMBERS #{script_key(:failed)}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def output(*args)
|
83
|
+
@output_stream.puts(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
def run_blocks
|
87
|
+
self.instance_eval(&@before) if @before
|
88
|
+
|
89
|
+
output "Starting at #{Time.now}..."
|
90
|
+
run_loop
|
91
|
+
output 'done'
|
92
|
+
output "Finished at #{Time.now}...\n\n"
|
93
|
+
|
94
|
+
self.instance_eval(&@after) if @after
|
95
|
+
end
|
96
|
+
|
97
|
+
def run_loop
|
98
|
+
if @end_id
|
99
|
+
(@start_id..@end_id).each { |id| transform_one_record(fetch_record(id)) }
|
100
|
+
else
|
101
|
+
model_finder.find_each(start: @start_id) { |record| transform_one_record(record) }
|
102
|
+
end
|
103
|
+
|
104
|
+
expire_redis_sets
|
105
|
+
end
|
106
|
+
|
107
|
+
def transform_one_record(record)
|
108
|
+
return if record.nil?
|
109
|
+
|
110
|
+
output_progress
|
111
|
+
|
112
|
+
if already_run_for? record.id
|
113
|
+
@already_done += 1
|
114
|
+
else
|
115
|
+
mark_as_run_for record.id
|
116
|
+
@total_checked += 1
|
117
|
+
@success_count += 1 if self.instance_exec record, &@per_record
|
118
|
+
end
|
119
|
+
rescue
|
120
|
+
errmsg = "Record #{record.id}: #{$!}"
|
121
|
+
output "Error: #{errmsg}"
|
122
|
+
@errors << errmsg
|
123
|
+
mark_as_failed_for record.id
|
124
|
+
end
|
125
|
+
|
126
|
+
def script_redis
|
127
|
+
@script_redis ||= ScriptRedis.new(@script_description, redis_expiration: @redis_expiration)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Scripterator
|
2
|
+
class ScriptRedis
|
3
|
+
DEFAULT_EXPIRATION = 3 * 30 * 24 * 60 * 60 # 3 months
|
4
|
+
|
5
|
+
def initialize(script_description, options = {})
|
6
|
+
@key_prefix = "one_timer_script:#{script_description.downcase.split.join('_')}"
|
7
|
+
@redis_expiration = options[:redis_expiration] || DEFAULT_EXPIRATION
|
8
|
+
end
|
9
|
+
|
10
|
+
def checked_ids
|
11
|
+
redis.smembers(script_key(:checked)).map &:to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
def failed_ids
|
15
|
+
redis.smembers(script_key(:failed)).map &:to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def already_run_for?(id)
|
19
|
+
redis.sismember script_key(:checked), id
|
20
|
+
end
|
21
|
+
|
22
|
+
def expire_redis_sets
|
23
|
+
unless @redis_expiration <= 0
|
24
|
+
%w(checked failed).each { |set| redis.expire script_key(set), @redis_expiration }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def mark_as_failed_for(id)
|
29
|
+
redis.sadd script_key(:failed), id
|
30
|
+
end
|
31
|
+
|
32
|
+
def mark_as_run_for(id)
|
33
|
+
redis.sadd script_key(:checked), id
|
34
|
+
end
|
35
|
+
|
36
|
+
def script_key(set_name)
|
37
|
+
"#{@key_prefix}:#{set_name}"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def redis
|
43
|
+
@redis ||= Scripterator.config.redis || Redis.new
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/scripterator/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Ted Dumitrescu"]
|
6
|
+
gem.email = ["ted@lumoslabs.com"]
|
7
|
+
gem.description = %q{Script iterator for ActiveRecord models}
|
8
|
+
gem.summary = %q{DSL for running operations on each of a set of models}
|
9
|
+
gem.homepage = "http://lumosity.com"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
14
|
+
gem.name = "scripterator"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Scripterator::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "activerecord", "~> 3.0"
|
19
|
+
gem.add_dependency "redis", "~> 3.0"
|
20
|
+
|
21
|
+
gem.add_development_dependency "fakeredis", "~> 0.4"
|
22
|
+
gem.add_development_dependency "rspec", "~> 2.13"
|
23
|
+
gem.add_development_dependency "sqlite3", "~> 1.3.8"
|
24
|
+
end
|
data/spec/runner_spec.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Scripterator::Runner do
|
4
|
+
let(:runner) { Scripterator::Runner.new(description, &awesome_script) }
|
5
|
+
let(:description) { 'Convert widgets to eggplant parmigiana' }
|
6
|
+
let(:options) { {start_id: start_id, output_stream: StringIO.new} }
|
7
|
+
let(:start_id) { 1 }
|
8
|
+
|
9
|
+
let(:awesome_script) do
|
10
|
+
Proc.new do
|
11
|
+
before { Widget.before_stuff }
|
12
|
+
for_each_widget { |widget| Widget.transform_a_widget(widget) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { runner.run(options) }
|
17
|
+
|
18
|
+
shared_examples_for 'raises an error' do
|
19
|
+
specify do
|
20
|
+
expect { subject }.to raise_error
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'infers the model from the for_each block' do
|
25
|
+
runner.send(:model_finder).should == Widget
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when a model block is defined' do
|
29
|
+
let(:awesome_script) do
|
30
|
+
Proc.new do
|
31
|
+
model { Widget.where(name: 'bla') }
|
32
|
+
before { Widget.before_stuff }
|
33
|
+
for_each_widget { |widget| Widget.transform_a_widget(widget) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
let!(:widget1) { Widget.create(name: 'foo') }
|
37
|
+
let!(:widget2) { Widget.create(name: 'bla') }
|
38
|
+
|
39
|
+
it 'uses the given model finder code' do
|
40
|
+
Widget.should_receive(:transform_a_widget).once.with(widget2)
|
41
|
+
Widget.should_not_receive(:transform_a_widget).with(widget1)
|
42
|
+
subject
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when no start or end ID is passed' do
|
47
|
+
let(:options) { {} }
|
48
|
+
|
49
|
+
it_behaves_like 'raises an error'
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when no per-record block is defined' do
|
53
|
+
let(:awesome_script) do
|
54
|
+
Proc.new do
|
55
|
+
model { Widget }
|
56
|
+
before { Widget.before_stuff }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it_behaves_like 'raises an error'
|
61
|
+
|
62
|
+
it 'does not run any other given blocks' do
|
63
|
+
Widget.should_not_receive :before_stuff
|
64
|
+
subject rescue nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when there are no records for the specified model' do
|
69
|
+
it 'does not run the per-record block' do
|
70
|
+
Widget.should_not_receive :transform_a_widget
|
71
|
+
subject
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'when there are records for the specified model' do
|
76
|
+
let(:num_widgets) { 3 }
|
77
|
+
|
78
|
+
before { num_widgets.times { Widget.create! } }
|
79
|
+
|
80
|
+
it 'runs the given script blocks' do
|
81
|
+
Widget.should_receive :before_stuff
|
82
|
+
Widget.should_receive(:transform_a_widget).exactly(num_widgets).times
|
83
|
+
subject
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when not all records are checked' do
|
87
|
+
let(:start_id) { Widget.last.id }
|
88
|
+
|
89
|
+
it 'marks only the checked IDs as checked' do
|
90
|
+
subject
|
91
|
+
Scripterator.already_run_for?(description, Widget.first.id).should be_false
|
92
|
+
Scripterator.checked_ids(description).should_not include Widget.first.id
|
93
|
+
Scripterator.already_run_for?(description, Widget.last.id).should be_true
|
94
|
+
Scripterator.checked_ids(description).should include Widget.last.id
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when some records have already been checked' do
|
99
|
+
let(:checked_ids) { [Widget.first.id] }
|
100
|
+
|
101
|
+
before do
|
102
|
+
Scripterator.stub(checked_ids: checked_ids)
|
103
|
+
Scripterator::ScriptRedis.any_instance.stub(already_run_for?: false)
|
104
|
+
Scripterator::ScriptRedis.any_instance.stub(:already_run_for?).with(Widget.first.id).and_return(true)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'only runs the per-record code for unchecked records' do
|
108
|
+
Widget.should_receive(:transform_a_widget).exactly(num_widgets - 1).times
|
109
|
+
subject
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'when the code for some records fails' do
|
114
|
+
before do
|
115
|
+
Widget.stub :transform_a_widget do |widget|
|
116
|
+
raise 'Last widget expl0de' if widget.id == Widget.last.id
|
117
|
+
true
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'marks only the failed IDs as failed' do
|
122
|
+
subject
|
123
|
+
Scripterator.failed_ids(description).should_not include Widget.first.id
|
124
|
+
Scripterator.failed_ids(description).should include Widget.last.id
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when Redis client is set to nil' do
|
129
|
+
before { Scripterator.configure { |config| config.redis = nil } }
|
130
|
+
after { Scripterator.instance_variable_set(:@config, nil) }
|
131
|
+
|
132
|
+
it 'runs without Redis' do
|
133
|
+
expect { subject }.not_to raise_error
|
134
|
+
Scripterator.checked_ids(description).should be_empty
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
|
6
|
+
require 'fakeredis/rspec'
|
7
|
+
require 'scripterator'
|
8
|
+
require 'support/widget'
|
9
|
+
|
10
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
13
|
+
config.run_all_when_everything_filtered = true
|
14
|
+
config.filter_run :focus
|
15
|
+
|
16
|
+
# Run specs in random order to surface order dependencies. If you find an
|
17
|
+
# order dependency and want to debug it, you can fix the order by providing
|
18
|
+
# the seed, which is printed after each run.
|
19
|
+
# --seed 1234
|
20
|
+
config.order = 'random'
|
21
|
+
|
22
|
+
config.around do |example|
|
23
|
+
ActiveRecord::Base.transaction do
|
24
|
+
example.run
|
25
|
+
raise ActiveRecord::Rollback
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
4
|
+
|
5
|
+
ActiveRecord::Migration.create_table :widgets do |t|
|
6
|
+
t.string :name
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
class Widget < ActiveRecord::Base
|
11
|
+
# some dummy methods for setting message expectations in specs
|
12
|
+
def self.before_stuff; end
|
13
|
+
def self.transform_a_widget(widget); true; end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scripterator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ted Dumitrescu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-09-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: fakeredis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.4'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.13'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.13'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.3.8
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.3.8
|
83
|
+
description: Script iterator for ActiveRecord models
|
84
|
+
email:
|
85
|
+
- ted@lumoslabs.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- .gitignore
|
91
|
+
- .rspec
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE
|
94
|
+
- README.md
|
95
|
+
- lib/scripterator.rb
|
96
|
+
- lib/scripterator/configuration.rb
|
97
|
+
- lib/scripterator/runner.rb
|
98
|
+
- lib/scripterator/script_redis.rb
|
99
|
+
- lib/scripterator/version.rb
|
100
|
+
- scripterator.gemspec
|
101
|
+
- spec/runner_spec.rb
|
102
|
+
- spec/spec_helper.rb
|
103
|
+
- spec/support/widget.rb
|
104
|
+
homepage: http://lumosity.com
|
105
|
+
licenses: []
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ! '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.1.4
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: DSL for running operations on each of a set of models
|
127
|
+
test_files:
|
128
|
+
- spec/runner_spec.rb
|
129
|
+
- spec/spec_helper.rb
|
130
|
+
- spec/support/widget.rb
|
131
|
+
has_rdoc:
|