ultra_marathon 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,40 @@
1
+ require 'forwardable'
2
+ require 'ultra_marathon/logger'
3
+ require 'active_support/concern'
4
+
5
+ module UltraMarathon
6
+ module Logging
7
+ extend ActiveSupport::Concern
8
+
9
+ ## Private Instance Methods
10
+
11
+ def logger
12
+ @logger ||= self.class.logger_class.new
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ ## Public Class Methods
18
+
19
+ # If the instance variable is callable, the result of invoking that block
20
+ # is set to be the instance variable. Otherwise returns it, defaulting
21
+ # to the included Logger class
22
+ def logger_class
23
+ if @logger_class && @logger_class.respond_to?(:call)
24
+ @logger_class = @logger_class.call
25
+ else
26
+ @logger_class ||= Logger
27
+ end
28
+ end
29
+
30
+ ## Private Class Methods
31
+
32
+ private
33
+
34
+ # Sets the log class. Can take a callable object or class
35
+ def log_class(log_class)
36
+ @logger_class = log_class
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,79 @@
1
+ require 'set'
2
+ require 'forwardable'
3
+
4
+ module UltraMarathon
5
+ class Store
6
+ include Enumerable
7
+ extend Forwardable
8
+
9
+ def_delegators :store, :[], :[]=, :delete, :empty?, :length, :size
10
+
11
+ ## Public Instance Methods
12
+
13
+ def initialize(new_runners=[])
14
+ new_runners.each do |new_runner|
15
+ add(new_runner)
16
+ end
17
+ end
18
+
19
+ def <<(runner)
20
+ store[runner.name] = runner
21
+ end
22
+ alias_method :add, :<<
23
+
24
+ def each(&block)
25
+ runners.each(&block)
26
+ end
27
+
28
+ def pluck(&block)
29
+ runners.map(&block)
30
+ end
31
+
32
+ def names
33
+ Set.new(store.keys)
34
+ end
35
+
36
+ def includes_all?(query_names)
37
+ (Set.new(query_names) - self.names).empty?
38
+ end
39
+
40
+ # When determining attributes, the user should always check for existence.
41
+ # If they don't, return nil.
42
+ def success?(name)
43
+ if exists? name
44
+ store[name].success
45
+ end
46
+ end
47
+
48
+ def failed?(name)
49
+ if exists? name
50
+ !success? name
51
+ end
52
+ end
53
+
54
+ def exists?(name)
55
+ store.key? name
56
+ end
57
+ alias_method :exist?, :exists?
58
+
59
+ def ==(other)
60
+ if other.is_a? self.class
61
+ other.names == self.names
62
+ else
63
+ false
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ ## Private Instance Methods
70
+
71
+ def runners
72
+ store.values
73
+ end
74
+
75
+ def store
76
+ @store ||= Hash.new
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,93 @@
1
+ require 'set'
2
+ require 'active_support/core_ext/proc'
3
+ require 'ultra_marathon/callbacks'
4
+ require 'ultra_marathon/logging'
5
+
6
+ module UltraMarathon
7
+ class SubRunner
8
+ include Callbacks
9
+ include Logging
10
+ attr_accessor :run_block, :success
11
+ attr_reader :sub_context, :options, :name
12
+
13
+ callbacks :before_run, :after_run, :after_all, :on_error, :on_reset
14
+ after_all :log_header_and_sub_context
15
+
16
+ on_error lambda { self.success = false }
17
+ on_error lambda { |error| logger.error error }
18
+
19
+ # The :context option is required, because you'll never want to run a
20
+ # SubRunner in context of itself.
21
+ # SubContext is necessary because we want to run in the context of the
22
+ # other class, but do other things (like log) in the context of this one.
23
+ def initialize(options, run_block)
24
+ @name = options[:name]
25
+ @options = options
26
+ @sub_context = SubContext.new(options[:context], run_block)
27
+ end
28
+
29
+ def run!
30
+ begin
31
+ self.success = true
32
+ run_sub_context
33
+ rescue StandardError => error
34
+ invoke_on_error_callbacks(error)
35
+ ensure
36
+ invoke_after_all_callbacks
37
+ end
38
+ end
39
+
40
+ def reset
41
+ invoke_on_reset_callbacks
42
+ end
43
+
44
+ # Set of all sub runners that should be run before this one.
45
+ # This class cannot do anything with this information, but it is useful
46
+ # to the enveloping runner.
47
+ def parents
48
+ @parents ||= Set.new(options[:requires])
49
+ end
50
+
51
+ private
52
+
53
+ def run_sub_context
54
+ invoke_before_run_callbacks
55
+ sub_context.call
56
+ invoke_after_run_callbacks
57
+ end
58
+
59
+ def log_header_and_sub_context
60
+ logger.info log_header
61
+ log_sub_context
62
+ end
63
+
64
+ def log_sub_context
65
+ logger.info sub_context.logger.contents
66
+ end
67
+
68
+ def log_header
69
+ "Running '#{name}' SubRunner"
70
+ end
71
+ end
72
+
73
+ class SubContext
74
+ include Logging
75
+ attr_reader :context, :run_block
76
+
77
+ def initialize(context, run_block)
78
+ @context = context
79
+ # Ruby cannot marshal procs or lambdas, so we need to define a method.
80
+ # Binding to self allows us to intercept logging calls.
81
+ define_singleton_method :call, &run_block.bind(self)
82
+ end
83
+
84
+ # If the original context responds, delegate to it
85
+ def method_missing(method, *args, &block)
86
+ if context.respond_to? method
87
+ context.send(method, *args, &block)
88
+ else
89
+ super
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,18 @@
1
+ module UltraMarathon
2
+ class Version
3
+ MAJOR = 0 unless defined? MAJOR
4
+ MINOR = 0 unless defined? MINOR
5
+ PATCH = 1 unless defined? PATCH
6
+ PRE = nil unless defined? PRE
7
+
8
+ class << self
9
+
10
+ # @return [String]
11
+ def to_s
12
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ require 'ultra_marathon/abstract_runner'
2
+ require 'ultra_marathon/callbacks'
3
+ require 'ultra_marathon/instrumentation'
4
+ require 'ultra_marathon/logging'
5
+ require 'ultra_marathon/sub_runner'
6
+ require 'ultra_marathon/store'
7
+
8
+ module UltraMarathon
9
+
10
+ end
@@ -0,0 +1,8 @@
1
+ require 'awesome_print'
2
+ require 'timecop'
3
+ require 'ultra_marathon'
4
+ require 'rspec'
5
+ require 'rspec/autorun'
6
+ require 'support/test_helpers'
7
+
8
+ include TestHelpers
@@ -0,0 +1,41 @@
1
+ require 'fileutils'
2
+ module TestHelpers
3
+
4
+ def wait_for_lock(mutex)
5
+ ensure_path_to_mutex(mutex)
6
+ total_wait_time = 0.0
7
+ while File.exists?(mutex_path(mutex))
8
+ sleep(0.1)
9
+ total_wait_time += 0.1
10
+ if total_wait_time > 60 * 5
11
+ raise "Took too long to obtain a lock"
12
+ end
13
+ end
14
+ File.open(mutex_path(mutex), "w") {}
15
+ end
16
+
17
+ def release_lock(mutex)
18
+ File.delete(mutex_path(mutex)) if File.exists? mutex_path(mutex)
19
+ end
20
+
21
+ private
22
+
23
+ # returns the mutext path, making sure to only dump crap in tmp/
24
+ def mutex_path(mutex)
25
+ if mutex.start_with? 'tmp'
26
+ mutex
27
+ else
28
+ 'tmp/' << mutex
29
+ end
30
+ end
31
+
32
+ # Given a mutex 'log/maintenance/walrus_maintenance.log',
33
+ # ensures that 'tmp/log/maintenance/' directory exists
34
+ def ensure_path_to_mutex(mutex)
35
+ path = mutex_path(mutex)
36
+ if path =~ /\Atmp\/.+\/([^\/]+)\z/
37
+ FileUtils.mkdir_p path[0...-$1.length]
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,11 @@
1
+ require 'support/file_mutexes'
2
+
3
+ module TestHelpers
4
+
5
+ # create an anonymous test class so we don't pollute the global namespace
6
+ def anonymous_test_class(inherited_class=Object, &block)
7
+ Class.new(inherited_class).tap do |klass|
8
+ klass.class_eval(&block) if block_given?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,161 @@
1
+ require 'spec_helper'
2
+
3
+ describe UltraMarathon::AbstractRunner do
4
+ let(:test_class) { anonymous_test_class(UltraMarathon::AbstractRunner) }
5
+ let(:test_instance) { test_class.new }
6
+
7
+ describe '#run!' do
8
+ subject { test_instance.run! }
9
+
10
+ describe 'with one run block' do
11
+ before(:each) { test_class.send :run, &run_block }
12
+
13
+ context 'when everything is fine' do
14
+ let(:run_block) { Proc.new { logger.info 'Look at me go!' } }
15
+
16
+ it 'should run the given block in the context of the instance' do
17
+ subject
18
+ test_instance.logger.contents.should include "Look at me go!\n"
19
+ end
20
+
21
+ it 'should be a success' do
22
+ subject
23
+ test_instance.success.should be
24
+ end
25
+ end
26
+
27
+ context 'when things go wrong' do
28
+ let(:run_block) { Proc.new { raise 'Benoit!' } }
29
+
30
+ it 'should catch the error' do
31
+ expect { subject }.to_not raise_error
32
+ end
33
+
34
+ it 'should logger.info the error message' do
35
+ subject
36
+ test_instance.logger.contents.should include 'Benoit!'
37
+ end
38
+
39
+ it 'should set success to false' do
40
+ subject
41
+ test_instance.success.should_not be
42
+ end
43
+ end
44
+ end
45
+
46
+ describe 'with multiple run blocks' do
47
+
48
+ context 'when everything is fine' do
49
+
50
+ before(:each) do
51
+ test_class.send(:run, &->{ logger.info 'I am the Walrus' })
52
+ test_class.send(:run, :yolo, &->{ logger.info 'Koo koo ka choo' })
53
+ end
54
+
55
+ it 'should run both blocks' do
56
+ subject
57
+ test_instance.logger.contents.should include 'I am the Walrus'
58
+ test_instance.logger.contents.should include 'Koo koo ka choo'
59
+ end
60
+ end
61
+
62
+ context 'when one goes awry' do
63
+ before(:each) do
64
+ test_class.send(:run, :walrus, &->{ raise 'I am the Walrus' })
65
+ test_class.send(:run, :wat, &->{ logger.info 'Koo koo ka choo' })
66
+ end
67
+
68
+ it 'should still run the other' do
69
+ subject
70
+ test_instance.logger.contents.should include 'I am the Walrus'
71
+ test_instance.logger.contents.should include 'Koo koo ka choo'
72
+ end
73
+ end
74
+
75
+ context 'when one run relies on another' do
76
+ context 'when everything is okay' do
77
+ before(:each) do
78
+ test_class.send(:run, :wat, requires: [:walrus], &->{ logger.info 'Koo koo ka choo' })
79
+ test_class.send(:run, :walrus, &->{ logger.info 'I am the Walrus' })
80
+ end
81
+
82
+ it 'should run both blocks' do
83
+ subject
84
+ test_instance.logger.contents.should include 'I am the Walrus'
85
+ test_instance.logger.contents.should include 'Koo koo ka choo'
86
+ end
87
+
88
+ it 'should run the parents before the children' do
89
+ subject
90
+ walrus_log_position = test_instance.logger.contents.index('I am the Walrus')
91
+ wat_log_position = test_instance.logger.contents.index('Koo koo ka choo')
92
+ walrus_log_position.should be < wat_log_position
93
+ end
94
+ end
95
+
96
+ context 'when the parent fails' do
97
+ before(:each) do
98
+ test_class.send(:run, :wat, requires: [:walrus], &->{ logger.info 'Koo koo ka choo' })
99
+ test_class.send(:run, :walrus, &->{ raise 'I am the Walrus!' })
100
+ end
101
+
102
+ it 'should logger.info the error' do
103
+ subject
104
+ test_instance.logger.contents.should include 'I am the Walrus!'
105
+ end
106
+
107
+ it 'should not run the child' do
108
+ subject
109
+ test_instance.logger.contents.should_not include 'Koo koo ka choo'
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ describe 'callbacks' do
117
+ before(:each) { test_class.send :run, &run_block }
118
+
119
+ subject { test_instance.run! }
120
+
121
+ describe 'before_run callback' do
122
+ let(:run_block) { Proc.new { logger.info 'Blastoff!' } }
123
+
124
+ before(:each) do
125
+ test_class.before_run ->{ logger.info '3-2-1' }
126
+ end
127
+
128
+ it 'should invoke before_run callbacks before run!' do
129
+ subject
130
+ test_instance.logger.contents.should include '3-2-1'
131
+ test_instance.logger.contents.index('3-2-1').should be < test_instance.logger.contents.index('Blastoff!')
132
+ end
133
+ end
134
+
135
+ describe 'after_run callback' do
136
+ let(:run_block) { Proc.new { logger.info 'Blastoff!' } }
137
+
138
+ before(:each) do
139
+ test_class.after_run ->{ logger.info 'We have liftoff!' }
140
+ end
141
+
142
+ it 'should invoke before_run callbacks before run!' do
143
+ subject
144
+ test_instance.logger.contents.should include 'We have liftoff!'
145
+ test_instance.logger.contents.index('We have liftoff!').should be > test_instance.logger.contents.index('Blastoff!')
146
+ end
147
+ end
148
+ end
149
+
150
+ describe '#reset' do
151
+ it 'should change success to true' do
152
+ test_instance.success = false
153
+ test_instance.reset
154
+ test_instance.success.should be
155
+ end
156
+
157
+ it 'returns itself' do
158
+ test_instance.reset.should be test_instance
159
+ end
160
+ end
161
+ end