exel 1.2.1 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
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,24 +1,18 @@
1
- require_relative '../processor_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
5
  # Implements the +async+ instruction by using the configured async provider to run a block asynchronously.
6
6
  class AsyncProcessor
7
- include EXEL::ProcessorHelper
8
7
  attr_reader :provider
9
8
 
10
9
  def initialize(context)
11
10
  @context = context
12
11
  @provider = EXEL.async_provider.new(context)
13
-
14
- log_prefix_with '[AsyncProcessor]'
15
12
  end
16
13
 
17
14
  def process(block)
18
- log_process do
19
- @provider.do_async(block)
20
- log_info 'call to async completed'
21
- end
15
+ @provider.do_async(block)
22
16
  end
23
17
  end
24
18
  end
@@ -1,11 +1,9 @@
1
- require_relative '../processor_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module EXEL
4
4
  module Processors
5
5
  # Implements the +run+ instruction.
6
6
  class RunProcessor
7
- include EXEL::ProcessorHelper
8
-
9
7
  # Requires +context[:job]+ to contain the name of the job to be run.
10
8
  def initialize(context)
11
9
  @context = context
@@ -13,9 +11,7 @@ module EXEL
13
11
 
14
12
  # Runs the specified job with the current context
15
13
  def process(_block = nil)
16
- log_process "running job #{@context[:job]}" do
17
- EXEL::Job.run(@context[:job], @context)
18
- end
14
+ EXEL::Job.run(@context[:job], @context)
19
15
  end
20
16
  end
21
17
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'csv'
2
4
  require 'tempfile'
3
- require_relative '../processor_helper'
5
+ require_relative '../logging_helper'
4
6
 
5
7
  module EXEL
6
8
  module Processors
@@ -11,8 +13,10 @@ module EXEL
11
13
  # * +:delete_resource+ Defaults to true, can be set to false to preserve the original resource. Otherwise, it will
12
14
  # be deleted when splitting is complete
13
15
  # * +:chunk_size+ Set to specify the number of lines that each chunk should contain
16
+ # * +:max_chunks+ Set to specify the maximum number of chunks that should be processed. The resource will not be
17
+ # consumed beyond this limit.
14
18
  class SplitProcessor
15
- include EXEL::ProcessorHelper
19
+ include EXEL::LoggingHelper
16
20
 
17
21
  attr_accessor :file_name, :block
18
22
 
@@ -25,16 +29,16 @@ module EXEL
25
29
  @tempfile_count = 0
26
30
  @context = context
27
31
  @file = context[:resource]
32
+ @max_chunks = @context[:max_chunks] || Float::INFINITY
28
33
  @context[:delete_resource] = true if @context[:delete_resource].nil?
29
-
30
- log_prefix_with '[SplitProcessor]'
31
34
  end
32
35
 
33
36
  def process(callback)
34
- log_process do
35
- process_file(callback)
36
- finish(callback)
37
- end
37
+ process_file(callback)
38
+ finish(callback)
39
+ ensure
40
+ @file.close
41
+ File.delete(@file.path) if @context[:delete_resource]
38
42
  end
39
43
 
40
44
  def process_line(line, callback)
@@ -62,8 +66,10 @@ module EXEL
62
66
  def process_file(callback)
63
67
  csv_options = @context[:csv_options] || {col_sep: ','}
64
68
 
65
- CSV.foreach(@file.path, csv_options) do |line|
69
+ CSV.foreach(@file.path, **csv_options) do |line|
66
70
  process_line(line, callback)
71
+
72
+ break if @tempfile_count == @max_chunks
67
73
  end
68
74
  rescue CSV::MalformedCSVError => e
69
75
  log_error "CSV::MalformedCSVError => #{e.message}"
@@ -97,7 +103,6 @@ module EXEL
97
103
 
98
104
  def finish(callback)
99
105
  process_line(:eof, callback)
100
- File.delete(@file.path) if @context[:delete_resource]
101
106
  end
102
107
  end
103
108
  end
@@ -1,19 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
4
  module Providers
3
5
  # The default remote provider. Doesn't actually upload and download files to and from remote storage, but rather
4
6
  # just works with local files.
5
7
  class LocalFileProvider
6
8
  def upload(file)
7
- "file://#{file.path}"
9
+ RemoteValue.new(URI("file://#{File.absolute_path(file)}"))
8
10
  end
9
11
 
