ultra_marathon 0.1.7 → 0.1.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +97 -1
- data/lib/core_ext/class.rb +13 -0
- data/lib/core_ext/extensions.rb +4 -0
- data/lib/core_ext/object.rb +12 -0
- data/lib/core_ext/string.rb +9 -0
- data/lib/ultra_marathon.rb +2 -1
- data/lib/ultra_marathon/abstract_runner.rb +29 -144
- data/lib/ultra_marathon/base_runner.rb +216 -0
- data/lib/ultra_marathon/callbacks.rb +15 -50
- data/lib/ultra_marathon/collection_runner.rb +111 -0
- data/lib/ultra_marathon/contexticution.rb +53 -0
- data/lib/ultra_marathon/instrumentation.rb +32 -9
- data/lib/ultra_marathon/instrumentation/profile.rb +38 -9
- data/lib/ultra_marathon/instrumentation/store.rb +103 -0
- data/lib/ultra_marathon/logging.rb +1 -1
- data/lib/ultra_marathon/store.rb +13 -4
- data/lib/ultra_marathon/sub_context.rb +36 -0
- data/lib/ultra_marathon/sub_runner.rb +116 -60
- data/lib/ultra_marathon/version.rb +1 -1
- data/spec/spec_helper.rb +11 -0
- data/spec/ultra_marathon/abstract_runner_spec.rb +44 -1
- data/spec/ultra_marathon/collection_runner_spec.rb +96 -0
- data/spec/ultra_marathon/instrumentation/profile_spec.rb +5 -4
- data/spec/ultra_marathon/instrumentation/store_spec.rb +73 -0
- data/spec/ultra_marathon/instrumentation_spec.rb +1 -1
- data/spec/ultra_marathon/sub_runner_spec.rb +5 -1
- metadata +15 -1
@@ -20,7 +20,7 @@ module UltraMarathon
|
|
20
20
|
# is set to be the instance variable. Otherwise returns it, defaulting
|
21
21
|
# to the included Logger class
|
22
22
|
def logger_class
|
23
|
-
if @logger_class
|
23
|
+
if @logger_class.respond_to? :call
|
24
24
|
@logger_class = @logger_class.call
|
25
25
|
else
|
26
26
|
@logger_class ||= Logger
|
data/lib/ultra_marathon/store.rb
CHANGED
@@ -21,6 +21,10 @@ module UltraMarathon
|
|
21
21
|
end
|
22
22
|
alias_method :add, :<<
|
23
23
|
|
24
|
+
def merge(other_store)
|
25
|
+
self.store.merge!(other_store.store)
|
26
|
+
end
|
27
|
+
|
24
28
|
def each(&block)
|
25
29
|
runners.each(&block)
|
26
30
|
end
|
@@ -64,6 +68,15 @@ module UltraMarathon
|
|
64
68
|
end
|
65
69
|
end
|
66
70
|
|
71
|
+
protected
|
72
|
+
|
73
|
+
## Protected Instance Methods
|
74
|
+
|
75
|
+
# allow access for another store for merging
|
76
|
+
def store
|
77
|
+
@store ||= Hash.new
|
78
|
+
end
|
79
|
+
|
67
80
|
private
|
68
81
|
|
69
82
|
## Private Instance Methods
|
@@ -71,9 +84,5 @@ module UltraMarathon
|
|
71
84
|
def runners
|
72
85
|
store.values
|
73
86
|
end
|
74
|
-
|
75
|
-
def store
|
76
|
-
@store ||= Hash.new
|
77
|
-
end
|
78
87
|
end
|
79
88
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module UltraMarathon
|
2
|
+
class SubContext
|
3
|
+
include Logging
|
4
|
+
attr_reader :__context
|
5
|
+
|
6
|
+
# Initializes the SubContext and defines #call as the passed in run_block
|
7
|
+
# @param context [Object] the context in which to run the run_block. Any
|
8
|
+
# logging calls will be intercepted to allow threaded execution.
|
9
|
+
# @param run_block [Proc] the block to be run in the given context
|
10
|
+
# @return [self] the initialized SubContext
|
11
|
+
def initialize(context, &run_block)
|
12
|
+
@__context = context
|
13
|
+
# Ruby cannot marshal procs or lambdas, so we need to define a method.
|
14
|
+
# Binding to self allows us to intercept logging calls.
|
15
|
+
define_singleton_method(:call, run_block.bind(self))
|
16
|
+
end
|
17
|
+
|
18
|
+
# If the original context responds, including private methods,
|
19
|
+
# delegate to it
|
20
|
+
#
|
21
|
+
# @param method [Symbol] the method called. If context responds to this
|
22
|
+
# method, it will be called on the context
|
23
|
+
# @param args [Array] the arguments the method was called with
|
24
|
+
# @param block [Proc] proc called with method, if applicable
|
25
|
+
# @raise [NoMethodError] if the context, including private methods, does not
|
26
|
+
# respond to the method called
|
27
|
+
def method_missing(method, *args, &block)
|
28
|
+
|
29
|
+
if __context.respond_to?(method, true)
|
30
|
+
__context.send(method, *args, &block)
|
31
|
+
else
|
32
|
+
raise NoMethodError.new("undefined local variable or method `#{method.to_s}' for #{__context.class.name}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -3,108 +3,164 @@ require 'core_ext/proc'
|
|
3
3
|
require 'ultra_marathon/callbacks'
|
4
4
|
require 'ultra_marathon/instrumentation'
|
5
5
|
require 'ultra_marathon/logging'
|
6
|
+
require 'ultra_marathon/sub_context'
|
6
7
|
|
7
8
|
module UltraMarathon
|
8
9
|
class SubRunner
|
9
10
|
include Callbacks
|
10
11
|
include Instrumentation
|
11
12
|
include Logging
|
12
|
-
attr_accessor :
|
13
|
-
attr_reader :
|
13
|
+
attr_accessor :success, :run_thread
|
14
|
+
attr_reader :options, :name, :run_block_or_sub_context
|
14
15
|
|
15
16
|
callbacks :before_run, :after_run, :on_error, :on_reset
|
16
|
-
|
17
|
+
|
18
|
+
before_run lambda { logger.info "Running '#{name}' SubRunner" }
|
19
|
+
after_run lambda { logger.info sub_context.logger.contents }
|
20
|
+
on_reset lambda { self.run_thread = nil }
|
17
21
|
|
18
22
|
on_error lambda { self.success = false }
|
19
|
-
on_error lambda { |error| logger.error
|
23
|
+
on_error lambda { |error| logger.error(error) }
|
24
|
+
|
25
|
+
instrumentation_prefix lambda { |sub_runner| "sub_runner.#{sub_runner.name}." }
|
20
26
|
|
21
27
|
# The :context option is required, because you'll never want to run a
|
22
28
|
# SubRunner in context of itself.
|
23
29
|
# SubContext is necessary because we want to run in the context of the
|
24
30
|
# other class, but do other things (like log) in the context of this one.
|
25
|
-
|
26
|
-
|
31
|
+
#
|
32
|
+
#
|
33
|
+
# @param options [Hash] the options for instantiation
|
34
|
+
# @option options [String] :name The name of the sub runner
|
35
|
+
# @option options [Object] :context The context in which the run block will
|
36
|
+
# be run. Only required if second parameter is a callable object and not
|
37
|
+
# a SubContext already.
|
38
|
+
# @option options [Boolean] :instrument (false) whether to log
|
39
|
+
# instrumentation information.
|
40
|
+
# @option options [Boolean] :threaded (false) whether to run in a separate
|
41
|
+
# thread.
|
42
|
+
# @option options [Array, Set] :requires ([]) the names of sub runners that
|
43
|
+
# should have successfully run before this one. Not used by this class
|
44
|
+
# but necessary state for enveloping runners.
|
45
|
+
# @param run_block_or_sub_context [Proc, SubContext] either a proc to be run
|
46
|
+
# within a new SubContext
|
47
|
+
def initialize(options, run_block_or_sub_context)
|
48
|
+
@run_block_or_sub_context = run_block_or_sub_context
|
49
|
+
@name = options.delete(:name)
|
27
50
|
@options = {
|
28
|
-
instrument: false
|
51
|
+
instrument: false,
|
52
|
+
threaded: false,
|
53
|
+
requires: Set.new,
|
54
|
+
timeout: 100
|
29
55
|
}.merge(options)
|
30
|
-
@sub_context = SubContext.new(options[:context], run_block)
|
31
56
|
end
|
32
57
|
|
58
|
+
# Run the run block or sub context. If {#threaded?} is true, will
|
59
|
+
# envelope the run in a thread and immediately return. If there is already
|
60
|
+
# an active thread, it will not run again.
|
61
|
+
# @return [self]
|
33
62
|
def run!
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rescue StandardError => error
|
39
|
-
invoke_on_error_callbacks(error)
|
40
|
-
ensure
|
41
|
-
invoke_after_run_callbacks
|
42
|
-
end
|
63
|
+
if threaded?
|
64
|
+
run_in_thread
|
65
|
+
else
|
66
|
+
run_without_thread
|
43
67
|
end
|
44
|
-
|
68
|
+
self
|
45
69
|
end
|
46
70
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
71
|
+
# Tells whether the runner has completed. If running in a threaded context,
|
72
|
+
# checks if the thread is alive. Otherwise, returns true.
|
73
|
+
# @return [Boolean] whether the runner has compeleted running
|
74
|
+
def complete?
|
75
|
+
if threaded?
|
76
|
+
!running?
|
77
|
+
else
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# If {#threaded?}, returns if the run_thread is alive. Otherwise false.
|
83
|
+
# @return [Boolean] whether the SubRunner is currently executing {#run!}
|
84
|
+
def running?
|
85
|
+
if threaded?
|
86
|
+
run_thread && run_thread.alive?
|
87
|
+
else
|
88
|
+
false
|
55
89
|
end
|
56
90
|
end
|
57
91
|
|
92
|
+
# @return [Boolean] whether {#run!} will be executed in a thread.
|
93
|
+
def threaded?
|
94
|
+
!!options[:threaded]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Invokes all on_reset callbacks
|
98
|
+
# @return [self]
|
58
99
|
def reset
|
59
100
|
invoke_on_reset_callbacks
|
101
|
+
self
|
60
102
|
end
|
61
103
|
|
62
|
-
# Set of all sub runners that should be run before this one
|
63
|
-
#
|
64
|
-
#
|
104
|
+
# Set of all sub runners that should be run before this one, as specified
|
105
|
+
# by the :requires option.
|
106
|
+
# @return [Set] set of all runner names that should be run before this one.
|
65
107
|
def parents
|
66
108
|
@parents ||= Set.new(options[:requires])
|
67
109
|
end
|
68
110
|
|
69
111
|
private
|
70
112
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
logger.info log_header
|
78
|
-
log_sub_context
|
79
|
-
end
|
80
|
-
|
81
|
-
def log_sub_context
|
82
|
-
logger.info sub_context.logger.contents
|
113
|
+
# Wraps {#run_without_thread} in a Thread unless {#running?} returns true.
|
114
|
+
# @return [Thread, nil]
|
115
|
+
def run_in_thread
|
116
|
+
unless running?
|
117
|
+
self.run_thread = Thread.new { run_without_thread }
|
118
|
+
end
|
83
119
|
end
|
84
120
|
|
85
|
-
|
86
|
-
|
121
|
+
# Runs the before_run callbacks, then calls sub_context
|
122
|
+
# If an error is raised in the sub_context, it invokes the on_error
|
123
|
+
# callbacks passing in that error
|
124
|
+
# Finally, runs the after_run callbacks whether or not an error was raised
|
125
|
+
# @return [self]
|
126
|
+
def run_without_thread
|
127
|
+
instrument('__run!') do
|
128
|
+
begin
|
129
|
+
self.success = true
|
130
|
+
instrument('callbacks.before_run') { invoke_before_run_callbacks }
|
131
|
+
sub_context.call
|
132
|
+
rescue StandardError => error
|
133
|
+
instrument('callbacks.on_error') { invoke_on_error_callbacks(error) }
|
134
|
+
ensure
|
135
|
+
instrument('callbacks.after_run') { invoke_after_run_callbacks }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
log_instrumentation if options[:instrument]
|
139
|
+
self
|
87
140
|
end
|
88
|
-
end
|
89
|
-
|
90
|
-
class SubContext
|
91
|
-
include Logging
|
92
|
-
attr_reader :context, :run_block
|
93
141
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
142
|
+
# Logs the start time, end time, and total time for {#run!}
|
143
|
+
# @return [void]
|
144
|
+
def log_instrumentation
|
145
|
+
run_profile = instrumentations['__run!']
|
146
|
+
logger.info """
|
147
|
+
Start Time: #{run_profile.formatted_start_time}
|
148
|
+
End Time: #{run_profile.formatted_end_time}
|
149
|
+
Total Time: #{run_profile.formatted_total_time}
|
150
|
+
"""
|
99
151
|
end
|
100
152
|
|
101
|
-
#
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
153
|
+
# Returns the sub context to be called by {#run!}. If initialized with an
|
154
|
+
# instance on SubContext, memoizes to that. Otherwise, creates a new
|
155
|
+
# SubContext with the passed in context and run block
|
156
|
+
# @return [SubContext]
|
157
|
+
def sub_context
|
158
|
+
@sub_context ||= begin
|
159
|
+
if run_block_or_sub_context.is_a? SubContext
|
160
|
+
run_block_or_sub_context
|
161
|
+
else
|
162
|
+
SubContext.new(options[:context], &run_block_or_sub_context)
|
163
|
+
end
|
108
164
|
end
|
109
165
|
end
|
110
166
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'fileutils'
|
1
2
|
require 'awesome_print'
|
2
3
|
require 'timecop'
|
3
4
|
require 'ultra_marathon'
|
@@ -6,3 +7,13 @@ require 'rspec/autorun'
|
|
6
7
|
require 'support/test_helpers'
|
7
8
|
|
8
9
|
include TestHelpers
|
10
|
+
|
11
|
+
# TODO Google this when I have Wifi
|
12
|
+
# TEST_TMP_DIRECTORY = 'test_tmp'.freeze
|
13
|
+
|
14
|
+
# before(:suite) do
|
15
|
+
# Dir.mkdir_p(TEST_TMP_DIRECTORY)
|
16
|
+
# Dir.foreach(TEST_TMP_DIRECTORY) do |file_name|
|
17
|
+
# File.delete(file_name)
|
18
|
+
# end
|
19
|
+
# end
|
@@ -5,10 +5,11 @@ describe UltraMarathon::AbstractRunner do
|
|
5
5
|
let(:test_instance) { test_class.new }
|
6
6
|
|
7
7
|
describe '#run!' do
|
8
|
+
let(:run_options) { Hash.new }
|
8
9
|
subject { test_instance.run! }
|
9
10
|
|
10
11
|
describe 'with one run block' do
|
11
|
-
before(:each) { test_class.send :run, &run_block }
|
12
|
+
before(:each) { test_class.send :run, :main, run_options, &run_block }
|
12
13
|
|
13
14
|
context 'when everything is fine' do
|
14
15
|
let(:run_block) { Proc.new { logger.info 'Look at me go!' } }
|
@@ -52,6 +53,35 @@ describe UltraMarathon::AbstractRunner do
|
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
56
|
+
context 'with a collection' do
|
57
|
+
let(:items) { [1,2,3,4] }
|
58
|
+
let(:run_block) do
|
59
|
+
Proc.new do |n|
|
60
|
+
squares << (n ** 2)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
let(:test_class) do
|
64
|
+
anonymous_test_class(UltraMarathon::AbstractRunner) do
|
65
|
+
def squares
|
66
|
+
@squares ||= []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
before(:each) { test_instance.class.send :run_collection, :main, items, run_options, &run_block }
|
72
|
+
|
73
|
+
it 'should run successfully' do
|
74
|
+
subject
|
75
|
+
test_instance.should be_success
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should execute the run blocks in the context of the test class' do
|
79
|
+
subject
|
80
|
+
test_instance.squares.sort.should eq [1,4,9,16]
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
55
85
|
describe 'with multiple run blocks' do
|
56
86
|
|
57
87
|
context 'when everything is fine' do
|
@@ -119,10 +149,12 @@ describe UltraMarathon::AbstractRunner do
|
|
119
149
|
end
|
120
150
|
end
|
121
151
|
end
|
152
|
+
|
122
153
|
end
|
123
154
|
end
|
124
155
|
|
125
156
|
describe 'callbacks' do
|
157
|
+
let(:run_block) { Proc.new { 7 } }
|
126
158
|
before(:each) { test_class.send :run, &run_block }
|
127
159
|
|
128
160
|
subject { test_instance.run! }
|
@@ -154,6 +186,17 @@ describe UltraMarathon::AbstractRunner do
|
|
154
186
|
test_instance.logger.contents.index('We have liftoff!').should be > test_instance.logger.contents.index('Blastoff!')
|
155
187
|
end
|
156
188
|
end
|
189
|
+
|
190
|
+
describe 'after_initialize callback' do
|
191
|
+
before(:each) do
|
192
|
+
test_class.after_initialize ->{ self.success = 'Pebbles' }
|
193
|
+
test_class.after_initialize ->{ self.success << ' in my shower' }
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'should invoke after_initialize callbacks in order' do
|
197
|
+
test_instance.success.should eq 'Pebbles in my shower'
|
198
|
+
end
|
199
|
+
end
|
157
200
|
end
|
158
201
|
|
159
202
|
describe '#reset' do
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe UltraMarathon::CollectionRunner do
|
5
|
+
let(:test_class) { anonymous_test_class(UltraMarathon::CollectionRunner) }
|
6
|
+
let(:test_instance) { test_class.new(collection, options, &run_block) }
|
7
|
+
let(:collection) { [] }
|
8
|
+
let(:options) { { name: :justice_league } }
|
9
|
+
let(:run_block) { Proc.new { } }
|
10
|
+
|
11
|
+
describe '#run!' do
|
12
|
+
subject(:run_collection) { test_instance.run! }
|
13
|
+
context 'with a single elements' do
|
14
|
+
let(:collection) { [1, 3, 5, 7, 9] }
|
15
|
+
let(:run_block) do
|
16
|
+
proc { |item| logger.info("Chillin with homie #{item}")}
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should run each member of the collection' do
|
20
|
+
run_collection
|
21
|
+
collection.each do |item|
|
22
|
+
sub_log = "Chillin with homie #{item}\n"
|
23
|
+
test_instance.logger.contents.should include sub_log
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when individual elements blow up' do
|
28
|
+
let(:collection) { [1,2,3,4,5] }
|
29
|
+
|
30
|
+
let(:run_block) do
|
31
|
+
proc { |item| raise 'hell' if item % 2 == 0 }
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should not raise an error and set success to false' do
|
35
|
+
expect { run_collection }.to_not raise_error
|
36
|
+
test_instance.success.should be false
|
37
|
+
test_instance.failed_sub_runners.length.should be 2
|
38
|
+
test_instance.successful_sub_runners.length.should be 3
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with multiple arguments' do
|
44
|
+
let(:collection) { [['Cassidy', 'Clay'], ['Tom', 'Jerry']] }
|
45
|
+
let(:run_block) do
|
46
|
+
proc { |homie1, homie2| logger.info("Chillin with homie #{homie1} & #{homie2}") }
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should run each member of the collection' do
|
50
|
+
run_collection
|
51
|
+
collection.each do |(homie1, homie2)|
|
52
|
+
sub_log = "Chillin with homie #{homie1} & #{homie2}\n"
|
53
|
+
test_instance.logger.contents.should include sub_log
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with a different iterator' do
|
59
|
+
let(:number_array_class) do
|
60
|
+
anonymous_test_class(Array) do
|
61
|
+
def each_odd(&block)
|
62
|
+
select(&:odd?).each(&block)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
let(:collection) { number_array_class.new([1,2,3,4,5,6]) }
|
67
|
+
let(:run_block) { proc { |odd_number| logger.info "#{odd_number} is an odd number!" } }
|
68
|
+
let(:options) { { name: :this_is_odd, iterator: :each_odd } }
|
69
|
+
|
70
|
+
it 'should use that iterator' do
|
71
|
+
run_collection
|
72
|
+
test_instance.logger.contents.should_not include "2 is an odd number!"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with a custom naming convention' do
|
77
|
+
let(:options) { { name: :jimmy, sub_name: proc { |index, item| 'Sector' << (item ** 2).to_s } } }
|
78
|
+
let(:collection) { [ 2, 4, 7] }
|
79
|
+
|
80
|
+
it 'should correctly set the sub_runner names as requested' do
|
81
|
+
run_collection
|
82
|
+
test_instance.logger.contents.should include "Running 'Sector49' SubRunner"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'passing threaded: true' do
|
87
|
+
let(:options) { { name: :threaded, threaded: true } }
|
88
|
+
let(:collection) { 0...10 }
|
89
|
+
let(:run_block) { proc { |n| sleep(0.01) } }
|
90
|
+
|
91
|
+
it 'should generate threaded subrunners' do
|
92
|
+
test_instance.send(:unrun_sub_runners).all?(&:threaded?).should be true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|