exel 0.0.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rspec +2 -1
  4. data/exel.gemspec +9 -2
  5. data/lib/exel/ast_node.rb +30 -0
  6. data/lib/exel/context.rb +79 -0
  7. data/lib/exel/deferred_context_value.rb +18 -0
  8. data/lib/exel/error/job_termination.rb +10 -0
  9. data/lib/exel/execution_worker.rb +13 -0
  10. data/lib/exel/handlers/s3_handler.rb +43 -0
  11. data/lib/exel/handlers/sidekiq_handler.rb +21 -0
  12. data/lib/exel/instruction.rb +17 -0
  13. data/lib/exel/instruction_node.rb +9 -0
  14. data/lib/exel/job.rb +74 -0
  15. data/lib/exel/logging.rb +30 -0
  16. data/lib/exel/null_instruction.rb +6 -0
  17. data/lib/exel/processor_helper.rb +67 -0
  18. data/lib/exel/processors/async_processor.rb +24 -0
  19. data/lib/exel/processors/split_processor.rb +85 -0
  20. data/lib/exel/resource.rb +35 -0
  21. data/lib/exel/sequence_node.rb +14 -0
  22. data/lib/exel/version.rb +1 -1
  23. data/lib/exel.rb +19 -1
  24. data/spec/exel/ast_node_spec.rb +52 -0
  25. data/spec/exel/context_spec.rb +151 -0
  26. data/spec/exel/deferred_context_value_spec.rb +21 -0
  27. data/spec/exel/execution_worker_spec.rb +13 -0
  28. data/spec/exel/handlers/s3_handler_spec.rb +49 -0
  29. data/spec/exel/handlers/sidekiq_handler_spec.rb +54 -0
  30. data/spec/exel/instruction_node_spec.rb +22 -0
  31. data/spec/exel/instruction_spec.rb +58 -0
  32. data/spec/exel/job_spec.rb +215 -0
  33. data/spec/exel/logging_spec.rb +36 -0
  34. data/spec/exel/null_instruction_spec.rb +5 -0
  35. data/spec/exel/processors/async_processor_spec.rb +16 -0
  36. data/spec/exel/processors/split_processor_spec.rb +90 -0
  37. data/spec/exel/resource_spec.rb +51 -0
  38. data/spec/exel/sequence_node_spec.rb +24 -0
  39. data/spec/spec_helper.rb +7 -0
  40. 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
@@ -0,0 +1,5 @@
1
+ module EXEL
2
+ describe NullInstruction do
3
+ it { is_expected.to respond_to :execute }
4
+ end
5
+ end