exel 1.4.0 → 1.5.1
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.
- 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
|