10
- def download(uri)
11
- raise 'URI must begin with "file://"' unless uri.start_with? 'file://'
12
- File.open(uri.split('file://').last)
12
+ def download(remote_value)
13
+ scheme = remote_value.uri.scheme
14
+ raise "Unsupported URI scheme '#{scheme}'" unless scheme == 'file'
15
+ File.open(remote_value.uri.path)
13
16
  end
14
17
 
15
- def self.remote?(uri)
16
- uri =~ %r{file://}
18
+ def self.remote?(value)
19
+ value.is_a?(RemoteValue)
17
20
  end
18
21
  end
19
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
4
  module Providers
3
5
  # The default remote provider. Provides async execution by running the given EXEL block in a new Thread
@@ -0,0 +1,11 @@
1
+ class RemoteValue
2
+ attr_reader :uri
3
+
4
+ def initialize(uri)
5
+ @uri = uri
6
+ end
7
+
8
+ def ==(other)
9
+ other.class == self.class && other.uri == @uri
10
+ end
11
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative './ast_node'
2
4
 
3
5
  module EXEL
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
4
  # Contains methods to handle remote and local values. Used for {Context} serialization
3
5
  module Value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EXEL
2
- VERSION = '1.2.1'.freeze
4
+ VERSION = '1.5.2'
3
5
  end
@@ -1,40 +1,61 @@
1
- module EXEL
2
- describe ASTNode do
3
- let(:context) { instance_double(EXEL::Context) }
1
+ # frozen_string_literal: true
4
2
 
5
- def instruction
6
- instance_double(Instruction, execute: nil)
7
- end
3
+ describe EXEL::ASTNode do
4
+ let(:context) { instance_double(EXEL::Context) }
5
+
6
+ def instruction
7
+ instance_double(EXEL::Instruction, execute: nil)
8
+ end
9
+
10
+ class TestNode < EXEL::ASTNode
11
+ end
8
12
 
9
- TestNode = Class.new(ASTNode)
13
+ describe '#start' do
14
+ context 'when a JobTermination error bubbles up' do
15
+ let(:node) { TestNode.new(instruction) }
16
+
17
+ before do
18
+ allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
19
+ end
20
+
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
10
25
 
11
- describe '#start' do
12
- context 'when an JobTermination error bubbles up' do
13
- it 'ensures the process fails silently' do
14
- node = TestNode.new(instruction)
15
- allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
16
- expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
17
- expect { node.start(context) }.not_to raise_error
26
+ it 'logs the error by default' do
27
+ expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
28
+ node.start(context)
29
+ end
30
+
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)
18
39
  end
19
40
  end
20
41
  end
42
+ end
21
43
 
22
- describe '#run' do
23
- it 'raises an error if not implemented' do
24
- expect { TestNode.new(instruction).run(context) }.to raise_error 'EXEL::TestNode does not implement #process'
25
- 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'
26
47
  end
48
+ end
27
49
 
28
- describe '#add_child' do
29
- it 'adds the given node to its children' do
30
- root = ASTNode.new(instruction)
31
- child_node = ASTNode.new(instruction)
32
- child_node2 = ASTNode.new(instruction)
33
- root.add_child(child_node)
34
- root.add_child(child_node2)
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)
35
57
 
36
- expect(root.children).to eq([child_node, child_node2])
37
- end
58
+ expect(root.children).to eq([child_node, child_node2])
38
59
  end
39
60
  end
40
61
  end
@@ -1,108 +1,108 @@
1
- module EXEL
2
- describe Context do
3
- subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
1
+ # frozen_string_literal: true
4
2
 
5
- it { is_expected.to be_a(Hash) }
3
+ describe EXEL::Context do
4
+ subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
6
5
 
7
- describe '#initialize' do
8
- it 'initializes with a hash' do
9
- expect(context[:key1]).to eq('1')
10
- expect(context[:key2]).to eq(2)
11
- expect(context[:key3]).to be_nil
12
- end
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
13
13
  end
14
+ end
14
15
 
15
- describe '#deep_dup' do
16
- it 'returns a deep copy of itself' do
17
- context[:a] = {nested: []}
16
+ describe '#deep_dup' do
17
+ it 'returns a deep copy of itself' do
18
+ context[:a] = {nested: []}
18
19
 
