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.
@@ -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