exel 1.2.1 → 1.5.2
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/.gitignore +1 -2
- data/.rubocop.yml +23 -14
- data/.rubocop_airbnb.yml +2 -0
- data/.rubocop_todo.yml +1 -13
- data/.travis.yml +26 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +118 -0
- data/Guardfile +1 -0
- data/README.md +96 -31
- data/Rakefile +2 -0
- data/exel.gemspec +7 -7
- data/lib/exel.rb +7 -1
- data/lib/exel/ast_node.rb +6 -10
- data/lib/exel/context.rb +4 -1
- data/lib/exel/deferred_context_value.rb +3 -1
- data/lib/exel/error/job_termination.rb +12 -0
- data/lib/exel/events.rb +6 -0
- data/lib/exel/instruction.rb +5 -2
- data/lib/exel/instruction_node.rb +2 -0
- data/lib/exel/job.rb +8 -4
- data/lib/exel/listen_instruction.rb +2 -0
- data/lib/exel/logging.rb +24 -1
- data/lib/exel/logging/logger_wrapper.rb +31 -0
- data/lib/exel/logging_helper.rb +36 -0
- data/lib/exel/middleware/chain.rb +67 -0
- data/lib/exel/middleware/logging.rb +30 -0
- data/lib/exel/null_instruction.rb +2 -0
- data/lib/exel/processor_helper.rb +9 -1
- data/lib/exel/processors/async_processor.rb +2 -8
- data/lib/exel/processors/run_processor.rb +2 -6
- data/lib/exel/processors/split_processor.rb +15 -10
- data/lib/exel/providers/local_file_provider.rb +9 -6
- data/lib/exel/providers/threaded_async_provider.rb +2 -0
- data/lib/exel/remote_value.rb +11 -0
- data/lib/exel/sequence_node.rb +2 -0
- data/lib/exel/value.rb +2 -0
- data/lib/exel/version.rb +3 -1
- data/spec/exel/ast_node_spec.rb +48 -27
- data/spec/exel/context_spec.rb +77 -77
- data/spec/exel/deferred_context_value_spec.rb +42 -42
- data/spec/exel/events_spec.rb +68 -59
- data/spec/exel/instruction_node_spec.rb +17 -16
- data/spec/exel/instruction_spec.rb +49 -42
- data/spec/exel/job_spec.rb +99 -84
- data/spec/exel/listen_instruction_spec.rb +11 -10
- data/spec/exel/logging/logger_wrapper_spec.rb +93 -0
- data/spec/exel/logging_helper_spec.rb +24 -0
- data/spec/exel/logging_spec.rb +69 -24
- data/spec/exel/middleware/chain_spec.rb +65 -0
- data/spec/exel/middleware/logging_spec.rb +31 -0
- data/spec/exel/middleware_spec.rb +68 -0
- data/spec/exel/null_instruction_spec.rb +4 -4
- data/spec/exel/processors/async_processor_spec.rb +17 -18
- data/spec/exel/processors/run_processor_spec.rb +10 -11
- data/spec/exel/processors/split_processor_spec.rb +99 -74
- data/spec/exel/providers/local_file_provider_spec.rb +26 -28
- data/spec/exel/providers/threaded_async_provider_spec.rb +37 -38
- data/spec/exel/sequence_node_spec.rb +12 -11
- data/spec/exel/value_spec.rb +33 -33
- data/spec/exel_spec.rb +9 -7
- data/spec/integration/integration_spec.rb +3 -1
- data/spec/spec_helper.rb +4 -2
- data/spec/support/integration_test_classes.rb +4 -3
- metadata +37 -48
@@ -1,15 +1,14 @@
|
|
1
|
-
|
2
|
-
module Processors
|
3
|
-
describe RunProcessor do
|
4
|
-
subject { RunProcessor.new(context) }
|
5
|
-
let(:context) { EXEL::Context.new(job: :test_job) }
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
3
|
+
describe EXEL::Processors::RunProcessor do
|
4
|
+
subject { EXEL::Processors::RunProcessor.new(context) }
|
5
|
+
|
6
|
+
let(:context) { EXEL::Context.new(job: :test_job) }
|
7
|
+
|
8
|
+
describe '#process' do
|
9
|
+
it 'runs the job named in context[:job] with the current context' do
|
10
|
+
expect(EXEL::Job).to receive(:run).with(:test_job, context)
|
11
|
+
subject.process
|
13
12
|
end
|
14
13
|
end
|
15
14
|
end
|
@@ -1,96 +1,121 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe EXEL::Processors::SplitProcessor do
|
4
|
+
subject(:splitter) { EXEL::Processors::SplitProcessor.new(context) }
|
5
|
+
|
6
|
+
let(:chunk_file) { instance_double(File) }
|
7
|
+
let(:file) { create_file(1) }
|
8
|
+
let(:context) { EXEL::Context.new(resource: file) }
|
9
|
+
let(:callback) { instance_double(EXEL::SequenceNode) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow_any_instance_of(StringIO).to receive(:path).and_return('/text.txt')
|
13
|
+
allow(File).to receive(:delete)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#process' do
|
17
|
+
let(:file) { create_file(3) }
|
18
|
+
|
19
|
+
it 'processes file with 3 lines line by line' do
|
20
|
+
allow(CSV).to receive(:foreach).and_yield('line0').and_yield('line1').and_yield('line2')
|
21
|
+
|
22
|
+
3.times do |i|
|
23
|
+
expect(splitter).to receive(:process_line).with("line#{i}", callback)
|
13
24
|
end
|
25
|
+
expect(splitter).to receive(:process_line).with(:eof, callback)
|
14
26
|
|
15
|
-
|
16
|
-
|
27
|
+
expect(File).to receive(:delete).with(file.path)
|
28
|
+
expect(file).to receive(:close)
|
17
29
|
|
18
|
-
|
19
|
-
|
30
|
+
splitter.process(callback)
|
31
|
+
end
|
20
32
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
expect(splitter).to receive(:process_line).with(:eof, callback)
|
33
|
+
it 'aborts parsing the csv file if it is malformed' do
|
34
|
+
allow(CSV).to receive(:foreach).and_raise(CSV::MalformedCSVError.new('message', '1'))
|
35
|
+
expect(splitter).to receive(:process_line).with(:eof, callback)
|
25
36
|
|
26
|
-
|
37
|
+
splitter.process(callback)
|
38
|
+
end
|
27
39
|
|
28
|
-
|
29
|
-
|
40
|
+
it 'does not delete the resource file if :delete_resource is set to false in the context' do
|
41
|
+
allow(CSV).to receive(:foreach).and_yield(:eof)
|
42
|
+
expect(File).not_to receive(:delete).with(file.path)
|
30
43
|
|
31
|
-
|
32
|
-
|
33
|
-
|
44
|
+
context[:delete_resource] = false
|
45
|
+
splitter.process(callback)
|
46
|
+
end
|
34
47
|
|
35
|
-
|
36
|
-
|
48
|
+
it 'stops splitting at :max_chunks if it is set in the context' do
|
49
|
+
allow(CSV).to receive(:foreach).and_yield(['line0']).and_yield(['line1']).and_yield(['line2'])
|
37
50
|
|
38
|
-
|
39
|
-
allow(CSV).to receive(:foreach).and_yield(:eof)
|
40
|
-
expect(File).not_to receive(:delete).with(file.path)
|
51
|
+
chunk_file = create_file(0)
|
41
52
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
53
|
+
allow(Tempfile).to receive(:new).and_return(chunk_file)
|
54
|
+
expect(callback).to receive(:run).once
|
55
|
+
allow_any_instance_of(StringIO).to receive(:path).and_return('test path')
|
46
56
|
|
47
|
-
|
48
|
-
[
|
49
|
-
{input: 1, chunks: %W(0\n)},
|
50
|
-
{input: 3, chunks: %W(0\n1\n 2\n)},
|
51
|
-
{input: 4, chunks: %W(0\n1\n 2\n3\n)}
|
52
|
-
].each do |data|
|
53
|
-
it "produces #{data[:chunks].size} chunks with #{data[:input]} input lines" do
|
54
|
-
context[:chunk_size] = 2
|
55
|
-
|
56
|
-
data[:chunks].each do |chunk|
|
57
|
-
expect(splitter).to receive(:generate_chunk).with(chunk).and_return(chunk_file)
|
58
|
-
expect(callback).to receive(:run).with(context) do
|
59
|
-
expect(context[:resource]).to eq(chunk_file)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
data[:input].times { |i| splitter.process_line([i.to_s], callback) }
|
64
|
-
splitter.process_line(:eof, callback)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
57
|
+
expect(File).to receive(:delete).with(file.path)
|
68
58
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
59
|
+
context[:chunk_size] = 2
|
60
|
+
context[:max_chunks] = 1
|
61
|
+
splitter.process(callback)
|
62
|
+
|
63
|
+
expect(chunk_file.read).to eq("line0\nline1\n")
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'ensures that the source file gets closed and deleted' do
|
67
|
+
allow(CSV).to receive(:foreach).and_raise(Interrupt)
|
68
|
+
|
69
|
+
expect(File).to receive(:delete).with(file.path)
|
70
|
+
expect(file).to receive(:close)
|
71
|
+
|
72
|
+
begin
|
73
|
+
splitter.process(callback)
|
74
|
+
rescue Interrupt
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
75
79
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
+
describe '#process_line' do
|
81
|
+
[
|
82
|
+
{input: 1, chunks: %W(0\n)},
|
83
|
+
{input: 3, chunks: %W(0\n1\n 2\n)},
|
84
|
+
{input: 4, chunks: %W(0\n1\n 2\n3\n)},
|
85
|
+
].each do |data|
|
86
|
+
it "produces #{data[:chunks].size} chunks with #{data[:input]} input lines" do
|
87
|
+
context[:chunk_size] = 2
|
88
|
+
|
89
|
+
data[:chunks].each do |chunk|
|
90
|
+
expect(splitter).to receive(:generate_chunk).with(chunk).and_return(chunk_file)
|
91
|
+
expect(callback).to receive(:run).with(context) do
|
92
|
+
expect(context[:resource]).to eq(chunk_file)
|
80
93
|
end
|
81
94
|
end
|
82
|
-
end
|
83
95
|
|
84
|
-
|
85
|
-
|
96
|
+
data[:input].times { |i| splitter.process_line([i.to_s], callback) }
|
97
|
+
splitter.process_line(:eof, callback)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
86
101
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
102
|
+
describe '#generate_chunk' do
|
103
|
+
it 'creates a file with the contents of the given string' do
|
104
|
+
file = splitter.generate_chunk('abc')
|
105
|
+
content = file.read
|
106
|
+
expect(content).to eq('abc')
|
107
|
+
end
|
91
108
|
|
92
|
-
|
109
|
+
it 'creates a file with a unique name' do
|
110
|
+
3.times do |i|
|
111
|
+
file = splitter.generate_chunk('content')
|
112
|
+
expect(file.path).to include("text_#{i + 1}_")
|
93
113
|
end
|
94
114
|
end
|
95
115
|
end
|
116
|
+
|
117
|
+
def create_file(lines)
|
118
|
+
content = Array.new(lines) { |i| CSV.generate_line(["line#{i}"]) }.join
|
119
|
+
StringIO.new(content)
|
120
|
+
end
|
96
121
|
end
|
@@ -1,36 +1,34 @@
|
|
1
|
-
|
2
|
-
module Providers
|
3
|
-
describe LocalFileProvider do
|
4
|
-
let(:file) { instance_double(File, path: '/path/to/file') }
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
expect(subject.upload(file)).to eq('file:///path/to/file')
|
9
|
-
end
|
10
|
-
end
|
3
|
+
describe EXEL::Providers::LocalFileProvider do
|
4
|
+
let(:file) { File.open(File.expand_path('../../../fixtures/sample.csv', __FILE__)) }
|
11
5
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
expect(subject.download('file:///path/to/file')).to eq(file)
|
16
|
-
end
|
6
|
+
it 'can upload/download a file' do
|
7
|
+
remote_value = subject.upload(file)
|
8
|
+
expect(remote_value.uri.path).to eq(file.path)
|
17
9
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
10
|
+
restored_file = subject.download(remote_value)
|
11
|
+
expect(restored_file.path).to eq(file.path)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'doesn`t accept URIs for schemes other than file://' do
|
15
|
+
expect { subject.download(RemoteValue.new(URI('s3://bucket/file'))) }.to raise_error "Unsupported URI scheme 's3'"
|
16
|
+
end
|
22
17
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
describe '.remote?' do
|
19
|
+
it 'returns true for remote values' do
|
20
|
+
expect(EXEL::Providers::LocalFileProvider.remote?(RemoteValue.new(URI('file:///path/to/file')))).to be_truthy
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns false for file:// URIs' do
|
24
|
+
expect(EXEL::Providers::LocalFileProvider.remote?('file:///path/to/file')).to be_falsey
|
25
|
+
expect(EXEL::Providers::LocalFileProvider.remote?(URI('file:///path/to/file'))).to be_falsey
|
26
|
+
end
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
33
|
-
end
|
28
|
+
it 'returns false for anything else' do
|
29
|
+
expect(EXEL::Providers::LocalFileProvider.remote?('s3://file')).to be_falsey
|
30
|
+
expect(EXEL::Providers::LocalFileProvider.remote?(1)).to be_falsey
|
31
|
+
expect(EXEL::Providers::LocalFileProvider.remote?(nil)).to be_falsey
|
34
32
|
end
|
35
33
|
end
|
36
34
|
end
|
@@ -1,53 +1,52 @@
|
|
1
|
-
|
2
|
-
module Providers
|
3
|
-
class ContextMutatingProcessor
|
4
|
-
def initialize(context)
|
5
|
-
@context = context
|
6
|
-
end
|
1
|
+
# frozen_string_literal: true
|
7
2
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
class ContextMutatingProcessor
|
4
|
+
def initialize(context)
|
5
|
+
@context = context
|
6
|
+
end
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
|
8
|
+
def process(_block)
|
9
|
+
@context[:array] << @context[:arg]
|
10
|
+
end
|
11
|
+
end
|
16
12
|
|
17
|
-
|
18
|
-
|
13
|
+
describe EXEL::Providers::ThreadedAsyncProvider do
|
14
|
+
subject { described_class.new(context) }
|
19
15
|
|
20
|
-
|
21
|
-
expect(dsl_block).to receive(:start).with(context)
|
22
|
-
expect(Thread).to receive(:new).and_yield
|
16
|
+
let(:context) { EXEL::Context.new }
|
23
17
|
|
24
|
-
|
25
|
-
|
18
|
+
describe '#do_async' do
|
19
|
+
let(:dsl_block) { instance_double(EXEL::ASTNode) }
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
it 'runs the block in a new thread' do
|
22
|
+
expect(dsl_block).to receive(:start).with(context)
|
23
|
+
expect(Thread).to receive(:new).and_yield
|
30
24
|
|
31
|
-
|
32
|
-
|
33
|
-
process with: ContextMutatingProcessor, arg: 1
|
34
|
-
complete += 1
|
35
|
-
end
|
36
|
-
|
37
|
-
async do
|
38
|
-
process with: ContextMutatingProcessor, arg: 2
|
39
|
-
complete += 1
|
40
|
-
end
|
41
|
-
end
|
25
|
+
subject.do_async(dsl_block)
|
26
|
+
end
|
42
27
|
|
43
|
-
|
28
|
+
it 'passes a copy of the context to each thread' do
|
29
|
+
context[:array] = []
|
30
|
+
complete = 0
|
44
31
|
|
45
|
-
|
46
|
-
|
32
|
+
EXEL::Job.define :thread_test do
|
33
|
+
async do
|
34
|
+
process with: ContextMutatingProcessor, arg: 1
|
35
|
+
complete += 1
|
36
|
+
end
|
47
37
|
|
48
|
-
|
38
|
+
async do
|
39
|
+
process with: ContextMutatingProcessor, arg: 2
|
40
|
+
complete += 1
|
49
41
|
end
|
50
42
|
end
|
43
|
+
|
44
|
+
EXEL::Job.run(:thread_test, context)
|
45
|
+
|
46
|
+
start_time = Time.now
|
47
|
+
sleep 0.1 while complete < 2 && Time.now - start_time < 2
|
48
|
+
|
49
|
+
expect(context[:array]).to be_empty
|
51
50
|
end
|
52
51
|
end
|
53
52
|
end
|
@@ -1,17 +1,18 @@
|
|
1
|
-
|
2
|
-
describe SequenceNode do
|
3
|
-
subject(:node) { described_class.new(instance_double(ASTNode), instance_double(ASTNode)) }
|
4
|
-
let(:context) { instance_double(EXEL::Context) }
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
3
|
+
describe EXEL::SequenceNode do
|
4
|
+
subject(:node) { described_class.new(instance_double(EXEL::ASTNode), instance_double(EXEL::ASTNode)) }
|
7
5
|
|
8
|
-
|
9
|
-
it 'runs each child node in sequence' do
|
10
|
-
expect(node.children.first).to receive(:run).with(context).once.ordered
|
11
|
-
expect(node.children.last).to receive(:run).with(context).once.ordered
|
6
|
+
let(:context) { instance_double(EXEL::Context) }
|
12
7
|
|
13
|
-
|
14
|
-
|
8
|
+
it { is_expected.to be_an(EXEL::ASTNode) }
|
9
|
+
|
10
|
+
describe '#run' do
|
11
|
+
it 'runs each child node in sequence' do
|
12
|
+
expect(node.children.first).to receive(:run).with(context).once.ordered
|
13
|
+
expect(node.children.last).to receive(:run).with(context).once.ordered
|
14
|
+
|
15
|
+
node.run(context)
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
data/spec/exel/value_spec.rb
CHANGED
@@ -1,50 +1,50 @@
|
|
1
|
-
|
2
|
-
describe Value do
|
3
|
-
let(:uri) { 's3://test_file.csv' }
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
3
|
+
describe EXEL::Value do
|
4
|
+
let(:uri) { 's3://test_file.csv' }
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
before { allow(EXEL).to receive(:remote_provider).and_return(EXEL::Providers::DummyRemoteProvider) }
|
7
|
+
|
8
|
+
describe '.remotize' do
|
9
|
+
context 'when the value is not a file' do
|
10
|
+
it 'returns the value' do
|
11
|
+
expect(EXEL::Value.remotize('test')).to eq('test')
|
12
12
|
end
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
[File, Tempfile].each do |file_class|
|
16
|
+
context "when the value is an instance of #{file_class}" do
|
17
|
+
let(:file) { instance_double(file_class) }
|
17
18
|
|
18
|
-
|
19
|
+
before { allow(file).to receive(:is_a?) { |klass| klass == file_class } }
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
it 'uploads the file using the remote provider' do
|
22
|
+
expect_any_instance_of(EXEL::Providers::DummyRemoteProvider).to receive(:upload).with(file)
|
23
|
+
EXEL::Value.remotize(file)
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
26
|
+
it 'returns the URI of the uploaded file' do
|
27
|
+
allow_any_instance_of(EXEL::Providers::DummyRemoteProvider).to receive(:upload).with(file).and_return(uri)
|
28
|
+
expect(EXEL::Value.remotize(file)).to eq(uri)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
34
|
+
describe '.localize' do
|
35
|
+
context 'with a local value' do
|
36
|
+
it 'returns the value' do
|
37
|
+
expect(EXEL::Value.localize('test')).to eq('test')
|
38
38
|
end
|
39
|
+
end
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
context 'with a remote file' do
|
42
|
+
it 'returns the downloaded file' do
|
43
|
+
expect(EXEL::Providers::DummyRemoteProvider).to receive(:remote?).with(uri).and_return(true)
|
44
|
+
file = double(:file)
|
45
|
+
expect_any_instance_of(EXEL::Providers::DummyRemoteProvider).to receive(:download).with(uri).and_return(file)
|
45
46
|
|
46
|
-
|
47
|
-
end
|
47
|
+
expect(EXEL::Value.localize(uri)).to eq(file)
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|