19
- dup = context.deep_dup
20
- expect(context).to eq(dup)
21
- expect(context).not_to be_equal(dup)
20
+ dup = context.deep_dup
21
+ expect(context).to eq(dup)
22
+ expect(context).not_to be_equal(dup)
22
23
 
23
- dup[:a][:nested] << 1
24
- expect(context[:a][:nested]).to be_empty
25
- end
24
+ dup[:a][:nested] << 1
25
+ expect(context[:a][:nested]).to be_empty
26
26
  end
27
+ end
27
28
 
28
- describe '#serialize' do
29
- before { allow(Value).to receive(:upload) }
30
-
31
- it 'writes the serialized context to a file and upload it' do
32
- expect(Value).to receive(:remotize).with(context[:key1]).and_return('remote_value1')
33
- expect(Value).to receive(:remotize).with(context[:key2]).and_return('remote_value2')
29
+ describe '#serialize' do
30
+ before { allow(EXEL::Value).to receive(:upload) }
34
31
 
35
- expect(SecureRandom).to receive(:uuid).and_return('uuid')
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')
36
35
 
37
- expect(Value).to receive(:remotize) do |file|
38
- expect(file.read).to eq(Marshal.dump(Context.new(key1: 'remote_value1', key2: 'remote_value2')))
39
- expect(file.path).to include('uuid')
40
- 'file_uri'
41
- end
36
+ expect(SecureRandom).to receive(:uuid).and_return('uuid')
42
37
 
43
- expect(context.serialize).to eq('file_uri')
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'
44
42
  end
45
43
 
46
- it 'does not mutate the current context' do
47
- allow(Value).to receive(:remotize).and_return('remote_value')
48
- original_table = context.dup
49
- context.serialize
50
- expect(context).to eq(original_table)
51
- end
44
+ expect(context.serialize).to eq('file_uri')
52
45
  end
53
46
 
54
- describe '.deserialize' do
55
- it 'deserializes a given uri' do
56
- file = StringIO.new(Marshal.dump(context))
57
- expect(Value).to receive(:localize).with('uri').and_return(file)
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
58
54
 
59
- expect(Context.deserialize('uri')).to eq(context)
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)
60
59
 
61
- expect(file).to be_closed
62
- end
60
+ expect(EXEL::Context.deserialize('uri')).to eq(context)
61
+
62
+ expect(file).to be_closed
63
63
  end
64
+ end
64
65
 
65
- shared_examples 'a reader method' do
66
- subject(:context) { EXEL::Context.new(key: 'value') }
66
+ shared_examples 'a reader method' do
67
+ subject(:context) { EXEL::Context.new(key: 'value') }
67
68
 
68
- it 'returns the value' do
69
- expect(context.send(method, :key)).to eq('value')
70
- end
69
+ it 'returns the value' do
70
+ expect(context.send(method, :key)).to eq('value')
71
+ end
71
72
 
72
- it 'localizes the returned value' do
73
- expect(Value).to receive(:localize).with('value').and_return('localized')
74
- expect(context.send(method, :key)).to eq('localized')
75
- end
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
76
77
 
77
- it 'stores the localized value' do
78
- allow(Value).to receive(:localize).with('value').and_return('localized')
79
- context.send(method, :key)
80
- allow(Value).to receive(:localize).with('localized').and_return('localized')
81
- context.send(method, :key)
82
- end
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
83
84
 
84
- it 'looks up deferred values' do
85
- # eq(context) as an argument matcher is necessary to prevent RSpec from calling fetch on the context, leading to
86
- # a stack overflow
87
- expect(DeferredContextValue).to receive(:resolve).with('value', eq(context)).and_return('resolved')
88
- expect(context.send(method, :key)).to eq('resolved')
89
- 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')
90
90
  end
91
+ end
91
92
 
92
- describe '#[]' do
93
- it_behaves_like 'a reader method' do
94
- let(:method) { :[] }
95
- end
93
+ describe '#[]' do
94
+ it_behaves_like 'a reader method' do
95
+ let(:method) { :[] }
96
96
  end
97
+ end
97
98
 
98
- describe '#fetch' do
99
- it 'raises an exception if the key is not found' do
100
- expect { context.fetch(:unknown) }.to raise_error(KeyError)
101
- end
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
102
103
 
103
- it_behaves_like 'a reader method' do
104
- let(:method) { :fetch }
105
- end
104
+ it_behaves_like 'a reader method' do
105
+ let(:method) { :fetch }
106
106
  end
107
107
  end
108
108
  end