methodical 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/methodical.rb +1 -0
- data/lib/methodical/action_item.rb +139 -0
- data/lib/methodical/checklist.rb +29 -0
- data/lib/methodical/disposition.rb +132 -0
- data/lib/methodical/dsl.rb +111 -0
- data/lib/methodical/executable.rb +61 -0
- data/lib/methodical/modifier.rb +43 -0
- data/lib/methodical/simple_action_item.rb +30 -0
- data/lib/methodical/walkthrough.rb +143 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- data/test/methodical/checklist_test.rb +79 -0
- data/test/methodical/disposition_test.rb +120 -0
- data/test/methodical/dsl_test.rb +217 -0
- data/test/methodical/modifier_test.rb +79 -0
- data/test/methodical/simple_action_item_test.rb +825 -0
- data/test/methodical/walkthrough_test.rb +387 -0
- data/test/test_helper.rb +2 -0
- metadata +147 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
module Methodical
|
2
|
+
module Executable
|
3
|
+
attr_writer :raise_on_error
|
4
|
+
|
5
|
+
# Default exception handling:
|
6
|
+
# * RuntimeErrors are considered failures. The exception will be captured
|
7
|
+
# and not re-raised.
|
8
|
+
# * StandardErrors are considered indicative of a programming error in the
|
9
|
+
# action. The action will be marked as bad and failure recorded; but the
|
10
|
+
# exception will not be re-thrown.
|
11
|
+
# * Exceptions not caught by the other cases are considered fatal errors.
|
12
|
+
# The step will be marked bad and the error recorded, and the exception
|
13
|
+
# will then be re-raised.
|
14
|
+
def execute!(baton=nil, raise_on_error=raise_on_error?)
|
15
|
+
disposition = catch_disposition do
|
16
|
+
call(baton, self)
|
17
|
+
end
|
18
|
+
rescue RuntimeError => error
|
19
|
+
disposition = save_and_return_disposition(:failed, error.message, nil, error)
|
20
|
+
raise if raise_on_error
|
21
|
+
disposition
|
22
|
+
rescue StandardError => error
|
23
|
+
disposition = save_and_return_disposition(:bad, error.message, nil, error)
|
24
|
+
raise if raise_on_error
|
25
|
+
disposition
|
26
|
+
rescue Exception => error
|
27
|
+
save_and_return_disposition(:bad, error.message, nil, error)
|
28
|
+
raise
|
29
|
+
else
|
30
|
+
save_and_return_disposition(
|
31
|
+
disposition.status,
|
32
|
+
disposition.explanation,
|
33
|
+
disposition.result,
|
34
|
+
nil,
|
35
|
+
disposition.details)
|
36
|
+
end
|
37
|
+
|
38
|
+
def catch_disposition
|
39
|
+
result = catch(:methodical_disposition) do
|
40
|
+
yield
|
41
|
+
end
|
42
|
+
Methodical::Disposition(result)
|
43
|
+
end
|
44
|
+
|
45
|
+
def raise_on_error?
|
46
|
+
defined?(@raise_on_error) ? @raise_on_error : false
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def save_and_return_disposition(status, explanation, result, error, details="")
|
52
|
+
details = if details.blank? && error
|
53
|
+
error.backtrace.join("\n")
|
54
|
+
else
|
55
|
+
details
|
56
|
+
end
|
57
|
+
update!(status, explanation, result, error, details)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'methodical/executable'
|
3
|
+
|
4
|
+
module Methodical
|
5
|
+
class Modifier < SimpleDelegator
|
6
|
+
include Executable
|
7
|
+
|
8
|
+
def initialize(name, action_item=nil, &block)
|
9
|
+
@name = name
|
10
|
+
__setobj__(action_item)
|
11
|
+
@block = block
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"<#{@name}>(#{action_item.to_s})"
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(baton=nil, raise_on_error=false)
|
19
|
+
@block.call(action_item, baton)
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(rhs)
|
23
|
+
if self.action_item
|
24
|
+
self.action_item << rhs
|
25
|
+
else
|
26
|
+
self.action_item = rhs
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
if RUBY_VERSION=='1.8.6'
|
32
|
+
def clone
|
33
|
+
the_clone = Object.instance_method(:clone).bind(self).call
|
34
|
+
the_clone.__setobj__(__getobj__.clone)
|
35
|
+
the_clone
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
alias_method :action_item, :__getobj__
|
40
|
+
alias_method :action_item=, :__setobj__
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'methodical/action_item'
|
2
|
+
|
3
|
+
module Methodical
|
4
|
+
class SimpleActionItem < ActionItem
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def initialize(title, callable=nil, &block)
|
8
|
+
unless(!!callable ^ !!block)
|
9
|
+
raise ArgumentError, "Either a callable or a block must be provided"
|
10
|
+
end
|
11
|
+
@block = callable || block
|
12
|
+
super(title)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(baton, step)
|
16
|
+
@block.call(baton, step)
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
self.block.eql?(other.block)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
attr_reader :block
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Methodical
|
5
|
+
class Walkthrough < DelegateClass(Array)
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :checklist
|
9
|
+
attr_reader :next_step_index
|
10
|
+
attr_reader :last_step_index
|
11
|
+
attr_reader :baton
|
12
|
+
attr_reader :index
|
13
|
+
attr_reader :decisive_index
|
14
|
+
|
15
|
+
def_delegators :checklist, :title
|
16
|
+
|
17
|
+
def initialize(checklist, baton=nil)
|
18
|
+
@checklist = checklist
|
19
|
+
@continue = true
|
20
|
+
@baton = baton
|
21
|
+
@index = 0
|
22
|
+
@decisive_index = nil
|
23
|
+
@halted = false
|
24
|
+
@started = false
|
25
|
+
super(Array.new(checklist.map{|ai| ai.clone}))
|
26
|
+
each do |step|
|
27
|
+
step.walkthrough = self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def perform!(baton=@baton, raise_on_error=false, &block)
|
32
|
+
@baton = baton
|
33
|
+
until done?
|
34
|
+
self.next!(baton, raise_on_error, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def next!(baton=@baton, raise_on_error=false)
|
39
|
+
raise "Already performed" if done?
|
40
|
+
|
41
|
+
@started = true
|
42
|
+
|
43
|
+
action_item = fetch(index)
|
44
|
+
yield(self, index, action_item, baton) if block_given?
|
45
|
+
|
46
|
+
execute_or_update_step(action_item, baton, raise_on_error)
|
47
|
+
|
48
|
+
@decisive_index = index if action_item.decisive?
|
49
|
+
@halted = true if action_item.halted?
|
50
|
+
|
51
|
+
yield(self, index, action_item, baton) if block_given?
|
52
|
+
advance!
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"##<#{self.class.name}:#{title}:#{object_id}>"
|
57
|
+
end
|
58
|
+
|
59
|
+
def basic_report
|
60
|
+
inject(""){|report, action_item|
|
61
|
+
report << action_item.synopsis << "\n"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def failed_steps
|
66
|
+
find_all{|step| step.failed?}
|
67
|
+
end
|
68
|
+
|
69
|
+
def decisive_step
|
70
|
+
decided? ? fetch(decisive_index) : nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def status
|
74
|
+
if !started? then :not_started
|
75
|
+
elsif !decided? then :in_progress
|
76
|
+
elsif succeeded? then :succeeded
|
77
|
+
else :failed
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def failed?
|
82
|
+
decided? && !decisive_step.done_and_ok?
|
83
|
+
end
|
84
|
+
|
85
|
+
def succeeded?
|
86
|
+
return true if empty?
|
87
|
+
decided? && decisive_step.done_and_ok?
|
88
|
+
end
|
89
|
+
|
90
|
+
def done?
|
91
|
+
index >= size
|
92
|
+
end
|
93
|
+
alias_method :finished?, :done?
|
94
|
+
|
95
|
+
def decided?
|
96
|
+
!!decisive_index
|
97
|
+
end
|
98
|
+
|
99
|
+
def halted?
|
100
|
+
@halted
|
101
|
+
end
|
102
|
+
|
103
|
+
def in_progress?
|
104
|
+
started? && !done?
|
105
|
+
end
|
106
|
+
|
107
|
+
def started?
|
108
|
+
@started
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def advance!
|
114
|
+
@index += 1
|
115
|
+
if @index >= size
|
116
|
+
@decisive_index ||= (size - 1)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def continue?
|
121
|
+
!done? && !halted?
|
122
|
+
end
|
123
|
+
|
124
|
+
def execute_or_update_step(step, baton, raise_on_error)
|
125
|
+
if decided?
|
126
|
+
step.ignored = true
|
127
|
+
end
|
128
|
+
|
129
|
+
if continue?
|
130
|
+
step.raise_on_error = raise_on_error
|
131
|
+
step.execute!(baton, raise_on_error)
|
132
|
+
else
|
133
|
+
reason = if failed?
|
134
|
+
"Run aborted by prior step"
|
135
|
+
else
|
136
|
+
"Satisfied by prior step"
|
137
|
+
end
|
138
|
+
step.update!(:skipped, reason, nil)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.expand_path("../test_helper", File.dirname(__FILE__))
|
2
|
+
require 'methodical/checklist'
|
3
|
+
require 'methodical/dsl'
|
4
|
+
|
5
|
+
class ActionItemTest < Test::Unit::TestCase
|
6
|
+
include Methodical::DSL
|
7
|
+
|
8
|
+
context "with no action items" do
|
9
|
+
specify "has a title" do
|
10
|
+
it = Methodical::Checklist.new("My Checklist")
|
11
|
+
assert_equal "My Checklist", it.title
|
12
|
+
end
|
13
|
+
|
14
|
+
specify "knows it has no items" do
|
15
|
+
it = Methodical::Checklist.new("My Checklist")
|
16
|
+
assert_equal 0, it.size
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
context "with three action items" do
|
22
|
+
specify "knows it has three items" do
|
23
|
+
it = Methodical::Checklist.new("My Checklist")
|
24
|
+
it << stub("ActionItem 1")
|
25
|
+
it << stub("ActionItem 2")
|
26
|
+
it << stub("ActionItem 3")
|
27
|
+
assert_equal 3, it.size
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "#perform_walkthrough" do
|
32
|
+
specify "returns step list as Walkthrough object" do
|
33
|
+
it = Methodical::Checklist.new("My Checklist")
|
34
|
+
assert_kind_of Methodical::Walkthrough, it.perform_walkthrough!
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "creates, performs, and returns a new walkthrough" do
|
38
|
+
sensor = :unset
|
39
|
+
walkthrough = stub("Walkthrough")
|
40
|
+
it = Methodical::Checklist.new("My Checklist")
|
41
|
+
|
42
|
+
Methodical::Walkthrough.expects(:new).with(it).returns(walkthrough)
|
43
|
+
walkthrough.expects(:perform!).with("BATON", false).yields(42)
|
44
|
+
|
45
|
+
result = it.perform_walkthrough!("BATON") do |*args|
|
46
|
+
sensor = args
|
47
|
+
end
|
48
|
+
assert_equal [42], sensor
|
49
|
+
assert_same walkthrough, result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "#new_walkthrough" do
|
54
|
+
specify "returns a Walkthrough" do
|
55
|
+
it = Methodical::Checklist.new("My Checklist")
|
56
|
+
assert_kind_of Methodical::Walkthrough, it.new_walkthrough
|
57
|
+
end
|
58
|
+
|
59
|
+
specify "populates the returned walkthrough" do
|
60
|
+
it = Methodical::Checklist.new("My Checklist")
|
61
|
+
it << stub("ActionItem 1", :clone => a1 = stub_everything("ActionItem 1b"))
|
62
|
+
it << stub("ActionItem 2", :clone => a2 = stub_everything("ActionItem 2b") )
|
63
|
+
it << stub("ActionItem 3", :clone => a3 = stub_everything("ActionItem 3b"))
|
64
|
+
assert_equal [a1, a2, a3], it.new_walkthrough
|
65
|
+
end
|
66
|
+
|
67
|
+
specify "does not execute the returned walkthrough" do
|
68
|
+
it = Methodical::Checklist.new("My Checklist")
|
69
|
+
it << stub("ActionItem 1", :clone => a1 = stub_everything("ActionItem 1b"))
|
70
|
+
a1.expects(:call).never
|
71
|
+
it.new_walkthrough
|
72
|
+
end
|
73
|
+
|
74
|
+
specify "returns a walkthrough which points back to the checklist" do
|
75
|
+
it = Methodical::Checklist.new("My Checklist")
|
76
|
+
assert_same it, it.new_walkthrough.checklist
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require File.expand_path("../test_helper", File.dirname(__FILE__))
|
2
|
+
require 'methodical/disposition'
|
3
|
+
|
4
|
+
class DispositionTest < Test::Unit::TestCase
|
5
|
+
def self.test_predicate(predicate, truth_table)
|
6
|
+
context "##{predicate}" do
|
7
|
+
truth_table.each_pair do |status, expected|
|
8
|
+
specify "#{expected} when #{status.inspect}" do
|
9
|
+
assert_equal expected, Methodical::Disposition(status, "", nil).send(predicate)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
specify "aliases memento to result" do
|
16
|
+
it = Methodical::Disposition.new([:in_progress, "", nil])
|
17
|
+
it.result = 42
|
18
|
+
assert_equal 42, it.memento
|
19
|
+
it.memento = 24
|
20
|
+
assert_equal 24, it.result
|
21
|
+
end
|
22
|
+
|
23
|
+
test_predicate(:ok?, {
|
24
|
+
:succeeded => true,
|
25
|
+
:sufficient => true,
|
26
|
+
:not_started => true,
|
27
|
+
:in_progress => true,
|
28
|
+
:finish => true,
|
29
|
+
:skipped => true,
|
30
|
+
:failed => false,
|
31
|
+
:bad => false,
|
32
|
+
:abort => false
|
33
|
+
})
|
34
|
+
|
35
|
+
test_predicate(:succeeded?, {
|
36
|
+
:succeeded => true,
|
37
|
+
:sufficient => true,
|
38
|
+
:not_started => false,
|
39
|
+
:in_progress => false,
|
40
|
+
:finish => true,
|
41
|
+
:skipped => false,
|
42
|
+
:failed => false,
|
43
|
+
:bad => false,
|
44
|
+
:abort => false
|
45
|
+
})
|
46
|
+
|
47
|
+
test_predicate(:bad?, {
|
48
|
+
:succeeded => false,
|
49
|
+
:sufficient => false,
|
50
|
+
:not_started => false,
|
51
|
+
:in_progress => false,
|
52
|
+
:finish => false,
|
53
|
+
:skipped => false,
|
54
|
+
:failed => false,
|
55
|
+
:bad => true,
|
56
|
+
:abort => false
|
57
|
+
})
|
58
|
+
|
59
|
+
test_predicate(:done?, {
|
60
|
+
:succeeded => true,
|
61
|
+
:sufficient => true,
|
62
|
+
:not_started => false,
|
63
|
+
:in_progress => false,
|
64
|
+
:finish => true,
|
65
|
+
:skipped => true,
|
66
|
+
:failed => true,
|
67
|
+
:bad => true,
|
68
|
+
:abort => true
|
69
|
+
})
|
70
|
+
|
71
|
+
test_predicate(:skipped?, {
|
72
|
+
:succeeded => false,
|
73
|
+
:sufficient => false,
|
74
|
+
:not_started => false,
|
75
|
+
:in_progress => false,
|
76
|
+
:finish => false,
|
77
|
+
:skipped => true,
|
78
|
+
:failed => false,
|
79
|
+
:bad => false,
|
80
|
+
:abort => false
|
81
|
+
})
|
82
|
+
|
83
|
+
test_predicate(:continuable?, {
|
84
|
+
:succeeded => true,
|
85
|
+
:sufficient => true,
|
86
|
+
:finish => false,
|
87
|
+
:not_started => true,
|
88
|
+
:in_progress => true,
|
89
|
+
:skipped => true,
|
90
|
+
:failed => true,
|
91
|
+
:bad => true,
|
92
|
+
:abort => false
|
93
|
+
})
|
94
|
+
|
95
|
+
test_predicate(:decisive?, {
|
96
|
+
:succeeded => false,
|
97
|
+
:sufficient => true,
|
98
|
+
:finish => true,
|
99
|
+
:not_started => false,
|
100
|
+
:in_progress => false,
|
101
|
+
:skipped => false,
|
102
|
+
:failed => true,
|
103
|
+
:bad => true,
|
104
|
+
:abort => true
|
105
|
+
})
|
106
|
+
|
107
|
+
test_predicate(:done_and_ok?, {
|
108
|
+
:succeeded => true,
|
109
|
+
:sufficient => true,
|
110
|
+
:finish => true,
|
111
|
+
:not_started => false,
|
112
|
+
:in_progress => false,
|
113
|
+
:skipped => true,
|
114
|
+
:failed => false,
|
115
|
+
:bad => false,
|
116
|
+
:abort => false
|
117
|
+
})
|
118
|
+
|
119
|
+
end
|
120
|
+
|