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.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +4 -4
  3. data/.gitignore +1 -2
  4. data/.rubocop.yml +23 -14
  5. data/.rubocop_airbnb.yml +2 -0
  6. data/.rubocop_todo.yml +1 -13
  7. data/.travis.yml +26 -0
  8. data/Gemfile +2 -2
  9. data/Gemfile.lock +118 -0
  10. data/Guardfile +1 -0
  11. data/README.md +96 -31
  12. data/Rakefile +2 -0
  13. data/exel.gemspec +7 -7
  14. data/lib/exel.rb +7 -1
  15. data/lib/exel/ast_node.rb +6 -10
  16. data/lib/exel/context.rb +4 -1
  17. data/lib/exel/deferred_context_value.rb +3 -1
  18. data/lib/exel/error/job_termination.rb +12 -0
  19. data/lib/exel/events.rb +6 -0
  20. data/lib/exel/instruction.rb +5 -2
  21. data/lib/exel/instruction_node.rb +2 -0
  22. data/lib/exel/job.rb +8 -4
  23. data/lib/exel/listen_instruction.rb +2 -0
  24. data/lib/exel/logging.rb +24 -1
  25. data/lib/exel/logging/logger_wrapper.rb +31 -0
  26. data/lib/exel/logging_helper.rb +36 -0
  27. data/lib/exel/middleware/chain.rb +67 -0
  28. data/lib/exel/middleware/logging.rb +30 -0
  29. data/lib/exel/null_instruction.rb +2 -0
  30. data/lib/exel/processor_helper.rb +9 -1
  31. data/lib/exel/processors/async_processor.rb +2 -8
  32. data/lib/exel/processors/run_processor.rb +2 -6
  33. data/lib/exel/processors/split_processor.rb +15 -10
  34. data/lib/exel/providers/local_file_provider.rb +9 -6
  35. data/lib/exel/providers/threaded_async_provider.rb +2 -0
  36. data/lib/exel/remote_value.rb +11 -0
  37. data/lib/exel/sequence_node.rb +2 -0
  38. data/lib/exel/value.rb +2 -0
  39. data/lib/exel/version.rb +3 -1
  40. data/spec/exel/ast_node_spec.rb +48 -27
  41. data/spec/exel/context_spec.rb +77 -77
  42. data/spec/exel/deferred_context_value_spec.rb +42 -42
  43. data/spec/exel/events_spec.rb +68 -59
  44. data/spec/exel/instruction_node_spec.rb +17 -16
  45. data/spec/exel/instruction_spec.rb +49 -42
  46. data/spec/exel/job_spec.rb +99 -84
  47. data/spec/exel/listen_instruction_spec.rb +11 -10
  48. data/spec/exel/logging/logger_wrapper_spec.rb +93 -0
  49. data/spec/exel/logging_helper_spec.rb +24 -0
  50. data/spec/exel/logging_spec.rb +69 -24
  51. data/spec/exel/middleware/chain_spec.rb +65 -0
  52. data/spec/exel/middleware/logging_spec.rb +31 -0
  53. data/spec/exel/middleware_spec.rb +68 -0
  54. data/spec/exel/null_instruction_spec.rb +4 -4
  55. data/spec/exel/processors/async_processor_spec.rb +17 -18
  56. data/spec/exel/processors/run_processor_spec.rb +10 -11
  57. data/spec/exel/processors/split_processor_spec.rb +99 -74
  58. data/spec/exel/providers/local_file_provider_spec.rb +26 -28
  59. data/spec/exel/providers/threaded_async_provider_spec.rb +37 -38
  60. data/spec/exel/sequence_node_spec.rb +12 -11
  61. data/spec/exel/value_spec.rb +33 -33
  62. data/spec/exel_spec.rb +9 -7
  63. data/spec/integration/integration_spec.rb +3 -1
  64. data/spec/spec_helper.rb +4 -2
  65. data/spec/support/integration_test_classes.rb +4 -3
  66. metadata +37 -48
