ntl-orchestra 0.9.0

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