methodical 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,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