@@ -1,15 +1,14 @@
1
- module EXEL
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
- describe '#process' do
8
- it 'runs the job named in context[:job] with the current context' do
9
- expect(EXEL::Job).to receive(:run).with(:test_job, context)
10
- subject.process
11
- end
12
- end
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
- module EXEL
2
- module Processors
3
- describe SplitProcessor do
4
- let(:chunk_file) { instance_double(File) }
5
- let(:file) { create_file(1) }
6
- let(:context) { Context.new(resource: file) }
7
- let(:callback) { instance_double(SequenceNode) }
8
- subject(:splitter) { SplitProcessor.new(context) }
9
-
10
- before do
11
- allow_any_instance_of(StringIO).to receive(:path).and_return('/text.txt')
12
- allow(File).to receive(:delete)
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
- describe '#process' do
16
- let(:file) { create_file(3) }
27
+ expect(File).to receive(:delete).with(file.path)
28
+ expect(file).to receive(:close)
17
29
 
18
- it 'processes file with 3 lines line by line' do
19
- allow(CSV).to receive(:foreach).and_yield('line0').and_yield('line1').and_yield('line2')
30
+ splitter.process(callback)
31
+ end
20
32
 
21
- 3.times do |i|
22
- expect(splitter).to receive(:process_line).with("line#{i}", callback)
23
- end
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
- expect(File).to receive(:delete).with(file.path)
37
+ splitter.process(callback)
38
+ end
27
39
 
28
- splitter.process(callback)
29
- end
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
- it 'aborts parsing the csv file if it is malformed' do
32
- allow(CSV).to receive(:foreach).and_raise(CSV::MalformedCSVError)
33
- expect(splitter).to receive(:process_line).with(:eof, callback)
44
+ context[:delete_resource] = false
45
+ splitter.process(callback)
46
+ end
34
47
 
35
- splitter.process(callback)
36
- end
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
- it 'does not delete the resource file if :delete_resource is set to false in the context' do
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
- context[:delete_resource] = false
43
- splitter.process(callback)
44
- end
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
- describe '#process_line' do
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
- describe '#generate_chunk' do
70
- it 'creates a file with the contents of the given string' do
71
- file = splitter.generate_chunk('abc')
72
- content = file.read
73
- expect(content).to eq('abc')
74
- end
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
- it 'creates a file with a unique name' do
77
- 3.times do |i|
78
- file = splitter.generate_chunk('content')
79
- expect(file.path).to include("text_#{i + 1}_")
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
- def create_file(lines)
85
- content = ''
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
- lines.times do |i|
88
- line = CSV.generate_line(["line#{i}"])
89
- content << line
90
- end
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
- StringIO.new(content)
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
- module EXEL
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
- describe '#upload' do
7
- it 'returns a file:// URI for the file' do
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
- describe '#download' do
13
- it 'returns the file indicated by the URI' do
14
- expect(File).to receive(:open).with('/path/to/file').and_return(file)
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
- it 'doesn`t accept URIs for schemes other than file://' do
19
- expect { subject.download('s3://') }.to raise_error 'URI must begin with "file://"'
20
- end
21
- end
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
- describe '.remote?' do
24
- it 'returns true for file:// URIs' do
25
- expect(LocalFileProvider.remote?('file:///path/to/file')).to be_truthy
26
- end
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
- it 'returns false for anything else' do
29
- expect(LocalFileProvider.remote?('s3://file')).to be_falsey
30
- expect(LocalFileProvider.remote?(1)).to be_falsey
31
- expect(LocalFileProvider.remote?(nil)).to be_falsey
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
- module EXEL
2
- module Providers
3
- class ContextMutatingProcessor
4
- def initialize(context)
5
- @context = context
6
- end
1
+ # frozen_string_literal: true
7
2
 
8
- def process(_block)
9
- @context[:array] << @context[:arg]
10
- end
11
- end
3
+ class ContextMutatingProcessor
4
+ def initialize(context)
5
+ @context = context
6
+ end
12
7
 
