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.
@@ -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 && @logger_class.respond_to?(:call)
23
+ if @logger_class.respond_to? :call
24
24
  @logger_class = @logger_class.call
25
25
  else
26
26
  @logger_class ||= Logger
@@ -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 :run_block, :success
13
- attr_reader :sub_context, :options, :name
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
- after_run :log_header_and_sub_context
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 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
- def initialize(options, run_block)
26
- @name = options[:name]
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
- instrument(:run) do
35
- begin
36
- self.success = true
37
- run_sub_context
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
- log_instrumentation
68
+ self
45
69
  end
46
70
 
47
- def log_instrumentation
48
- if options[:instrument]
49
- run_profile = instrumentations[:run]
50
- logger.info """
51
- End Time: #{run_profile.formatted_end_time}
52
- Start Time: #{run_profile.formatted_start_time}
53
- Total Time: #{run_profile.formatted_total_time}
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
- # This class cannot do anything with this information, but it is useful
64
- # to the enveloping runner.
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
- def run_sub_context
72
- invoke_before_run_callbacks
73
- sub_context.call
74
- end
75
-
76
- def log_header_and_sub_context
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
- def log_header
86
- "Running '#{name}' SubRunner"
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
- def initialize(context, run_block)
95
- @context = context
96
- # Ruby cannot marshal procs or lambdas, so we need to define a method.
97
- # Binding to self allows us to intercept logging calls.
98
- define_singleton_method(:call, &run_block.bind(self))
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
- # If the original context responds, including private methods,
102
- # delegate to it
103
- def method_missing(method, *args, &block)
104
- if context.respond_to?(method, true)
105
- context.send(method, *args, &block)
106
- else
107
- raise NoMethodError.new("undefined local variable or method `#{method.to_s}' for #{context.class.name}")
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
@@ -2,7 +2,7 @@ module UltraMarathon
2
2
  class Version
3
3
  MAJOR = 0 unless defined? MAJOR
4
4
  MINOR = 1 unless defined? MINOR
5
- PATCH = 7 unless defined? PATCH
5
+ PATCH = 10 unless defined? PATCH
6
6
  PRE = nil unless defined? PRE
7
7
 
8
8
  class << self
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