exel 1.4.0 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +4 -4
- data/.rubocop.yml +20 -12
- data/.rubocop_airbnb.yml +2 -0
- data/.travis.yml +17 -4
- data/Gemfile +1 -2
- data/Gemfile.lock +64 -60
- data/README.md +1 -1
- data/Rakefile +1 -0
- data/exel.gemspec +4 -4
- data/lib/exel.rb +1 -0
- data/lib/exel/ast_node.rb +2 -1
- data/lib/exel/context.rb +2 -1
- data/lib/exel/deferred_context_value.rb +1 -0
- data/lib/exel/error/job_termination.rb +1 -0
- data/lib/exel/events.rb +1 -0
- data/lib/exel/instruction.rb +2 -1
- data/lib/exel/instruction_node.rb +1 -0
- data/lib/exel/job.rb +6 -4
- data/lib/exel/listen_instruction.rb +1 -0
- data/lib/exel/logging.rb +1 -0
- data/lib/exel/logging/logger_wrapper.rb +4 -1
- data/lib/exel/logging_helper.rb +1 -0
- data/lib/exel/middleware/chain.rb +1 -0
- data/lib/exel/middleware/logging.rb +1 -1
- data/lib/exel/null_instruction.rb +1 -0
- data/lib/exel/processor_helper.rb +1 -0
- data/lib/exel/processors/run_processor.rb +1 -0
- data/lib/exel/processors/split_processor.rb +2 -1
- data/lib/exel/providers/local_file_provider.rb +2 -1
- data/lib/exel/providers/threaded_async_provider.rb +1 -0
- data/lib/exel/sequence_node.rb +1 -0
- data/lib/exel/value.rb +1 -0
- data/lib/exel/version.rb +2 -1
- data/spec/exel/ast_node_spec.rb +42 -42
- data/spec/exel/context_spec.rb +76 -77
- data/spec/exel/deferred_context_value_spec.rb +41 -42
- data/spec/exel/events_spec.rb +65 -65
- data/spec/exel/instruction_node_spec.rb +16 -16
- data/spec/exel/instruction_spec.rb +46 -45
- data/spec/exel/job_spec.rb +94 -91
- data/spec/exel/listen_instruction_spec.rb +10 -10
- data/spec/exel/logging/logger_wrapper_spec.rb +67 -69
- data/spec/exel/logging_helper_spec.rb +15 -16
- data/spec/exel/logging_spec.rb +56 -56
- data/spec/exel/middleware/chain_spec.rb +51 -53
- data/spec/exel/middleware/logging_spec.rb +21 -23
- data/spec/exel/middleware_spec.rb +49 -50
- data/spec/exel/null_instruction_spec.rb +3 -4
- data/spec/exel/processors/async_processor_spec.rb +16 -18
- data/spec/exel/processors/run_processor_spec.rb +9 -11
- data/spec/exel/processors/split_processor_spec.rb +91 -93
- data/spec/exel/providers/local_file_provider_spec.rb +25 -28
- data/spec/exel/providers/threaded_async_provider_spec.rb +36 -38
- data/spec/exel/sequence_node_spec.rb +11 -11
- data/spec/exel/value_spec.rb +32 -33
- data/spec/exel_spec.rb +8 -7
- data/spec/integration/integration_spec.rb +2 -1
- data/spec/spec_helper.rb +3 -2
- data/spec/support/integration_test_classes.rb +3 -1
- metadata +16 -30
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module EXEL
|
3
4
|
module Logging
|
4
5
|
# Wraps calls to a logger to add {Logging} prefix to log messages
|
@@ -20,7 +21,9 @@ module EXEL
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def add(severity, message = nil, progname = nil)
|
23
|
-
|
24
|
+
if message.nil? && block_given?
|
25
|
+
message = yield
|
26
|
+
end
|
24
27
|
__getobj__.add(severity, "#{Logging.prefix}#{message}", progname)
|
25
28
|
end
|
26
29
|
end
|
data/lib/exel/logging_helper.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module EXEL
|
3
4
|
module Middleware
|
4
5
|
# Middleware to add a prefix to all messages logged during processor execution. The prefix is specified by the
|
@@ -12,7 +13,6 @@ module EXEL
|
|
12
13
|
|
13
14
|
def log_process
|
14
15
|
start_time = Time.now
|
15
|
-
EXEL.logger.info 'Starting'
|
16
16
|
|
17
17
|
yield
|
18
18
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'csv'
|
3
4
|
require 'tempfile'
|
4
5
|
require_relative '../logging_helper'
|
@@ -65,7 +66,7 @@ module EXEL
|
|
65
66
|
def process_file(callback)
|
66
67
|
csv_options = @context[:csv_options] || {col_sep: ','}
|
67
68
|
|
68
|
-
CSV.foreach(@file.path, csv_options) do |line|
|
69
|
+
CSV.foreach(@file.path, **csv_options) do |line|
|
69
70
|
process_line(line, callback)
|
70
71
|
|
71
72
|
break if @tempfile_count == @max_chunks
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module EXEL
|
3
4
|
module Providers
|
4
5
|
# The default remote provider. Doesn't actually upload and download files to and from remote storage, but rather
|
@@ -14,7 +15,7 @@ module EXEL
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def self.remote?(uri)
|
17
|
-
uri =~ %r{file://}
|
18
|
+
uri.to_s =~ %r{file://}
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
data/lib/exel/sequence_node.rb
CHANGED
data/lib/exel/value.rb
CHANGED
data/lib/exel/version.rb
CHANGED
data/spec/exel/ast_node_spec.rb
CHANGED
@@ -1,61 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
module EXEL
|
3
|
-
describe ASTNode do
|
4
|
-
let(:context) { instance_double(EXEL::Context) }
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
end
|
3
|
+
describe EXEL::ASTNode do
|
4
|
+
let(:context) { instance_double(EXEL::Context) }
|
9
5
|
|
10
|
-
|
6
|
+
def instruction
|
7
|
+
instance_double(EXEL::Instruction, execute: nil)
|
8
|
+
end
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
let(:node) { TestNode.new(instruction) }
|
10
|
+
class TestNode < EXEL::ASTNode
|
11
|
+
end
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
describe '#start' do
|
14
|
+
context 'when a JobTermination error bubbles up' do
|
15
|
+
let(:node) { TestNode.new(instruction) }
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
17
|
+
before do
|
18
|
+
allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
|
19
|
+
end
|
24
20
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
21
|
+
it 'ensures the process fails silently' do
|
22
|
+
expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
|
23
|
+
expect { node.start(context) }.not_to raise_error
|
24
|
+
end
|
29
25
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
26
|
+
it 'logs the error by default' do
|
27
|
+
expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
|
28
|
+
node.start(context)
|
29
|
+
end
|
34
30
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
context 'given a log instruction' do
|
32
|
+
before do
|
33
|
+
allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination.new('Error', :warn))
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'logs the error with the given cmd' do
|
37
|
+
expect(EXEL.logger).to receive(:warn).with('JobTerminationError: Error')
|
38
|
+
node.start(context)
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
42
|
+
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end
|
44
|
+
describe '#run' do
|
45
|
+
it 'raises an error if not implemented' do
|
46
|
+
expect { TestNode.new(instruction).run(context) }.to raise_error 'TestNode does not implement #process'
|
47
47
|
end
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
describe '#add_child' do
|
51
|
+
it 'adds the given node to its children' do
|
52
|
+
root = EXEL::ASTNode.new(instruction)
|
53
|
+
child_node = EXEL::ASTNode.new(instruction)
|
54
|
+
child_node2 = EXEL::ASTNode.new(instruction)
|
55
|
+
root.add_child(child_node)
|
56
|
+
root.add_child(child_node2)
|
56
57
|
|
57
|
-
|
58
|
-
end
|
58
|
+
expect(root.children).to eq([child_node, child_node2])
|
59
59
|
end
|
60
60
|
end
|
61
61
|
end
|
data/spec/exel/context_spec.rb
CHANGED
@@ -1,109 +1,108 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
module EXEL
|
3
|
-
describe Context do
|
4
|
-
subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
|
5
2
|
|
6
|
-
|
3
|
+
describe EXEL::Context do
|
4
|
+
subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
it { is_expected.to be_a(Hash) }
|
7
|
+
|
8
|
+
describe '#initialize' do
|
9
|
+
it 'initializes with a hash' do
|
10
|
+
expect(context[:key1]).to eq('1')
|
11
|
+
expect(context[:key2]).to eq(2)
|
12
|
+
expect(context[:key3]).to be_nil
|
14
13
|
end
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
describe '#deep_dup' do
|
17
|
+
it 'returns a deep copy of itself' do
|
18
|
+
context[:a] = {nested: []}
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
dup = context.deep_dup
|
21
|
+
expect(context).to eq(dup)
|
22
|
+
expect(context).not_to be_equal(dup)
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
end
|
24
|
+
dup[:a][:nested] << 1
|
25
|
+
expect(context[:a][:nested]).to be_empty
|
27
26
|
end
|
27
|
+
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
it 'writes the serialized context to a file and upload it' do
|
33
|
-
expect(Value).to receive(:remotize).with(context[:key1]).and_return('remote_value1')
|
34
|
-
expect(Value).to receive(:remotize).with(context[:key2]).and_return('remote_value2')
|
29
|
+
describe '#serialize' do
|
30
|
+
before { allow(EXEL::Value).to receive(:upload) }
|
35
31
|
|
36
|
-
|
32
|
+
it 'writes the serialized context to a file and upload it' do
|
33
|
+
expect(EXEL::Value).to receive(:remotize).with(context[:key1]).and_return('remote_value1')
|
34
|
+
expect(EXEL::Value).to receive(:remotize).with(context[:key2]).and_return('remote_value2')
|
37
35
|
|
38
|
-
|
39
|
-
expect(file.read).to eq(Marshal.dump(Context.new(key1: 'remote_value1', key2: 'remote_value2')))
|
40
|
-
expect(file.path).to include('uuid')
|
41
|
-
'file_uri'
|
42
|
-
end
|
36
|
+
expect(SecureRandom).to receive(:uuid).and_return('uuid')
|
43
37
|
|
44
|
-
|
38
|
+
expect(EXEL::Value).to receive(:remotize) do |file|
|
39
|
+
expect(file.read).to eq(Marshal.dump(EXEL::Context.new(key1: 'remote_value1', key2: 'remote_value2')))
|
40
|
+
expect(file.path).to include('uuid')
|
41
|
+
'file_uri'
|
45
42
|
end
|
46
43
|
|
47
|
-
|
48
|
-
allow(Value).to receive(:remotize).and_return('remote_value')
|
49
|
-
original_table = context.dup
|
50
|
-
context.serialize
|
51
|
-
expect(context).to eq(original_table)
|
52
|
-
end
|
44
|
+
expect(context.serialize).to eq('file_uri')
|
53
45
|
end
|
54
46
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
47
|
+
it 'does not mutate the current context' do
|
48
|
+
allow(EXEL::Value).to receive(:remotize).and_return('remote_value')
|
49
|
+
original_table = context.dup
|
50
|
+
context.serialize
|
51
|
+
expect(context).to eq(original_table)
|
52
|
+
end
|
53
|
+
end
|
59
54
|
|
60
|
-
|
55
|
+
describe '.deserialize' do
|
56
|
+
it 'deserializes a given uri' do
|
57
|
+
file = StringIO.new(Marshal.dump(context))
|
58
|
+
expect(EXEL::Value).to receive(:localize).with('uri').and_return(file)
|
61
59
|
|
62
|
-
|
63
|
-
|
60
|
+
expect(EXEL::Context.deserialize('uri')).to eq(context)
|
61
|
+
|
62
|
+
expect(file).to be_closed
|
64
63
|
end
|
64
|
+
end
|
65
65
|
|
66
|
-
|
67
|
-
|
66
|
+
shared_examples 'a reader method' do
|
67
|
+
subject(:context) { EXEL::Context.new(key: 'value') }
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
it 'returns the value' do
|
70
|
+
expect(context.send(method, :key)).to eq('value')
|
71
|
+
end
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
it 'localizes the returned value' do
|
74
|
+
expect(EXEL::Value).to receive(:localize).with('value').and_return('localized')
|
75
|
+
expect(context.send(method, :key)).to eq('localized')
|
76
|
+
end
|
77
77
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
78
|
+
it 'stores the localized value' do
|
79
|
+
allow(EXEL::Value).to receive(:localize).with('value').and_return('localized')
|
80
|
+
context.send(method, :key)
|
81
|
+
allow(EXEL::Value).to receive(:localize).with('localized').and_return('localized')
|
82
|
+
context.send(method, :key)
|
83
|
+
end
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
end
|
85
|
+
it 'looks up deferred values' do
|
86
|
+
# eq(context) as an argument matcher is necessary to prevent RSpec from calling fetch on the context, leading to
|
87
|
+
# a stack overflow
|
88
|
+
expect(EXEL::DeferredContextValue).to receive(:resolve).with('value', eq(context)).and_return('resolved')
|
89
|
+
expect(context.send(method, :key)).to eq('resolved')
|
91
90
|
end
|
91
|
+
end
|
92
92
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
end
|
93
|
+
describe '#[]' do
|
94
|
+
it_behaves_like 'a reader method' do
|
95
|
+
let(:method) { :[] }
|
97
96
|
end
|
97
|
+
end
|
98
98
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
99
|
+
describe '#fetch' do
|
100
|
+
it 'raises an exception if the key is not found' do
|
101
|
+
expect { context.fetch(:unknown) }.to raise_error(KeyError)
|
102
|
+
end
|
103
103
|
|
104
|
-
|
105
|
-
|
106
|
-
end
|
104
|
+
it_behaves_like 'a reader method' do
|
105
|
+
let(:method) { :fetch }
|
107
106
|
end
|
108
107
|
end
|
109
108
|
end
|
@@ -1,66 +1,65 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
module EXEL
|
3
|
-
describe DeferredContextValue do
|
4
|
-
subject(:deferred_value) { DeferredContextValue.new }
|
5
2
|
|
6
|
-
|
7
|
-
|
3
|
+
describe EXEL::DeferredContextValue do
|
4
|
+
subject(:deferred_value) { EXEL::DeferredContextValue.new }
|
8
5
|
|
9
|
-
|
10
|
-
|
6
|
+
describe '.resolve' do
|
7
|
+
subject(:deferred_value) { EXEL::DeferredContextValue.new[:key] }
|
11
8
|
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
context 'at the top level' do
|
10
|
+
let(:context) { {key: 'value', deferred_value: deferred_value} }
|
11
|
+
|
12
|
+
it 'returns the lookup value from the context' do
|
13
|
+
expect(EXEL::DeferredContextValue.resolve(context[:deferred_value], context)).to eq(context[:key])
|
15
14
|
end
|
15
|
+
end
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
context 'in an array' do
|
18
|
+
let(:context) { {key: 'value', array: [1, 2, deferred_value]} }
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
20
|
+
it 'returns the lookup value from the context' do
|
21
|
+
expect(EXEL::DeferredContextValue.resolve(context[:array], context)).to eq([1, 2, context[:key]])
|
23
22
|
end
|
23
|
+
end
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
context 'in a hash' do
|
26
|
+
let(:context) { {key: 'value', hash: {hash_key: deferred_value}} }
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
end
|
28
|
+
it 'returns the lookup value from the context' do
|
29
|
+
expect(EXEL::DeferredContextValue.resolve(context[:hash], context)).to eq(hash_key: context[:key])
|
31
30
|
end
|
31
|
+
end
|
32
32
|
|
33
|
-
|
34
|
-
|
33
|
+
context 'in a hash nested in an array' do
|
34
|
+
let(:context) { {key: 'value', nested: [{}, {hash_key: deferred_value}]} }
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
end
|
36
|
+
it 'looks up a deferred context value in a hash nested in an array' do
|
37
|
+
expect(EXEL::DeferredContextValue.resolve(context[:nested], context)).to eq([{}, {hash_key: context[:key]}])
|
39
38
|
end
|
39
|
+
end
|
40
40
|
|
41
|
-
|
42
|
-
|
41
|
+
context 'in an array nested in a hash' do
|
42
|
+
let(:context) { {key: 'value', nested: {hash_key: [1, deferred_value]}} }
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
end
|
44
|
+
it 'looks up a deferred context value in an array nested in a hash' do
|
45
|
+
expect(EXEL::DeferredContextValue.resolve(context[:nested], context)).to eq(hash_key: [1, context[:key]])
|
47
46
|
end
|
48
47
|
end
|
48
|
+
end
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
50
|
+
describe '#[]' do
|
51
|
+
it 'stores the given key in the keys attribute' do
|
52
|
+
deferred_value[:top_level_key]['sub_key']
|
53
|
+
expect(deferred_value.keys).to eq([:top_level_key, 'sub_key'])
|
55
54
|
end
|
55
|
+
end
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end
|
57
|
+
describe '#get' do
|
58
|
+
it 'looks up the value of the keys attribute in the passed-in context' do
|
59
|
+
allow(deferred_value).to receive(:keys).and_return([:top_level_key, 'sub_key'])
|
60
|
+
value = 'example_value'
|
61
|
+
context = EXEL::Context.new(top_level_key: {'sub_key' => value})
|
62
|
+
expect(deferred_value.get(context)).to eq(value)
|
64
63
|
end
|
65
64
|
end
|
66
65
|
end
|