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