ntl-orchestra 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +11 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +539 -0
  6. data/Rakefile +21 -0
  7. data/bin/rake +16 -0
  8. data/lib/orchestra/conductor.rb +119 -0
  9. data/lib/orchestra/configuration.rb +12 -0
  10. data/lib/orchestra/dsl/nodes.rb +72 -0
  11. data/lib/orchestra/dsl/object_adapter.rb +134 -0
  12. data/lib/orchestra/dsl/operations.rb +108 -0
  13. data/lib/orchestra/errors.rb +44 -0
  14. data/lib/orchestra/node/output.rb +61 -0
  15. data/lib/orchestra/node.rb +130 -0
  16. data/lib/orchestra/operation.rb +49 -0
  17. data/lib/orchestra/performance.rb +137 -0
  18. data/lib/orchestra/recording.rb +83 -0
  19. data/lib/orchestra/run_list.rb +171 -0
  20. data/lib/orchestra/thread_pool.rb +163 -0
  21. data/lib/orchestra/util.rb +98 -0
  22. data/lib/orchestra/version.rb +3 -0
  23. data/lib/orchestra.rb +35 -0
  24. data/orchestra.gemspec +26 -0
  25. data/test/examples/fizz_buzz.rb +32 -0
  26. data/test/examples/invitation_service.rb +118 -0
  27. data/test/integration/multithreading_test.rb +38 -0
  28. data/test/integration/recording_telemetry_test.rb +86 -0
  29. data/test/integration/replayable_operation_test.rb +53 -0
  30. data/test/lib/console.rb +103 -0
  31. data/test/lib/test_runner.rb +19 -0
  32. data/test/support/telemetry_recorder.rb +49 -0
  33. data/test/test_helper.rb +16 -0
  34. data/test/unit/conductor_test.rb +25 -0
  35. data/test/unit/dsl_test.rb +122 -0
  36. data/test/unit/node_test.rb +122 -0
  37. data/test/unit/object_adapter_test.rb +100 -0
  38. data/test/unit/operation_test.rb +224 -0
  39. data/test/unit/run_list_test.rb +131 -0
  40. data/test/unit/thread_pool_test.rb +105 -0
  41. data/test/unit/util_test.rb +20 -0
  42. data/tmp/.keep +0 -0
  43. metadata +159 -0
