exel 0.0.1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.rspec +2 -1
- data/exel.gemspec +9 -2
- data/lib/exel/ast_node.rb +30 -0
- data/lib/exel/context.rb +79 -0
- data/lib/exel/deferred_context_value.rb +18 -0
- data/lib/exel/error/job_termination.rb +10 -0
- data/lib/exel/execution_worker.rb +13 -0
- data/lib/exel/handlers/s3_handler.rb +43 -0
- data/lib/exel/handlers/sidekiq_handler.rb +21 -0
- data/lib/exel/instruction.rb +17 -0
- data/lib/exel/instruction_node.rb +9 -0
- data/lib/exel/job.rb +74 -0
- data/lib/exel/logging.rb +30 -0
- data/lib/exel/null_instruction.rb +6 -0
- data/lib/exel/processor_helper.rb +67 -0
- data/lib/exel/processors/async_processor.rb +24 -0
- data/lib/exel/processors/split_processor.rb +85 -0
- data/lib/exel/resource.rb +35 -0
- data/lib/exel/sequence_node.rb +14 -0
- data/lib/exel/version.rb +1 -1
- data/lib/exel.rb +19 -1
- data/spec/exel/ast_node_spec.rb +52 -0
- data/spec/exel/context_spec.rb +151 -0
- data/spec/exel/deferred_context_value_spec.rb +21 -0
- data/spec/exel/execution_worker_spec.rb +13 -0
- data/spec/exel/handlers/s3_handler_spec.rb +49 -0
- data/spec/exel/handlers/sidekiq_handler_spec.rb +54 -0
- data/spec/exel/instruction_node_spec.rb +22 -0
- data/spec/exel/instruction_spec.rb +58 -0
- data/spec/exel/job_spec.rb +215 -0
- data/spec/exel/logging_spec.rb +36 -0
- data/spec/exel/null_instruction_spec.rb +5 -0
- data/spec/exel/processors/async_processor_spec.rb +16 -0
- data/spec/exel/processors/split_processor_spec.rb +90 -0
- data/spec/exel/resource_spec.rb +51 -0
- data/spec/exel/sequence_node_spec.rb +24 -0
- data/spec/spec_helper.rb +7 -0
- metadata +151 -18
@@ -0,0 +1,52 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe ASTNode do
|
3
|
+
let(:context) { {} }
|
4
|
+
|
5
|
+
def build_tree
|
6
|
+
@node_3 = ASTNode.new(instruction)
|
7
|
+
@node_4 = ASTNode.new(instruction)
|
8
|
+
@node_2 = ASTNode.new(instruction, [@node_3, @node_4])
|
9
|
+
@node_5 = ASTNode.new(instruction)
|
10
|
+
@node_1 = ASTNode.new(instruction, [@node_2, @node_5])
|
11
|
+
end
|
12
|
+
|
13
|
+
def instruction
|
14
|
+
instance_double(Instruction, execute: nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#start' do
|
18
|
+
class TestNode < ASTNode
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when an JobTermination error bubbles up' do
|
22
|
+
it 'should ensure the process fails silently' do
|
23
|
+
node = TestNode.new(instruction)
|
24
|
+
allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
|
25
|
+
expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
|
26
|
+
expect { node.start(context) }.to_not raise_error
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#run' do
|
32
|
+
class TestNode < ASTNode;
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should raise an error if not implemented' do
|
36
|
+
expect { TestNode.new(instruction).run(context) }.to raise_error 'EXEL::TestNode does not implement #process'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#add_child' do
|
41
|
+
it 'should add the given node to its children' do
|
42
|
+
root = ASTNode.new(instruction)
|
43
|
+
child_node = ASTNode.new(instruction)
|
44
|
+
child_node2 = ASTNode.new(instruction)
|
45
|
+
root.add_child(child_node)
|
46
|
+
root.add_child(child_node2)
|
47
|
+
|
48
|
+
expect(root.children).to eq([child_node, child_node2])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe Context do
|
3
|
+
subject(:context) { EXEL::Context.new(test1: 'foo', test2: 2) }
|
4
|
+
|
5
|
+
describe '#initialize' do
|
6
|
+
it 'should be able to initialize with a hash' do
|
7
|
+
expect(context.table[:test1]).to eq('foo')
|
8
|
+
expect(context.table[:test2]).to eq(2)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#serialize' do
|
13
|
+
let(:handler) { instance_double(Handlers::S3Handler, upload: nil) }
|
14
|
+
|
15
|
+
before do
|
16
|
+
allow(Handlers::S3Handler).to receive(:new).and_return(handler) #TODO don't stub new
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should write the serialized context to a file and upload it' do
|
20
|
+
expect(Resource).to receive(:remotize).with(context[:test1]).and_return('remote_value1')
|
21
|
+
expect(Resource).to receive(:remotize).with(context[:test2]).and_return('remote_value2')
|
22
|
+
|
23
|
+
expect(SecureRandom).to receive(:uuid).and_return('uuid')
|
24
|
+
|
25
|
+
expect(handler).to receive(:upload) do |file|
|
26
|
+
expect(file.read).to eq(Marshal.dump(Context.new(test1: 'remote_value1', test2: 'remote_value2')))
|
27
|
+
expect(file.path).to include('uuid')
|
28
|
+
'file_uri'
|
29
|
+
end
|
30
|
+
|
31
|
+
expect(context.serialize).to eq('file_uri')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should not mutate the current context' do
|
35
|
+
original_table = context.table.dup
|
36
|
+
context.serialize
|
37
|
+
expect(context.table).to eq(original_table)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '.deserialize' do
|
42
|
+
it 'should deserialize a given uri' do
|
43
|
+
uri = 'test_uri'
|
44
|
+
file = StringIO.new(Marshal.dump(context))
|
45
|
+
expect_any_instance_of(Handlers::S3Handler).to receive(:download).with(uri).and_return(file)
|
46
|
+
|
47
|
+
expect(Context.deserialize(uri)).to eq(context)
|
48
|
+
|
49
|
+
expect(file).to be_closed
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '#[]' do
|
54
|
+
subject(:context) { EXEL::Context.new(key: Resource.remotize('value')) }
|
55
|
+
|
56
|
+
it 'should return localized values' do
|
57
|
+
expect(context[:key]).to eq('value')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should store the localized value' do
|
61
|
+
context[:key]
|
62
|
+
expect(context.table[:key]).to eq('value')
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'DeferredContextValue object' do
|
66
|
+
context 'at the top level' do
|
67
|
+
it 'should return the lookup value from the context' do
|
68
|
+
deferred_context_value = DeferredContextValue.new[:key]
|
69
|
+
context[:deferred_value] = deferred_context_value
|
70
|
+
expect(context[:deferred_value]).to eq(context[:key])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
context 'in array' do
|
74
|
+
it 'should return the lookup value from the context' do
|
75
|
+
deferred_context_value = DeferredContextValue.new[:key]
|
76
|
+
context[:array] = [1, 2, deferred_context_value]
|
77
|
+
expect(context[:array]).to eq([1, 2, context[:key]])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'in hash' do
|
82
|
+
it 'should return the lookup value from the context' do
|
83
|
+
deferred_context_value = DeferredContextValue.new[:key]
|
84
|
+
context[:hash] = {hash_key: deferred_context_value}
|
85
|
+
expect(context[:hash]).to eq({hash_key: context[:key]})
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'in nested arrays and hashes' do
|
90
|
+
it 'should lookup a deferred context value in a hash nested in an array' do
|
91
|
+
deferred_context_value = DeferredContextValue.new[:key]
|
92
|
+
context[:nested] = [{}, {hash_key: deferred_context_value}]
|
93
|
+
expect(context[:nested]).to eq([{}, {hash_key: context[:key]}])
|
94
|
+
end
|
95
|
+
it 'should lookup a deferred context value in an array nested in a hash' do
|
96
|
+
deferred_context_value = DeferredContextValue.new[:key]
|
97
|
+
context[:nested] = {hash_key: [1, deferred_context_value]}
|
98
|
+
expect(context[:nested]).to eq({hash_key: [1, context[:key]]})
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#[]=' do
|
105
|
+
it 'should add the key/value pair to table' do
|
106
|
+
context[:new_key] = 'new_value'
|
107
|
+
expect(context.table[:new_key]).to eq('new_value')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe '#delete' do
|
112
|
+
it 'should delete the key/value pair from the table' do
|
113
|
+
context[:key] = 'value'
|
114
|
+
context[:key2] = 'value2'
|
115
|
+
context.delete(:key)
|
116
|
+
expect(context.table.keys).to_not include(:key)
|
117
|
+
expect(context.table.keys).to include(:key2)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#merge!' do
|
122
|
+
it 'should merge given keys and values into the context' do
|
123
|
+
context.table[:overwrite] = 'overwrite'
|
124
|
+
context.table[:existing] = 'existing'
|
125
|
+
|
126
|
+
context.merge!(overwrite: 'changed', new: 'new')
|
127
|
+
|
128
|
+
expect(context.table[:overwrite]).to eq('changed')
|
129
|
+
expect(context.table[:existing]).to eq('existing')
|
130
|
+
expect(context.table[:new]).to eq('new')
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should return itself' do
|
134
|
+
expect(context.merge!(key: 'value')).to eq(context)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '#==' do
|
139
|
+
it { is_expected.to_not eq(nil) }
|
140
|
+
|
141
|
+
it { is_expected.to eq(context) }
|
142
|
+
|
143
|
+
it { is_expected.to_not eq(42) }
|
144
|
+
|
145
|
+
it { is_expected.to_not eq(Context.new(other_key: 'value')) }
|
146
|
+
|
147
|
+
it { is_expected.to eq(context.dup) }
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe DeferredContextValue do
|
3
|
+
subject(:deferred_context_value) { DeferredContextValue.new }
|
4
|
+
|
5
|
+
describe '#[]' do
|
6
|
+
it 'should store passed key in the keys attribute' do
|
7
|
+
deferred_context_value[:top_level_key]['sub_key']
|
8
|
+
expect(deferred_context_value.keys).to eq([:top_level_key, 'sub_key'])
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#get' do
|
13
|
+
it 'should look up the value of the keys attribute in the passed-in context' do
|
14
|
+
allow(deferred_context_value).to receive(:keys).and_return([:top_level_key, 'sub_key'])
|
15
|
+
value = 'example_value'
|
16
|
+
context = Context.new(top_level_key: {'sub_key' => value})
|
17
|
+
expect(deferred_context_value.get(context)).to eq(value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe ExecutionWorker do
|
3
|
+
it 'should run the given block with the deserialized context' do
|
4
|
+
dsl_block = instance_double(SequenceNode)
|
5
|
+
context = Context.new(test1: 'foo', test2: 2, _block: dsl_block)
|
6
|
+
context_uri = 'test uri'
|
7
|
+
|
8
|
+
expect(Context).to receive(:deserialize).with(context_uri).and_return(context)
|
9
|
+
expect(dsl_block).to receive(:start).with(context)
|
10
|
+
ExecutionWorker.new.perform(context_uri)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module EXEL
|
2
|
+
module Handlers
|
3
|
+
describe S3Handler do
|
4
|
+
subject(:handler) { S3Handler.new }
|
5
|
+
|
6
|
+
describe '#get_object' do
|
7
|
+
before do
|
8
|
+
EXEL.configure { |config| config[:s3_bucket] = 'bucket' }
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should have the correct bucket and file names' do
|
12
|
+
file_name = 'abc.txt'
|
13
|
+
s3_obj = handler.get_object(file_name)
|
14
|
+
expect(s3_obj.bucket_name).to eq('bucket')
|
15
|
+
expect(s3_obj.key).to eq(file_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#upload' do
|
20
|
+
let(:file) { double(path: '/path/to/abc.txt', close: nil) }
|
21
|
+
|
22
|
+
it 'should upload a file to s3' do
|
23
|
+
expect_any_instance_of(Aws::S3::Object).to receive(:upload_file).with(file)
|
24
|
+
|
25
|
+
handler.upload(file)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should return the file URI of the uploaded file' do
|
29
|
+
allow_any_instance_of(Aws::S3::Object).to receive(:upload_file).with(file)
|
30
|
+
expect(handler.upload(file)).to eq('s3://abc.txt')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#download' do
|
35
|
+
it 'should download the file from s3' do
|
36
|
+
file = double(:file)
|
37
|
+
s3_object = double(:s3_object)
|
38
|
+
|
39
|
+
expect(handler).to receive(:get_object).with('abc.txt').and_return(s3_object)
|
40
|
+
expect(Tempfile).to receive(:new).with('abc.txt', encoding: Encoding::ASCII_8BIT).and_return(file)
|
41
|
+
expect(s3_object).to receive(:get).with(hash_including(response_target: file)).and_return(file)
|
42
|
+
expect(file).to receive(:set_encoding).with(Encoding::UTF_8)
|
43
|
+
|
44
|
+
expect(handler.download('s3://abc.txt')).to eq(file)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module EXEL
|
2
|
+
module Handlers
|
3
|
+
describe SidekiqHandler do
|
4
|
+
subject(:handler) { SidekiqHandler.new(context) }
|
5
|
+
let(:callback) { instance_double(SequenceNode) }
|
6
|
+
let(:context) { EXEL::Context.new }
|
7
|
+
let(:serialized_context_uri) { 'context_uri' }
|
8
|
+
|
9
|
+
describe '#do_async' do
|
10
|
+
before do
|
11
|
+
allow(context).to receive(:serialize).and_return(serialized_context_uri)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should add the callback to the context before serializing it' do
|
15
|
+
expect(context).to receive(:[]=).with(:_block, callback).ordered
|
16
|
+
expect(context).to receive(:serialize).ordered
|
17
|
+
handler.do_async(callback)
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with queue name' do
|
21
|
+
let(:context) { EXEL::Context.new(queue: 'import_processor') }
|
22
|
+
|
23
|
+
it 'should push the execution worker to the given queue' do
|
24
|
+
expect(Sidekiq::Client).to receive(:push).with('queue' => context[:queue], 'class' => ExecutionWorker, 'args' => [serialized_context_uri])
|
25
|
+
handler.do_async(callback)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'without queue name' do
|
30
|
+
it 'should push the execution worker to the default queue' do
|
31
|
+
expect(Sidekiq::Client).to receive(:push).with('class' => ExecutionWorker, 'args' => [serialized_context_uri])
|
32
|
+
handler.do_async(callback)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with retries specified' do
|
37
|
+
let(:context) { EXEL::Context.new(retry: 1) }
|
38
|
+
|
39
|
+
it 'should push the execution worker with a specified number of retries' do
|
40
|
+
expect(Sidekiq::Client).to receive(:push).with('retry' => context[:retry], 'class' => ExecutionWorker, 'args' => [serialized_context_uri])
|
41
|
+
handler.do_async(callback)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'with no retries specified' do
|
46
|
+
it 'should push the execution worker with no specified number of retries' do
|
47
|
+
expect(Sidekiq::Client).to receive(:push).with('class' => ExecutionWorker, 'args' => [serialized_context_uri])
|
48
|
+
handler.do_async(callback)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe InstructionNode do
|
3
|
+
let(:context) { {} }
|
4
|
+
let(:instruction) { instance_double(Instruction, execute: nil) }
|
5
|
+
let(:child) { instance_double(ASTNode) }
|
6
|
+
subject(:node) { InstructionNode.new(instruction, [child]) }
|
7
|
+
|
8
|
+
it { is_expected.to be_kind_of(ASTNode) }
|
9
|
+
|
10
|
+
describe '#run' do
|
11
|
+
it 'should only execute the instruction' do
|
12
|
+
expect(instruction).to receive(:execute).with(context).once
|
13
|
+
node.run(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should not run it`s children' do
|
17
|
+
expect(child).to_not receive(:run)
|
18
|
+
node.run(context)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe Instruction do
|
3
|
+
subject(:instruction) { EXEL::Instruction.new('ins_name', processor_class, args) }
|
4
|
+
let(:processor_class) { double(:processor_class, new: processor_instance) }
|
5
|
+
let(:processor_instance) { double(:processor_instance, process: nil) }
|
6
|
+
let(:args) { {arg1: 'arg_value1', arg2: {}} }
|
7
|
+
let(:context) { {context_key: 'context_value'} }
|
8
|
+
|
9
|
+
describe '#run' do
|
10
|
+
it 'should call process on an instance of the processor class' do
|
11
|
+
expect(processor_class).to receive(:new).and_return(processor_instance)
|
12
|
+
expect(processor_instance).to receive(:process)
|
13
|
+
|
14
|
+
instruction.execute(context)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should not pass a copy of the context' do
|
18
|
+
allow(processor_class).to receive(:new) do |context_arg|
|
19
|
+
expect(context_arg).to be(context)
|
20
|
+
processor_instance
|
21
|
+
end
|
22
|
+
|
23
|
+
instruction.execute(context)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should add args to the context' do
|
27
|
+
instruction.execute(context)
|
28
|
+
expect(context.keys).to include(*args.keys)
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with args' do
|
32
|
+
it 'should pass the args to the processor' do
|
33
|
+
expect(processor_class).to receive(:new).with(hash_including(args))
|
34
|
+
instruction.execute(context)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'without args' do
|
39
|
+
let(:args) { nil }
|
40
|
+
|
41
|
+
it 'should just pass the context to the processor' do
|
42
|
+
expect(processor_class).to receive(:new).with(context)
|
43
|
+
instruction.execute(context)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'with a subtree' do
|
48
|
+
let(:subtree) { double(:subtree) }
|
49
|
+
subject(:instruction) { EXEL::Instruction.new('ins_name', processor_class, args, subtree) }
|
50
|
+
|
51
|
+
it 'should pass the subtree to the processor' do
|
52
|
+
expect(processor_instance).to receive(:process).with(subtree)
|
53
|
+
instruction.execute(context)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe Job do
|
3
|
+
describe '.define' do
|
4
|
+
let(:ast) { instance_double(SequenceNode, run: nil, start: nil) }
|
5
|
+
let(:block) { Proc.new {} }
|
6
|
+
|
7
|
+
after { Job.registry.clear }
|
8
|
+
|
9
|
+
it 'should register job definitions' do
|
10
|
+
Job.define :test_job, &block
|
11
|
+
expect(Job.registry[:test_job]).to eq(block)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should raise an exception if a job name is already in use' do
|
15
|
+
Job.define :test_job, &block
|
16
|
+
expect { Job.define :test_job, &block }.to raise_error 'Job :test_job is already defined'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '.run' do
|
21
|
+
let(:ast) { instance_double(SequenceNode, run: nil, start: nil) }
|
22
|
+
let(:context) { instance_double(Context) }
|
23
|
+
|
24
|
+
context 'with a string of DSL code' do
|
25
|
+
it 'should parse the code' do
|
26
|
+
dsl_code = 'code'
|
27
|
+
expect(Job::Parser).to receive(:parse).with(dsl_code).and_return(ast)
|
28
|
+
Job.run(dsl_code, context)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should run ast returned by the parser' do
|
32
|
+
allow(Job::Parser).to receive(:parse).and_return(ast)
|
33
|
+
expect(ast).to receive(:start).with(context)
|
34
|
+
Job.run('code', context)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with a job name' do
|
39
|
+
context 'of a defined job' do
|
40
|
+
let(:block) { Proc.new {} }
|
41
|
+
|
42
|
+
before do
|
43
|
+
allow(Job).to receive(:registry).and_return(test_job: block)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should run the job' do
|
47
|
+
expect(Job::Parser).to receive(:parse).with(block).and_return(ast)
|
48
|
+
expect(ast).to receive(:start).with(context)
|
49
|
+
Job.run(:test_job, context)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'of a undefined job' do
|
54
|
+
it 'should return nil' do
|
55
|
+
expect { Job.run(:test_job, context) }.to raise_error('Job "test_job" not found')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'mutation of arguments' do
|
62
|
+
class TestProcessor
|
63
|
+
def initialize(context)
|
64
|
+
context[:array] << context[:arg]
|
65
|
+
end
|
66
|
+
|
67
|
+
def process(callback)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should not persist between runs' do
|
72
|
+
Job.define :test do
|
73
|
+
process with: TestProcessor, array: [], arg: context[:value]
|
74
|
+
end
|
75
|
+
|
76
|
+
context = Context.new(value: 1)
|
77
|
+
Job.run(:test, context)
|
78
|
+
expect(context[:array]).to eq([1])
|
79
|
+
|
80
|
+
context = Context.new(value: 2)
|
81
|
+
Job.run(:test, context)
|
82
|
+
expect(context[:array]).to eq([2])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe Job::Parser do
|
88
|
+
let(:parser) { Job::Parser.new }
|
89
|
+
let(:ast) { instance_double(SequenceNode, run: nil) }
|
90
|
+
|
91
|
+
describe '#initialize' do
|
92
|
+
it 'should initialize a sequence node' do
|
93
|
+
expect(parser.ast).to be_kind_of(SequenceNode)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '.parse' do
|
98
|
+
let(:parser) { instance_double(Job::Parser, ast: ast, instance_eval: nil) }
|
99
|
+
|
100
|
+
before do
|
101
|
+
allow(Job::Parser).to receive(:new).and_return(parser)
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'given DSL code as a proc' do
|
105
|
+
it 'should eval the code as a block' do
|
106
|
+
dsl_proc = Proc.new {}
|
107
|
+
expect(parser).to receive(:instance_eval) do |*_args, &block|
|
108
|
+
expect(block).to eq(dsl_proc)
|
109
|
+
end
|
110
|
+
|
111
|
+
Job::Parser.parse(dsl_proc)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'given DSL code as a string' do
|
116
|
+
it 'should eval the code as a string' do
|
117
|
+
dsl_code = 'code'
|
118
|
+
expect(parser).to receive(:instance_eval).with(dsl_code)
|
119
|
+
|
120
|
+
Job::Parser.parse(dsl_code)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should return the parsed AST' do
|
125
|
+
expect(Job::Parser.parse(Proc.new {})).to eq(ast)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#process' do
|
130
|
+
let(:block) { Proc.new {} }
|
131
|
+
|
132
|
+
before do
|
133
|
+
allow(Job::Parser).to receive(:parse).and_return(ast)
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'without a block' do
|
137
|
+
it 'should create a process instruction' do
|
138
|
+
processor_class = double(:processor_class)
|
139
|
+
expect(Instruction).to receive(:new).with('process', processor_class, {arg1: 'arg1_value'}, nil)
|
140
|
+
|
141
|
+
parser.process with: processor_class, arg1: 'arg1_value'
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should append an instruction node to the AST with no children' do
|
145
|
+
expect(parser.ast).to receive(:add_child) do |node|
|
146
|
+
expect(node).to be_a_kind_of(InstructionNode)
|
147
|
+
expect(node.instruction.name).to eq('process')
|
148
|
+
expect(node.children).to eq([])
|
149
|
+
end
|
150
|
+
|
151
|
+
parser.process with: double(:processor_class)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'with a block' do
|
156
|
+
it 'should pass the parsed subtree to the instruction' do
|
157
|
+
processor_class = double(:processor_class)
|
158
|
+
expect(Job::Parser).to receive(:parse).with(block).and_return(ast)
|
159
|
+
expect(Instruction).to receive(:new).with('process', processor_class, {arg1: 'arg1_value'}, ast)
|
160
|
+
|
161
|
+
parser.process with: processor_class, arg1: 'arg1_value', &block
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should append an instruction node to the AST with the parsed block as its subtree' do
|
165
|
+
expect(parser.ast).to receive(:add_child) do |node|
|
166
|
+
expect(node).to be_a_kind_of(InstructionNode)
|
167
|
+
expect(node.instruction.name).to eq('process')
|
168
|
+
expect(node.children).to eq([ast])
|
169
|
+
end
|
170
|
+
|
171
|
+
parser.process with: double(:processor_class), &block
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
[
|
177
|
+
{method: :async, processor: Processors::AsyncProcessor},
|
178
|
+
{method: :split, processor: Processors::SplitProcessor}
|
179
|
+
].each do |data|
|
180
|
+
describe "##{data[:method]}" do
|
181
|
+
before do
|
182
|
+
allow(Job::Parser).to receive(:parse).and_return(ast)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should create a #{data[:method]} instruction" do
|
186
|
+
expect(Instruction).to receive(:new).with(data[:method].to_s, data[:processor], {arg1: 'arg1_value'}, ast)
|
187
|
+
parser.send(data[:method], {arg1: 'arg1_value'}) {}
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'should parse the block given' do
|
191
|
+
block = -> {}
|
192
|
+
expect(Job::Parser).to receive(:parse).with(block).and_return(ast)
|
193
|
+
|
194
|
+
parser.send(data[:method], &block)
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'should add parsed subtree and instruction to the AST' do
|
198
|
+
expect(parser.ast).to receive(:add_child) do |node|
|
199
|
+
expect(node).to be_a_kind_of(InstructionNode)
|
200
|
+
expect(node.instruction.name).to eq(data[:method].to_s)
|
201
|
+
expect(node.children).to eq([ast])
|
202
|
+
end
|
203
|
+
|
204
|
+
parser.send(data[:method]) {}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe '#context' do
|
210
|
+
it 'should return a DeferredContextValue' do
|
211
|
+
expect(parser.context).to be_a_kind_of(DeferredContextValue)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe Logging do
|
3
|
+
before { @restore_logger = Logging.logger }
|
4
|
+
after { Logging.logger = @restore_logger }
|
5
|
+
|
6
|
+
describe '.logger=' do
|
7
|
+
it 'should set a logger' do
|
8
|
+
logger = double(:logger)
|
9
|
+
Logging.logger = logger
|
10
|
+
expect(Logging.logger).to be(logger)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should set a null logger when nil given' do
|
14
|
+
expect(Logger).to receive(:new).with('/dev/null')
|
15
|
+
Logging.logger = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.logger' do
|
20
|
+
before { Logging.instance_variable_set(:@logger, nil) }
|
21
|
+
|
22
|
+
it 'should initialize the logger on first read if not already set' do
|
23
|
+
EXEL.configure do |config|
|
24
|
+
config[:log_level] = :warn
|
25
|
+
config[:log_filename] = 'log.txt'
|
26
|
+
end
|
27
|
+
|
28
|
+
logger = instance_double(Logger)
|
29
|
+
expect(Logger).to receive(:new).with('log.txt').and_return(logger)
|
30
|
+
expect(logger).to receive(:level=).with(Logger::WARN)
|
31
|
+
|
32
|
+
Logging.logger
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|