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