ultra_marathon 0.1.7 → 0.1.10
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 +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
|