@@ -0,0 +1,122 @@
1
+ class DSLTest < Minitest::Test
2
+ def test_failing_to_supply_perform_block
3
+ error = assert_raises ArgumentError do
4
+ Orchestra::Node::InlineNode.build do
5
+ provides :foo
6
+ depends_on :bar
7
+ end
8
+ end
9
+
10
+ assert_equal "expected inline node to define a perform block", error.message
11
+ end
12
+
13
+ def test_two_nodes_one_name
14
+ error = assert_raises ArgumentError do
15
+ Orchestra.define do
16
+ node :foo do
17
+ depends_on :bar
18
+ perform do bar + bar end
19
+ end
20
+ node :foo do
21
+ depends_on :qux
22
+ perform do qux * qux end
23
+ end
24
+ end
25
+ end
26
+
27
+ assert_equal "There are duplicate nodes named :foo", error.message
28
+ end
29
+
30
+ def test_result_node
31
+ operation = Orchestra.define do
32
+ result :foo do perform do 'foo' end end
33
+ end
34
+ assert_equal :foo, operation.result
35
+
36
+ error = assert_raises ArgumentError do
37
+ operation = Orchestra.define do
38
+ result do perform do 'foo' end end
39
+ end
40
+ end
41
+ assert_equal "Could not infer name for node from a provision", error.message
42
+
43
+ operation = Orchestra.define do
44
+ result do
45
+ provides :foo
46
+ perform do 'foo' end
47
+ end
48
+ end
49
+ assert_equal :foo, operation.result
50
+ end
51
+
52
+ def test_command_operations_using_finally
53
+ operation = Orchestra.define do
54
+ node :unnecessary do
55
+ provides :baz
56
+ perform do raise "Can't get here" end
57
+ end
58
+
59
+ node :necessary do
60
+ depends_on :baz
61
+ provides :bar
62
+ perform do baz + 1 end
63
+ end
64
+
65
+ finally do
66
+ depends_on :bar
67
+ perform do bar * 2 end
68
+ end
69
+ end
70
+
71
+ test_observer = Module.new do
72
+ extend self
73
+ attr :result
74
+ def update event, *args
75
+ return unless event == :operation_exited
76
+ _, @result = args
77
+ end
78
+ end
79
+
80
+ conductor = Orchestra::Conductor.new
81
+ conductor.add_observer test_observer
82
+
83
+ assert_equal nil, conductor.perform(operation, :baz => 3)
84
+ assert_equal 8, test_observer.result
85
+ end
86
+
87
+ def test_modifies
88
+ operation = Orchestra.define do
89
+ result do
90
+ modifies :list
91
+ perform do list << :foo end
92
+ end
93
+ end
94
+
95
+ ary = []
96
+ Orchestra.perform operation, :list => ary
97
+
98
+ assert_equal [:foo], ary
99
+ end
100
+
101
+ def test_must_supply_result
102
+ error = assert_raises ArgumentError do
103
+ Orchestra.define do
104
+ node :foo do
105
+ perform do 'foo' end
106
+ end
107
+ end
108
+ end
109
+
110
+ assert_equal "Must supply a result", error.message
111
+ end
112
+
113
+ def test_must_contain_at_least_one_node
114
+ error = assert_raises ArgumentError do
115
+ Orchestra.define do
116
+ self.result = :foo
117
+ end
118
+ end
119
+
120
+ assert_equal "Must supply at least one node", error.message
121
+ end
122
+ end
@@ -0,0 +1,122 @@
1
+ class NodeTest < Minitest::Test
2
+ def test_performing_a_node
3
+ node = build_simple_node
4
+
5
+ assert_equal(
6
+ { :bar => 4 },
7
+ node.perform(:foo => 2, :bar => 2),
8
+ )
9
+ end
10
+
11
+ def test_providing_a_single_hash
12
+ node = Orchestra::Node::InlineNode.new(
13
+ :dependencies => [:foo],
14
+ :provides => [:bar],
15
+ :perform_block => lambda { { :bar => (foo * 2) } },
16
+ )
17
+
18
+ assert_equal(
19
+ { :bar => 4 },
20
+ node.perform(:foo => 2),
21
+ )
22
+ end
23
+
24
+ def test_providing_a_single_hash_that_is_not_the_output
25
+ node = Orchestra::Node::InlineNode.new(
26
+ :dependencies => [:foo],
27
+ :provides => [:bar],
28
+ :perform_block => lambda { { :baz => (foo * 2) } },
29
+ )
30
+
31
+ assert_equal(
32
+ { :bar => { :baz => 4 } },
33
+ node.perform(:foo => 2),
34
+ )
35
+ end
36
+
37
+ def test_performing_a_collection_node
38
+ node = Orchestra::Node::InlineNode.new(
39
+ :dependencies => [:foo],
40
+ :provides => [:bar],
41
+ :perform_block => lambda { |e| e * 2 },
42
+ :collection => :foo,
43
+ )
44
+
45
+ assert_equal(
46
+ { :bar => [2, 4, 6, 8] },
47
+ node.perform(:foo => [1, 2, 3, 4]),
48
+ )
49
+ end
50
+
51
+ def test_defaulting
52
+ node = build_simple_node
53
+
54
+ assert_equal(
55
+ { :bar => 8 },
56
+ node.perform(:foo => 2),
57
+ )
58
+ end
59
+
60
+ def test_introspecting_dependencies
61
+ node = build_simple_node
62
+
63
+ assert_equal [:foo, :bar], node.dependencies
64
+ end
65
+
66
+ def test_introspecting_mandatory_dependencies
67
+ node = build_simple_node
68
+
69
+ assert_equal [:foo], node.required_dependencies
70
+ end
71
+
72
+ def test_node_fails_to_supply_provisions
73
+ node = Orchestra::Node::InlineNode.new(
74
+ :provides => [:foo, :bar, :baz],
75
+ :perform_block => lambda { nil },
76
+ )
77
+
78
+ error = assert_raises Orchestra::MissingProvisionError do node.perform end
79
+
80
+ assert_equal(
81
+ "failed to supply output: :foo, :bar and :baz",
82
+ error.message,
83
+ )
84
+ end
85
+
86
+ def test_cannot_return_nil
87
+ node = Orchestra::Node::InlineNode.new(
88
+ :provides => [:foo],
89
+ :perform_block => lambda do nil end
90
+ )
91
+
92
+ error = assert_raises Orchestra::MissingProvisionError do node.perform end
93
+
94
+ assert_equal(
95
+ "failed to supply output: :foo",
96
+ error.message,
97
+ )
98
+ end
99
+
100
+ def test_node_provides_extra_provisions
101
+ node = Orchestra::Node::InlineNode.new(
102
+ :provides => [:foo],
103
+ :perform_block => lambda do { :foo => :bar, :baz => :qux } end,
104
+ )
105
+
106
+ assert_equal(
107
+ { :foo => :bar },
108
+ node.perform,
109
+ )
110
+ end
111
+
112
+ private
113
+
114
+ def build_simple_node
115
+ Orchestra::Node::InlineNode.new(
116
+ :defaults => { :bar => lambda { 4 } },
117
+ :dependencies => [:foo, :bar],
118
+ :provides => [:bar],
119
+ :perform_block => lambda { foo * bar },
120
+ )
121
+ end
122
+ end
@@ -0,0 +1,100 @@
1
+ class ObjectAdapterTest < Minitest::Test
2
+ def setup
3
+ @builder = Orchestra::DSL::Operations::Builder.new
4
+ end
5
+
6
+ def test_method_does_not_exist_on_singleton
7
+ error = assert_raises NotImplementedError do
8
+ @builder.add_node Splitter, :provides => :words, :method => :foo
9
+ end
10
+
11
+ assert_equal "ObjectAdapterTest::Splitter does not implement method `foo'", error.message
12
+ end
13
+
14
+ def test_method_does_not_exist_on_object
15
+ error = assert_raises NotImplementedError do
16
+ @builder.add_node Upcaser, :provides => :words
17
+ end
18
+
19
+ assert_equal "ObjectAdapterTest::Upcaser does not implement instance method `perform'", error.message
20
+ end
21
+
22
+ def test_dependencies_inferred_from_method_defaults
23
+ node = @builder.add_node Upcaser, :iterates_over => :words, :provides => :upcased_words, :method => :call
24
+
25
+ assert_equal [:words, :transform], node.dependencies
26
+ assert_equal [:words], node.required_dependencies
27
+ end
28
+
29
+ def test_performing_an_operation_with_integrated_objects
30
+ operation = Orchestra.define do
31
+ node Splitter, :provides => :words
32
+ node Upcaser, :iterates_over => :words, :provides => :upcased_words, :method => :call
33
+ node Bolder, :iterates_over => :upcased_words, :provides => :bolded_words, :method => :call
34
+ node Joiner, :method => :join
35
+ self.result = :joiner
36
+ end
37
+
38
+ result = Orchestra.perform(
39
+ operation,
40
+ :sentence => "the quick brown fox jumps over the lazy dog",
41
+ :bold_text => "*",
42
+ )
43
+
44
+ assert_equal(
45
+ %(*THE* *QUICK* *BROWN* *FOX* *JUMPS* *OVER* *THE* *LAZY* *DOG*),
46
+ result,
47
+ )
48
+ end
49
+
50
+ def test_provent_singleton_objects_from_handling_collections
51
+ error = assert_raises ArgumentError do
52
+ Orchestra.define do
53
+ node Splitter, :iterates_over => :sentence
54
+ end
55
+ end
56
+
57
+ assert_equal(
58
+ "ObjectAdapterTest::Splitter is a singleton; cannot iterate over collection :sentence",
59
+ error.message
60
+ )
61
+ end
62
+
63
+ module Splitter
64
+ def self.perform sentence
65
+ sentence.split %r{[[:space:]]+}
66
+ end
67
+ end
68
+
69
+ class Upcaser
70
+ def initialize transform = :upcase
71
+ @transform = transform
72
+ end
73
+
74
+ def call element
75
+ element.public_send @transform
76
+ end
77
+ end
78
+
79
+ class Bolder
80
+ attr :bold_text
81
+
82
+ def initialize bold_text = "**"
83
+ @bold_text = bold_text
84
+ end
85
+
86
+ def call word
87
+ "#{bold_text}#{word}#{bold_text}"
88
+ end
89
+ end
90
+
91
+ class Joiner
92
+ def initialize bolded_words
93
+ @bolded_words = bolded_words
94
+ end
95
+
96
+ def join
97
+ @bolded_words.join ' '
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,224 @@
1
+ module OperationTest
2
+ class PerformTest < Minitest::Test
3
+ def test_simple_operation
4
+ operation = build_simple_operation
5
+
6
+ assert_equal(
7
+ %(THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG),
8
+ operation.perform(:sentence => %(the quick brown fox jumps over the lazy dog)),
9
+ )
10
+ end
11
+
12
+ def test_performing_operation_without_inputs
13
+ operation = build_simple_operation
14
+
15
+ error = assert_raises Orchestra::MissingInputError do
16
+ operation.perform
17
+ end
18
+
19
+ assert_equal %(Missing input :sentence), error.message
20
+ end
21
+
22
+ def test_mutating_inputs
23
+ operation = build_mutator
24
+
25
+ shopping_list = [%(1 clove garlic)]
26
+ operation.perform :shopping_list => shopping_list
27
+
28
+ assert_equal(
29
+ [%(1 clove garlic), %(2 bunches of carrots), %(1 stalk of celery), %(3 yellow onions)],
30
+ shopping_list,
31
+ )
32
+ end
33
+
34
+ def test_skipping_unnecessary_steps
35
+ operation = build_simple_operation
36
+
37
+ assert_equal(
38
+ %(THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG),
39
+ operation.perform(:upcased_word_list => %w(THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG)),
40
+ )
41
+ end
42
+
43
+ def test_passing_conductor_into_nodes
44
+ conductor = Orchestra::Conductor.new
45
+
46
+ node = Orchestra::Node::InlineNode.build do
47
+ depends_on :conductor
48
+ provides :conductor_id
49
+ perform do conductor.object_id end
50
+ end
51
+
52
+ assert_equal conductor.object_id, node.perform[:conductor_id]
53
+ end
54
+
55
+ def test_missing_input_errors
56
+ operation = Orchestra.define do
57
+ node :foo do
58
+ depends_on :bar
59
+ perform do bar + bar end
60
+ end
61
+ node :baz do
62
+ depends_on :qux
63
+ perform do qux * qux end
64
+ end
65
+ node :result do
66
+ depends_on :foo, :baz
67
+ perform do baz - foo end
68
+ end
69
+ self.result = :result
70
+ end
71
+
72
+ error = assert_raises Orchestra::MissingInputError do
73
+ Orchestra.perform operation, :bar => nil
74
+ end
75
+
76
+ assert_equal "Missing inputs :bar and :qux", error.message
77
+ end
78
+
79
+ private
80
+
81
+ def build_simple_operation
82
+ Orchestra.define do
83
+ node :split do
84
+ depends_on :sentence
85
+ provides :word_list
86
+ perform do sentence.split %r{[[:space:]]+} end
87
+ end
88
+
89
+ node :upcase do
90
+ depends_on :word_list
91
+ provides :upcased_word_list
92
+ perform do word_list.map &:upcase end
93
+ end
94
+
95
+ node :join do
96
+ depends_on :upcased_word_list
97
+ perform do upcased_word_list.join ' ' end
98
+ end
99
+
100
+ self.result = :join
101
+ end
102
+ end
103
+
104
+ def build_mutator
105
+ Orchestra.define do
106
+ node :carrots do
107
+ depends_on :shopping_list
108
+ provides :shopping_list
109
+ perform do shopping_list << "2 bunches of carrots" end
110
+ end
111
+
112
+ node :celery do
113
+ depends_on :shopping_list
114
+ provides :shopping_list
115
+ perform do shopping_list << "1 stalk of celery" end
116
+ end
117
+
118
+ node :onions do
119
+ depends_on :shopping_list
120
+ provides :shopping_list
121
+ perform do shopping_list << "3 yellow onions" end
122
+ end
123
+
124
+ self.result = :shopping_list
125
+ end
126
+ end
127
+
128
+ end
129
+
130
+ class IntrospectionTest < Minitest::Test
131
+ def test_introspecting_dependencies
132
+ node = Orchestra::Node::InlineNode.build do
133
+ depends_on :foo, :bar => :baz
134
+ provides :baz
135
+ perform do :noop end
136
+ end
137
+
138
+ assert_equal [:foo, :bar], node.dependencies
139
+ end
140
+
141
+ def test_introspecting_optional_dependencies
142
+ node = Orchestra::Node::InlineNode.build do
143
+ depends_on :foo, :bar => :baz
144
+ provides :qux
145
+ perform do :noop end
146
+ end
147
+
148
+ assert_equal [:bar], node.optional_dependencies
149
+ end
150
+
151
+ def test_introspecting_mandatory_dependencies
152
+ node = Orchestra::Node::InlineNode.build do
153
+ depends_on :foo, :bar => :baz
154
+ provides :baz
155
+ perform do :noop end
156
+ end
157
+
158
+ assert_equal [:foo], node.required_dependencies
159
+ end
160
+
161
+ end
162
+
163
+ class EmbeddingOperationsTest < Minitest::Test
164
+ def test_embedding_operations
165
+ inner = Orchestra.define do
166
+ node :double do
167
+ depends_on :number
168
+ provides :doubled
169
+ perform do number * 2 end
170
+ end
171
+
172
+ result :plus_one do
173
+ depends_on :doubled
174
+ perform do doubled + 1 end
175
+ end
176
+ end
177
+
178
+ outer = Orchestra.define do
179
+ node inner
180
+
181
+ result :squared do
182
+ depends_on :plus_one
183
+ perform do plus_one ** 2 end
184
+ end
185
+ end
186
+
187
+ telemetry = {}
188
+
189
+ conductor = Orchestra::Conductor.new
190
+ conductor.add_observer TelemetryRecorder.new telemetry
191
+
192
+ result = conductor.perform outer, :number => 4
193
+
194
+ assert_equal 81, result
195
+
196
+ assert_equal expected_telemetry, telemetry
197
+ end
198
+
199
+ private
200
+
201
+ def expected_telemetry
202
+ {
203
+ :input => { :number => 4 },
204
+ :movements => {
205
+ :double => {
206
+ :input => { :number => 4 },
207
+ :output => { :doubled => 8 },
208
+ },
209
+ :plus_one => {
210
+ :input => { :doubled => 8 },
211
+ :output => { :plus_one => 9 },
212
+ },
213
+ :squared => {
214
+ :input => { :plus_one => 9 },
215
+ :output => { :squared => 81 },
216
+ },
217
+ },
218
+ :output => 81,
219
+ :performance_name => nil,
220
+ :service_calls => [],
221
+ }
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,131 @@
1
+ class RunListTest < Minitest::Test
2
+ def test_all_are_required
3
+ builder.input_names << :foo
4
+
5
+ run_list = builder.build
6
+
7
+ assert_equal %w(foo⇒bar bar⇒baz baz⇒qux qux⇒res), run_list.node_names
8
+ assert_includes run_list.dependencies, :foo
9
+ end
10
+
11
+ def test_discards_unnecessary_nodes
12
+ builder['aba⇒cab'] = OpenStruct.new :required_dependencies => [:aba], :optional_dependencies => [], :provisions => [:cab]
13
+
14
+ run_list = builder.build
15
+
16
+ assert_equal %w(foo⇒bar bar⇒baz baz⇒qux qux⇒res), run_list.node_names
17
+ end
18
+
19
+ def test_supplying_dependencies
20
+ builder.input_names << :baz
21
+
22
+ run_list = builder.build
23
+
24
+ assert_equal %w(baz⇒qux qux⇒res), run_list.node_names
25
+ refute_includes run_list.dependencies, :foo
26
+ end
27
+
28
+ def test_nodes_that_modify
29
+ assemble_builder modifying_nodes
30
+
31
+ run_list = builder.build
32
+
33
+ assert_equal %w(foo bar baz), run_list.node_names
34
+ end
35
+
36
+ def test_reorders_optional_deps_before_mandatory_deps_when_possible
37
+ assemble_builder order_changes_because_of_optional_deps
38
+
39
+ run_list = builder.build
40
+
41
+ assert_equal %w(baz+foo bar+baz foo+bar final), run_list.node_names
42
+ assert_equal [], run_list.required_dependencies
43
+ assert_equal [:bar, :baz, :foo], run_list.optional_dependencies
44
+ end
45
+
46
+ def test_wrap_tsort_cycle_errors
47
+ assemble_builder circular_dependency_tree
48
+
49
+ error = assert_raises Orchestra::CircularDependencyError do
50
+ builder.build
51
+ end
52
+
53
+ assert_equal(
54
+ "Circular dependency detected! Check your dependencies/provides",
55
+ error.message
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ def assemble_builder nodes = default_nodes
62
+ @builder ||= begin
63
+ builder = Orchestra::RunList::Builder.new :res
64
+ builder.merge! nodes
65
+ builder
66
+ end
67
+ end
68
+ alias_method :builder, :assemble_builder
69
+
70
+ def default_nodes
71
+ {
72
+ 'foo⇒bar' => OpenStruct.new(:required_dependencies => [:foo], :provisions => [:bar], optional_dependencies: []),
73
+ 'bar⇒baz' => OpenStruct.new(:required_dependencies => [:bar], :provisions => [:baz], optional_dependencies: []),
74
+ 'baz⇒qux' => OpenStruct.new(:required_dependencies => [:baz], :provisions => [:qux], optional_dependencies: []),
75
+ 'qux⇒res' => OpenStruct.new(:required_dependencies => [:qux], :provisions => [:res], optional_dependencies: []),
76
+ }
77
+ end
78
+
79
+ def modifying_nodes
80
+ {
81
+ 'foo' => OpenStruct.new(:required_dependencies => [:shared], :provisions => [:shared], optional_dependencies: []),
82
+ 'bar' => OpenStruct.new(:required_dependencies => [:shared], :provisions => [:shared], optional_dependencies: []),
83
+ 'baz' => OpenStruct.new(:required_dependencies => [:shared], :provisions => [:shared, :res], optional_dependencies: []),
84
+ }
85
+ end
86
+
87
+ def circular_dependency_tree
88
+ {
89
+ 'foo+bar' => OpenStruct.new(
90
+ :optional_dependencies => [:bar],
91
+ :required_dependencies => [:foo],
92
+ :provisions => [:aba]
93
+ ),
94
+ 'bar+baz' => OpenStruct.new(
95
+ :optional_dependencies => [:foo],
96
+ :required_dependencies => [:bar],
97
+ :provisions => [:cab],
98
+ ),
99
+ 'final' => OpenStruct.new(
100
+ :optional_dependencies => [],
101
+ :required_dependencies => [:aba, :cab],
102
+ :provisions => [:res],
103
+ )
104
+ }
105
+ end
106
+
107
+ def order_changes_because_of_optional_deps
108
+ {
109
+ 'foo+bar' => OpenStruct.new(
110
+ :optional_dependencies => [],
111
+ :required_dependencies => [:foo, :bar],
112
+ :provisions => [:aba]
113
+ ),
114
+ 'bar+baz' => OpenStruct.new(
115
+ :optional_dependencies => [:bar],
116
+ :required_dependencies => [:baz],
117
+ :provisions => [:cab],
118
+ ),
119
+ 'baz+foo' => OpenStruct.new(
120
+ :optional_dependencies => [:baz, :foo],
121
+ :required_dependencies => [],
122
+ :provisions => [:bra],
123
+ ),
124
+ 'final' => OpenStruct.new(
125
+ :optional_dependencies => [],
126
+ :required_dependencies => [:aba, :cab, :bra],
127
+ :provisions => [:res],
128
+ ),
129
+ }
130
+ end
131
+ end