exel 0.0.1 → 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 (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