13
- describe ThreadedAsyncProvider do
14
- subject { described_class.new(context) }
15
- let(:context) { EXEL::Context.new }
8
+ def process(_block)
9
+ @context[:array] << @context[:arg]
10
+ end
11
+ end
16
12
 
17
- describe '#do_async' do
18
- let(:dsl_block) { instance_double(ASTNode) }
13
+ describe EXEL::Providers::ThreadedAsyncProvider do
14
+ subject { described_class.new(context) }
19
15
 
20
- it 'runs the block in a new thread' do
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
- subject.do_async(dsl_block)
25
- end
18
+ describe '#do_async' do
19
+ let(:dsl_block) { instance_double(EXEL::ASTNode) }
26
20
 
27
- it 'passes a copy of the context to each thread' do
28
- context[:array] = []
29
- complete = 0
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
- EXEL::Job.define :thread_test do
32
- async do
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
- EXEL::Job.run(:thread_test, context)
28
+ it 'passes a copy of the context to each thread' do
29
+ context[:array] = []
30
+ complete = 0
44
31
 
45
- start_time = Time.now
46
- sleep 0.1 while complete < 2 && Time.now - start_time < 2
32
+ EXEL::Job.define :thread_test do
33
+ async do
34
+ process with: ContextMutatingProcessor, arg: 1
35
+ complete += 1
36
+ end
47
37
 
48
- expect(context[:array]).to be_empty
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
- module EXEL
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
- it { is_expected.to be_an(ASTNode) }
3
+ describe EXEL::SequenceNode do
4
+ subject(:node) { described_class.new(instance_double(EXEL::ASTNode), instance_double(EXEL::ASTNode)) }
7
5
 
8
- describe '#run' do
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
- node.run(context)
14
- end
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
@@ -1,50 +1,50 @@
1
- module EXEL
2
- describe Value do
3
- let(:uri) { 's3://test_file.csv' }
1
+ # frozen_string_literal: true
4
2
 
5
- before { allow(EXEL).to receive(:remote_provider).and_return(EXEL::Providers::DummyRemoteProvider) }
3
+ describe EXEL::Value do
4
+ let(:uri) { 's3://test_file.csv' }
6
5
 
7
- describe '.remotize' do
8
- context 'when the value is not a file' do
9
- it 'returns the value' do
10
- expect(Value.remotize('test')).to eq('test')
11
- end
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
- [File, Tempfile].each do |file_class|
15
- context "when the value is an instance of #{file_class}" do
16
- let(:file) { instance_double(file_class) }
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
- before { allow(file).to receive(:is_a?) { |klass| klass == file_class } }
19
+ before { allow(file).to receive(:is_a?) { |klass| klass == file_class } }
19
20
 
20
- it 'uploads the file using the remote provider' do
21
- expect_any_instance_of(EXEL::Providers::DummyRemoteProvider).to receive(:upload).with(file)
22
- Value.remotize(file)
23
- end
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
- it 'returns the URI of the uploaded file' do
26
- allow_any_instance_of(EXEL::Providers::DummyRemoteProvider).to receive(:upload).with(file).and_return(uri)
27
- expect(Value.remotize(file)).to eq(uri)
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
- describe '.localize' do
34
- context 'with a local value' do
35
- it 'returns the value' do
36
- expect(Value.localize('test')).to eq('test')
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
- context 'with a remote file' do
41
- it 'returns the downloaded file' do
42
- expect(EXEL::Providers::DummyRemoteProvider).to receive(:remote?).with(uri).and_return(true)
43
- file = double(:file)
44
- expect_any_instance_of(EXEL::Providers::DummyRemoteProvider).to receive(:download).with(uri).and_return(file)
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
- expect(Value.localize(uri)).to eq(file)
47
- end
47
+ expect(EXEL::Value.localize(uri)).to eq(file)
48
48
  end
49
49
  end
50
50
  end