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.
@@ -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
+
@@ -0,0 +1 @@
1
+ require 'onceler'
@@ -0,0 +1,5 @@
1
+ require "rspec"
2
+ require "onceler/basic_helpers"
3
+ require "onceler/configuration"
4
+ require "onceler/extensions/active_record"
5
+
@@ -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,3 @@
1
+ module ActiveRecord::TestFixtures
2
+ def teardown_fixtures; end # we manage it ourselves
3
+ 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: