methodical 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Avdi Grimm
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,17 @@
1
+ = methodical
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Avdi Grimm. See LICENSE for details.
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "methodical"
8
+ gem.summary = %Q{Automation framework for sequential operations}
9
+ gem.description = %Q{Sorry, no description yet}
10
+ gem.email = "avdi@avdi.org"
11
+ gem.homepage = "http://github.com/avdi/methodical"
12
+ gem.authors = ["Avdi Grimm"]
13
+ gem.add_dependency "extlib", "~> 0.9.14"
14
+ gem.add_dependency "arrayfields", "~> 4.7"
15
+ gem.add_development_dependency "rspec", ">= 1.2.9"
16
+ gem.add_development_dependency "mocha", "~> 0.9.8"
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'spec/rake/spectask'
25
+ Spec::Rake::SpecTask.new(:spec) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.spec_files = FileList['spec/**/*_spec.rb']
28
+ spec.spec_files += FileList['test/**/*_test.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :spec => :check_dependencies
38
+
39
+ task :default => :spec
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "methodical #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1 @@
1
+ require 'extlib'
@@ -0,0 +1,139 @@
1
+ require 'forwardable'
2
+ require 'methodical/disposition'
3
+ require 'methodical/executable'
4
+
5
+ module Methodical
6
+ class ActionItem
7
+ include Executable
8
+ extend Forwardable
9
+
10
+ attr_reader :title
11
+ attr_reader :error
12
+ attr_writer :ignored
13
+ attr_reader :disposition
14
+ attr_accessor :walkthrough
15
+
16
+ def_delegators :disposition,
17
+ :status,
18
+ :status=,
19
+ :explanation,
20
+ :explanation=,
21
+ :result,
22
+ :result=,
23
+ :error,
24
+ :error=,
25
+ :details,
26
+ :details=,
27
+ :succeeded?,
28
+ :failed?,
29
+ :bad?,
30
+ :done?,
31
+ :halted?,
32
+ :done_and_ok?
33
+
34
+ def initialize(title)
35
+ @title = title
36
+ @ignored = false
37
+ @disposition = Disposition.new([:not_started, "", nil])
38
+ @raise_on_error = false
39
+ end
40
+
41
+ def to_s
42
+ synopsis
43
+ end
44
+
45
+ def synopsis
46
+ "#{title}: #{human_status}" +
47
+ (explanation.blank? ? "." : " (#{explanation})#{ignored_suffix}.")
48
+ end
49
+
50
+ def inspect
51
+ "##<#{self.class.name}:#{title}:#{object_id}>"
52
+ end
53
+
54
+ def human_status
55
+ case status
56
+ when :failed, :abort then "Failed"
57
+ when :bad then "Error"
58
+ when :succeeded, :sufficient, :finish then "OK"
59
+ when :in_progress then "In progress"
60
+ when :not_started then "Not started"
61
+ when :skipped then "Skipped"
62
+ else raise "Invalid status #{status.inspect}"
63
+ end
64
+ end
65
+
66
+ def ignored?
67
+ @ignored
68
+ end
69
+
70
+ def relevant?
71
+ !ignored?
72
+ end
73
+
74
+ def continue?
75
+ disposition.continuable?
76
+ end
77
+
78
+ def decisive?
79
+ !ignored? && disposition.decisive?
80
+ end
81
+
82
+ def update!(status, explanation, result, error=nil, details="")
83
+ self.status = status
84
+ self.explanation = explanation
85
+ self.result = result
86
+ self.error = error
87
+ self.details = details
88
+ disposition
89
+ end
90
+
91
+ # Disposition methods
92
+ def succeed!(explanation="", result=nil, details="")
93
+ throw(:methodical_disposition,
94
+ Methodical::Disposition(:succeeded, explanation, result, nil, details))
95
+ end
96
+
97
+ def fail!(explanation="", result=nil, error=nil, details="")
98
+ throw(:methodical_disposition,
99
+ Methodical::Disposition(:failed, explanation, result, error, details))
100
+ end
101
+
102
+ def skip!(explanation="", details="")
103
+ throw(:methodical_disposition,
104
+ Methodical::Disposition(:skipped, explanation, nil, nil, details))
105
+ end
106
+
107
+ def checkpoint!(explanation="", memento=nil, details="")
108
+ throw(:methodical_disposition,
109
+ Methodical::Disposition(:in_progress, explanation, memento, nil, details))
110
+ end
111
+
112
+ def sufficient!(explanation="", result=nil, details="")
113
+ throw(:methodical_disposition,
114
+ Methodical::Disposition(:sufficient, explanation, result, nil, details))
115
+ end
116
+
117
+ def finish!(explanation="", result=nil, details="")
118
+ throw(:methodical_disposition,
119
+ Methodical::Disposition(:finish, explanation, result, nil, details))
120
+ end
121
+
122
+ def abort!(explanation="", result=nil, error=nil, details="")
123
+ throw(:methodical_disposition,
124
+ Methodical::Disposition(:abort, explanation, result, error, details))
125
+ end
126
+
127
+ protected
128
+
129
+ private
130
+
131
+ def ignored_suffix
132
+ if ignored? && failed?
133
+ " (Ignored)"
134
+ else
135
+ ""
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,29 @@
1
+ require 'delegate'
2
+ require 'methodical/walkthrough'
3
+
4
+ module Methodical
5
+ class Checklist < DelegateClass(Array)
6
+ attr_reader :title
7
+
8
+ def initialize(title)
9
+ @title = title
10
+ super([])
11
+ end
12
+
13
+ def new_walkthrough
14
+ Walkthrough.new(self)
15
+ end
16
+
17
+ def perform_walkthrough!(baton=nil, raise_on_error=false, &block)
18
+ walkthrough = new_walkthrough
19
+ walkthrough.perform!(baton, raise_on_error, &block)
20
+ walkthrough
21
+ end
22
+
23
+ def <<(object)
24
+ raise ArgumentError, "No nils allowed" if object.nil?
25
+ super(object)
26
+ object
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,132 @@
1
+ require 'arrayfields'
2
+
3
+ module Methodical
4
+ def self.Disposition(*args)
5
+ if args.size == 1
6
+ object = args.first
7
+ if Disposition === object
8
+ object
9
+ elsif object.kind_of?(Array) &&
10
+ object.size >= 2 &&
11
+ object[0].kind_of?(Symbol) &&
12
+ object[1].kind_of?(String)
13
+ Disposition.new(object)
14
+ else
15
+ Disposition.new([:succeeded, "", object])
16
+ end
17
+ else
18
+ Disposition.new(args)
19
+ end
20
+ end
21
+
22
+ Disposition = Array.struct :status, :explanation, :result, :error, :details
23
+
24
+ # A Disposition represents the status of an ActionItem (step)
25
+ #
26
+ # Explanation of statuses:
27
+ #
28
+ # * not_started: The action has not yet been performed
29
+ # * in_progress: The action has been started, and has not finished.
30
+ # * succeeded: The action succeeded. If all actions succeed, the walkthrough
31
+ # is considered a success.
32
+ # * failed: The action failed. The walkthrough will fail unless the
33
+ # "ignored" flag was set; but the walkthrough will not be
34
+ # halted.
35
+ # * sufficient: The action succeeded. Later steps will be executed, but
36
+ # the walkthrough will be a success even if there are later
37
+ # failures.
38
+ # * finish: The action succeeded. No more steps will be performed.
39
+ # * abort: The action failed, and no more steps will be performed.
40
+ # * bad: An error occured outside of the range of any expected failure
41
+ # modes. The walkthrough will continue, but will be marked as
42
+ # failed.
43
+ # * skipped: The action was skipped. The "explanation" field should
44
+ # contain the reason for skipping the action.
45
+ class Disposition
46
+ VALID_STATUSES = [
47
+ :not_started,
48
+ :in_progress,
49
+ :succeeded,
50
+ :failed,
51
+ :sufficient,
52
+ :finish,
53
+ :abort,
54
+ :bad,
55
+ :skipped
56
+ ]
57
+
58
+ alias_method :base_initialize, :initialize
59
+ def initialize(*args)
60
+ base_initialize(*args)
61
+ self.details ||= ""
62
+ validate!
63
+ end
64
+
65
+ alias_method :memento, :result
66
+ alias_method :memento=, :result=
67
+
68
+ def ok?
69
+ !failed?
70
+ end
71
+
72
+ def failed?
73
+ [:failed, :bad, :abort].include?(status)
74
+ end
75
+
76
+ def succeeded?
77
+ [:succeeded, :sufficient, :finish].include?(status)
78
+ end
79
+
80
+ def bad?
81
+ status == :bad
82
+ end
83
+
84
+ def skipped?
85
+ status == :skipped
86
+ end
87
+
88
+ def done?
89
+ succeeded? || failed? || skipped?
90
+ end
91
+
92
+ def continuable?
93
+ !halted?
94
+ end
95
+
96
+ def halted?
97
+ status == :abort || status == :finish
98
+ end
99
+
100
+ def decisive?
101
+ [:sufficient, :finish, :failed, :bad, :abort].include?(status)
102
+ end
103
+
104
+ def done_and_ok?
105
+ done? && ok?
106
+ end
107
+
108
+ def merge(params)
109
+ params.inject(self.class.new(self)) {|d, (k,v)| d[k] = v; d}
110
+ end
111
+
112
+ private
113
+
114
+ def validate!
115
+ unless VALID_STATUSES.include?(status)
116
+ raise ArgumentError, "Invalid status #{status.inspect}"
117
+ end
118
+ unless explanation.kind_of?(String)
119
+ raise ArgumentError, "Explanation must be a String"
120
+ end
121
+ if result.kind_of?(Exception)
122
+ raise ArgumentError, "Result must not be an Exception"
123
+ end
124
+ if error && !error.kind_of?(Exception)
125
+ raise ArgumentError, "Error must be an Exception"
126
+ end
127
+ unless details.kind_of?(String)
128
+ raise ArgumentError, "Details must be a String"
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,111 @@
1
+ require 'methodical/simple_action_item'
2
+ require 'methodical/modifier'
3
+
4
+ module Methodical
5
+ module DSL
6
+ def action(title, &block)
7
+ SimpleActionItem.new(title, &block)
8
+ end
9
+
10
+ def sufficient
11
+ Modifier.new("Sufficient") do |action_item, baton|
12
+ disposition = action_item.execute!(baton)
13
+ if(disposition.status == :succeeded)
14
+ disposition.merge(:status => :sufficient)
15
+ else
16
+ disposition
17
+ end
18
+ end
19
+ end
20
+
21
+ def requisite
22
+ Modifier.new("Requisite") do |action_item, baton|
23
+ disposition = action_item.execute!(baton)
24
+ if(disposition.failed?)
25
+ disposition.merge(:status => :abort)
26
+ else
27
+ disposition
28
+ end
29
+ end
30
+ end
31
+
32
+ def skip_if(reason, &block)
33
+ Modifier.new("Skip if #{reason}") do |action_item, baton|
34
+ if block.call(baton, action_item)
35
+ action_item.skip!(reason)
36
+ else
37
+ action_item.call(baton, action_item)
38
+ end
39
+ end
40
+ end
41
+
42
+ def handle_error(error_type, &block)
43
+ Modifier.new("Handle error #{error_type}") do |action_item, baton|
44
+ begin
45
+ action_item.execute!(baton, true)
46
+ rescue error_type => error
47
+ block.call(baton, action_item, error)
48
+ end
49
+ end
50
+ end
51
+
52
+ def recover_failure
53
+ Modifier.new("Recover from failure") do |action_item, baton|
54
+ disposition = action_item.execute!(baton)
55
+ if disposition.status == :failed
56
+ yield(baton, action_item, disposition)
57
+ end
58
+ end
59
+ end
60
+
61
+ def ignore
62
+ Modifier.new("Ignore failures") do |action_item, baton|
63
+ action_item.ignored=true
64
+ action_item.call(baton, action_item)
65
+ end
66
+ end
67
+
68
+ # Filter and optionally modify step disposition
69
+ def filter(&block)
70
+ Modifier.new("Filter disposition") do |action_item, baton|
71
+ block.call(action_item.execute!(baton))
72
+ end
73
+ end
74
+
75
+ # TODO Factor this out into its own class, it's a bit big
76
+ # TODO we may want to roll this functionality into the core Checklist. It
77
+ # would be nice if a retried action would actually show up in the log, e.g.:
78
+ # 8. Do some work (Failed; Retrying)
79
+ # 8. Do some work (Succeeded)
80
+ def retry_on_failure(
81
+ times_to_retry=1, time_limit_in_seconds=:none, options={})
82
+ max_tries = times_to_retry.to_i + 1
83
+ clock = options.fetch(:clock) { Time }
84
+ cutoff_time = if time_limit_in_seconds == :none
85
+ :none
86
+ else
87
+ clock.now + time_limit_in_seconds
88
+ end
89
+ description =
90
+ "Retry #{times_to_retry} times"
91
+ unless time_limit_in_seconds == :none
92
+ description << " or #{time_limit_in_seconds} seconds"
93
+ end
94
+ Modifier.new(description) do
95
+ |action_item, baton|
96
+ tries = 0
97
+ begin
98
+ disposition = action_item.execute!(baton)
99
+ tries += 1
100
+ if disposition.failed? &&
101
+ (cutoff_time != :none) &&
102
+ (clock.now >= cutoff_time)
103
+ break
104
+ end
105
+ end until disposition.succeeded? || tries >= max_tries
106
+ disposition
107
+ end
108
+ end
109
+
110
+ end
111
+ end