methodical 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'methodical'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+ config.mock_with :mocha
9
+ end
@@ -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
+