once-ler 0.0.1
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.
- data/README.md +105 -0
- data/lib/once-ler.rb +1 -0
- data/lib/onceler.rb +5 -0
- data/lib/onceler/ambitious_helpers.rb +39 -0
- data/lib/onceler/around_all.rb +32 -0
- data/lib/onceler/basic_helpers.rb +90 -0
- data/lib/onceler/blank_tape.rb +55 -0
- data/lib/onceler/configuration.rb +19 -0
- data/lib/onceler/extensions/active_record.rb +3 -0
- data/lib/onceler/recorder.rb +127 -0
- metadata +88 -0
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# once-ler
|
2
|
+
|
3
|
+
once-ler supercharges your `let`s and `before`s with the performance
|
4
|
+
of `before(:all)`. You get the performance of fixtures without all the
|
5
|
+
headaches.
|
6
|
+
|
7
|
+
## Setup
|
8
|
+
|
9
|
+
Add it to your Gemfile
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem "once-ler"
|
13
|
+
```
|
14
|
+
|
15
|
+
And then in spec_helper.rb (or wherever):
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.include Onceler::BasicHelpers
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
## Basic usage
|
24
|
+
|
25
|
+
### before(:once) { ... }
|
26
|
+
|
27
|
+
Change a slow `before` to `before(:once)` to speed it up.
|
28
|
+
|
29
|
+
### let_once(...) { ... }
|
30
|
+
|
31
|
+
Change a slow `let` (or `let!`) to `let_once` to speed it up.
|
32
|
+
|
33
|
+
## Ambitious usage
|
34
|
+
|
35
|
+
If you're feeling bold, you can automatically speed up all
|
36
|
+
`let`s/`before(:each)`s in an example group:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
describe "something" do
|
40
|
+
onceler!
|
41
|
+
let(:foo) { ... } # behaves like let_once
|
42
|
+
before { ... } # behaves like before(:once)
|
43
|
+
before(:all) { ... } # no change here though
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Or even more ambitiously, apply it to all your specs:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
RSpec.configure do |c|
|
51
|
+
c.onceler!
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
## How does it work?
|
56
|
+
|
57
|
+
Any `before(:once)`/`let_once` blocks will run just once for the current
|
58
|
+
context/describe block, before any of its examples run. Any side effects
|
59
|
+
(ivars) and return values will be recorded, and will then be reapplied
|
60
|
+
before each spec in the block runs. Once-ler uses nested transactions
|
61
|
+
(savepoints) to ensure that specs don't mess with each other's database
|
62
|
+
rows.
|
63
|
+
|
64
|
+
This can give you a dramatic speedup, since you can minimize the number
|
65
|
+
of activerecord callbacks/inserts/updates.
|
66
|
+
|
67
|
+
## Caveats
|
68
|
+
|
69
|
+
* Your once'd blocks should have no side effects other than database
|
70
|
+
statements, return values, and instance variables.
|
71
|
+
* Your return values and instance variables need to be able to handle a
|
72
|
+
Marshal.dump/load round trip.
|
73
|
+
* Your once'd blocks' behavior should not depend on side effects of other
|
74
|
+
non-once'd blocks. For example:
|
75
|
+
* a `before(:once)` block should not reference instance variables set by a
|
76
|
+
`before` (but the inverse is fine).
|
77
|
+
* a `let_once` block should not call non-once'd `let`s or `subject`s.
|
78
|
+
* Because all `let_once`s will be recorded and replayed (even if not used
|
79
|
+
in a particular example), you should ensure they don't conflict with
|
80
|
+
each other (e.g. unique constraint violations, or one `let_once`
|
81
|
+
mutating the return value of another).
|
82
|
+
* Some effort is made to preserve object identity, but just for instance
|
83
|
+
variables and return values, e.g.:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
let_once(:user) { User.new }
|
87
|
+
let_once(:users) { [user] }
|
88
|
+
|
89
|
+
before(:once) do
|
90
|
+
@joe = user
|
91
|
+
@also_joe = @joe
|
92
|
+
@joe_ish = [@joe]
|
93
|
+
end
|
94
|
+
|
95
|
+
# within an example:
|
96
|
+
# user == @joe => true
|
97
|
+
# user.equal? @joe => true # yay
|
98
|
+
# user == @also_joe => true
|
99
|
+
# user.equal? @also_joe => true # yay
|
100
|
+
# user == users[0] => true
|
101
|
+
# user.equal? users[0] => false # d'oh
|
102
|
+
# user == @joe_ish[0] => true
|
103
|
+
# user.equal? @joe_ish[0] => false # d'oh
|
104
|
+
```
|
105
|
+
|
data/lib/once-ler.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'onceler'
|
data/lib/onceler.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Onceler
|
2
|
+
module AmbitiousHelpers
|
3
|
+
def before_once?(type)
|
4
|
+
super || type == :each || type.nil?
|
5
|
+
end
|
6
|
+
|
7
|
+
def let(name, &block)
|
8
|
+
let_once(name, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO NamedSubjectPreventSuper
|
12
|
+
def subject(name = nil, &block)
|
13
|
+
subject_once(name, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
# remove auto-before'ing of ! methods, since we memoize our own way
|
17
|
+
def let!(name, &block)
|
18
|
+
let(name, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def subject!(name = nil, &block)
|
22
|
+
subject(name, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
# make sure we have access to subsequently added methods when
|
26
|
+
# recording (not just `lets'). note that this really only works
|
27
|
+
# for truly functional methods with no external dependencies. e.g.
|
28
|
+
# methods that add stubs or set instance variables will not work
|
29
|
+
# while recording
|
30
|
+
def method_added(method_name)
|
31
|
+
return if method_name == @current_let_once
|
32
|
+
onceler = onceler(:create)
|
33
|
+
proxy = onceler.helper_proxy ||= new
|
34
|
+
onceler.helper_methods[method_name] ||= Proc.new do |*args|
|
35
|
+
proxy.send method_name, *args
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# adapted from https://gist.github.com/myronmarston/2005175
|
2
|
+
require 'delegate'
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
module Onceler
|
6
|
+
module AroundAll
|
7
|
+
class FiberAwareGroup < SimpleDelegator
|
8
|
+
def run_examples
|
9
|
+
Fiber.yield
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_proc
|
13
|
+
proc { run_examples }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def around_all(&block)
|
18
|
+
fibers = []
|
19
|
+
prepend_before(:all) do |group|
|
20
|
+
fiber = Fiber.new(&block)
|
21
|
+
fibers << fiber
|
22
|
+
fiber.resume(FiberAwareGroup.new(group))
|
23
|
+
end
|
24
|
+
|
25
|
+
after(:all) do |group|
|
26
|
+
fiber = fibers.pop
|
27
|
+
fiber.resume if fiber.alive?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "onceler/ambitious_helpers"
|
2
|
+
require "onceler/around_all"
|
3
|
+
require "onceler/recorder"
|
4
|
+
|
5
|
+
module Onceler
|
6
|
+
module BasicHelpers
|
7
|
+
def onceler
|
8
|
+
self.class.onceler
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.included(mod)
|
12
|
+
mod.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
include AroundAll
|
17
|
+
|
18
|
+
def let_once(name, &block)
|
19
|
+
raise "#let or #subject called without a block" if block.nil?
|
20
|
+
onceler(:create)[name] = block
|
21
|
+
@current_let_once = name
|
22
|
+
define_method(name) { onceler[name] }
|
23
|
+
end
|
24
|
+
|
25
|
+
def subject_once(name = nil, &block)
|
26
|
+
name ||= :subject
|
27
|
+
let_once(name, &block)
|
28
|
+
alias_method :subject, name if name != :subject
|
29
|
+
end
|
30
|
+
|
31
|
+
def before_once(&block)
|
32
|
+
onceler(:create) << block
|
33
|
+
end
|
34
|
+
|
35
|
+
def before_once?(type)
|
36
|
+
type == :once
|
37
|
+
end
|
38
|
+
|
39
|
+
def before(*args, &block)
|
40
|
+
if before_once?(args.first)
|
41
|
+
before_once(&block)
|
42
|
+
else
|
43
|
+
super(*args, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def onceler(create_own = false)
|
48
|
+
if create_own
|
49
|
+
@onceler ||= create_onceler!
|
50
|
+
else
|
51
|
+
@onceler || parent_onceler
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_onceler!
|
56
|
+
add_onceler_hooks!
|
57
|
+
Recorder.new(parent_onceler)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def parent_onceler
|
63
|
+
return unless superclass.respond_to?(:onceler)
|
64
|
+
superclass.onceler
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_onceler_hooks!
|
68
|
+
around_all do |group|
|
69
|
+
# TODO: configurable transaction fu (say, if you have multiple
|
70
|
+
# conns)
|
71
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
72
|
+
group.onceler.record!
|
73
|
+
group.run_examples
|
74
|
+
raise ActiveRecord::Rollback
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# only the outer-most group needs to do this
|
78
|
+
unless parent_onceler
|
79
|
+
register_hook :append, :before, :each do
|
80
|
+
onceler.replay_into!(self)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def onceler!
|
86
|
+
extend AmbitiousHelpers
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Onceler
|
2
|
+
class BlankTape
|
3
|
+
def initialize(modules)
|
4
|
+
modules.each { |mod| extend mod }
|
5
|
+
@__retvals = {}
|
6
|
+
@__retvals_recorded = {} # we might override an inherited one, so we need to differentiate
|
7
|
+
end
|
8
|
+
|
9
|
+
def __prepare_recording(recording)
|
10
|
+
method = recording.name
|
11
|
+
define_singleton_method(method) do
|
12
|
+
if @__retvals_recorded[method]
|
13
|
+
@__retvals[method]
|
14
|
+
else
|
15
|
+
@__retvals_recorded[method] = true
|
16
|
+
@__retvals[method] = __record(recording)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def __record(recording)
|
22
|
+
instance_eval(&recording.block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def __ivars
|
26
|
+
ivars = instance_variables - [:@__retvals, :@__retvals_recorded]
|
27
|
+
ivars.inject({}) do |hash, key|
|
28
|
+
val = instance_variable_get(key)
|
29
|
+
hash[key] = val
|
30
|
+
hash
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def __data
|
35
|
+
[__ivars, @__retvals]
|
36
|
+
end
|
37
|
+
|
38
|
+
def copy(mixins)
|
39
|
+
copy = self.class.new(mixins)
|
40
|
+
copy.copy_from(self)
|
41
|
+
copy
|
42
|
+
end
|
43
|
+
|
44
|
+
def copy_from(other)
|
45
|
+
ivars, @__retvals = Marshal.load(Marshal.dump(other.__data))
|
46
|
+
ivars.each do |key, value|
|
47
|
+
instance_variable_set(key, value)
|
48
|
+
end
|
49
|
+
@__retvals.each do |key, value|
|
50
|
+
define_singleton_method(key) { value }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Onceler
|
2
|
+
def self.configuration
|
3
|
+
@configuration ||= Configuration.new
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.configure
|
7
|
+
yield configuration
|
8
|
+
end
|
9
|
+
|
10
|
+
class Configuration
|
11
|
+
def modules
|
12
|
+
@modules ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def include(mod)
|
16
|
+
modules << mod
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require "onceler/blank_tape"
|
2
|
+
|
3
|
+
module Onceler
|
4
|
+
class Recorder
|
5
|
+
attr_accessor :tape, :helper_proxy
|
6
|
+
|
7
|
+
def initialize(parent)
|
8
|
+
@parent = parent
|
9
|
+
@recordings = []
|
10
|
+
@named_recordings = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(block)
|
14
|
+
@recordings << Recording.new(block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(name, block)
|
18
|
+
@named_recordings << name
|
19
|
+
@recordings << NamedRecording.new(block, name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](name)
|
23
|
+
@retvals[name]
|
24
|
+
end
|
25
|
+
|
26
|
+
def record!
|
27
|
+
@tape = @parent ? @parent.tape.copy(mixins) : BlankTape.new(mixins)
|
28
|
+
proxy_recordable_methods!
|
29
|
+
|
30
|
+
# we don't know the order named recordings will be called (or if
|
31
|
+
# they'll call each other), so prep everything first
|
32
|
+
@recordings.each do |recording|
|
33
|
+
recording.prepare_medium!(@tape)
|
34
|
+
end
|
35
|
+
@recordings.each do |recording|
|
36
|
+
recording.record_onto!(@tape)
|
37
|
+
end
|
38
|
+
@data = Marshal.dump(@tape.__data)
|
39
|
+
end
|
40
|
+
|
41
|
+
def proxy_recordable_methods!
|
42
|
+
# the proxy is used to run non-recordable methods that may be called
|
43
|
+
# by ones are recording. since the former could in turn call more of
|
44
|
+
# the latter, we need to proxy the other way too
|
45
|
+
return unless helper_proxy
|
46
|
+
methods = @named_recordings
|
47
|
+
reverse_proxy = @tape
|
48
|
+
helper_proxy.instance_eval do
|
49
|
+
methods.each do |method|
|
50
|
+
define_singleton_method(method) { reverse_proxy.send(method) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def helper_methods
|
56
|
+
@helper_methods ||= {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def mixins
|
60
|
+
mixins = (@parent ? @parent.mixins : Onceler.configuration.modules).dup
|
61
|
+
if methods = @helper_methods
|
62
|
+
mixin = Module.new do
|
63
|
+
methods.each do |key, method|
|
64
|
+
define_method(key, &method)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
mixins.push mixin
|
68
|
+
end
|
69
|
+
mixins
|
70
|
+
end
|
71
|
+
|
72
|
+
def reconsitute_data!
|
73
|
+
@ivars, @retvals = Marshal.load(@data)
|
74
|
+
identity_map = {}
|
75
|
+
reidentify!(@ivars, identity_map)
|
76
|
+
reidentify!(@retvals, identity_map)
|
77
|
+
end
|
78
|
+
|
79
|
+
def reidentify!(hash, identity_map)
|
80
|
+
hash.each do |key, value|
|
81
|
+
if identity_map.key?(value)
|
82
|
+
hash[key] = identity_map[value]
|
83
|
+
else
|
84
|
+
identity_map[value] = value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def replay_into!(instance)
|
90
|
+
reconsitute_data!
|
91
|
+
@ivars.each do |key, value|
|
92
|
+
instance.instance_variable_set(key, value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Recording
|
98
|
+
attr_reader :block
|
99
|
+
|
100
|
+
def initialize(block)
|
101
|
+
@block = block
|
102
|
+
end
|
103
|
+
|
104
|
+
def prepare_medium!(tape); end
|
105
|
+
|
106
|
+
def record_onto!(tape)
|
107
|
+
tape.__record(self)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class NamedRecording < Recording
|
112
|
+
attr_reader :name
|
113
|
+
|
114
|
+
def initialize(block, name = nil)
|
115
|
+
super(block)
|
116
|
+
@name = name
|
117
|
+
end
|
118
|
+
|
119
|
+
def prepare_medium!(tape)
|
120
|
+
tape.__prepare_recording(self)
|
121
|
+
end
|
122
|
+
|
123
|
+
def record_onto!(tape)
|
124
|
+
tape.send(@name)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: once-ler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jon Jensen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-06-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.14'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.14'
|
46
|
+
description: once-ler supercharges your let's and before's with the performance of
|
47
|
+
before(:all)
|
48
|
+
email: jon@instructure.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- README.md
|
54
|
+
- lib/once-ler.rb
|
55
|
+
- lib/onceler/ambitious_helpers.rb
|
56
|
+
- lib/onceler/around_all.rb
|
57
|
+
- lib/onceler/basic_helpers.rb
|
58
|
+
- lib/onceler/blank_tape.rb
|
59
|
+
- lib/onceler/configuration.rb
|
60
|
+
- lib/onceler/extensions/active_record.rb
|
61
|
+
- lib/onceler/recorder.rb
|
62
|
+
- lib/onceler.rb
|
63
|
+
homepage: http://github.com/instructure/onceler
|
64
|
+
licenses: []
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.9.3
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 1.3.5
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.8.23
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: rspec supercharger
|
87
|
+
test_files: []
|
88
|
+
has_rdoc:
|