exel 1.1.0 → 1.2.0
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 +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +10 -16
- data/Gemfile +2 -0
- data/README.md +5 -2
- data/exel.gemspec +2 -1
- data/lib/exel/ast_node.rb +2 -1
- data/lib/exel/context.rb +45 -42
- data/lib/exel/deferred_context_value.rb +35 -0
- data/lib/exel/error/job_termination.rb +3 -5
- data/lib/exel/events.rb +26 -0
- data/lib/exel/instruction.rb +2 -4
- data/lib/exel/instruction_node.rb +1 -0
- data/lib/exel/job.rb +32 -10
- data/lib/exel/listen_instruction.rb +17 -0
- data/lib/exel/null_instruction.rb +1 -0
- data/lib/exel/old_context.rb +109 -0
- data/lib/exel/processor_helper.rb +1 -4
- data/lib/exel/processors/async_processor.rb +1 -0
- data/lib/exel/processors/run_processor.rb +3 -0
- data/lib/exel/processors/split_processor.rb +12 -2
- data/lib/exel/providers/local_file_provider.rb +3 -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 +1 -1
- data/lib/exel.rb +20 -1
- data/spec/exel/ast_node_spec.rb +4 -4
- data/spec/exel/context_spec.rb +35 -109
- data/spec/exel/deferred_context_value_spec.rb +51 -7
- data/spec/exel/events_spec.rb +89 -0
- data/spec/exel/instruction_node_spec.rb +3 -3
- data/spec/exel/instruction_spec.rb +9 -9
- data/spec/exel/job_spec.rb +23 -13
- data/spec/exel/listen_instruction_spec.rb +14 -0
- data/spec/exel/logging_spec.rb +3 -3
- data/spec/exel/processors/split_processor_spec.rb +14 -6
- data/spec/exel/sequence_node_spec.rb +1 -1
- data/spec/exel_spec.rb +7 -0
- data/spec/fixtures/sample.csv +501 -0
- data/spec/integration/integration_spec.rb +51 -0
- data/spec/spec_helper.rb +17 -1
- data/spec/support/integration_test_classes.rb +44 -0
- metadata +32 -5
@@ -4,18 +4,28 @@ require_relative '../processor_helper'
|
|
4
4
|
|
5
5
|
module EXEL
|
6
6
|
module Processors
|
7
|
+
# Implements the +split+ instruction. Used to concurrently process a large file by splitting it into small chunks to
|
8
|
+
# be separately processed.
|
9
|
+
#
|
10
|
+
# =Supported Context Options
|
11
|
+
# * +:delete_resource+ Defaults to true, can be set to false to preserve the original resource. Otherwise, it will
|
12
|
+
# be deleted when splitting is complete
|
13
|
+
# * +:chunk_size+ Set to specify the number of lines that each chunk should contain
|
7
14
|
class SplitProcessor
|
8
15
|
include EXEL::ProcessorHelper
|
9
16
|
|
10
17
|
attr_accessor :file_name, :block
|
11
18
|
|
19
|
+
# Number of lines to include in each chunk. Can be overridden by setting :chunk_size in the context
|
12
20
|
DEFAULT_CHUNK_SIZE = 1000
|
13
21
|
|
22
|
+
# The context must contain a CSV File object in context[:resource]
|
14
23
|
def initialize(context)
|
15
24
|
@buffer = []
|
16
25
|
@tempfile_count = 0
|
17
26
|
@context = context
|
18
27
|
@file = context[:resource]
|
28
|
+
@context[:delete_resource] = true if @context[:delete_resource].nil?
|
19
29
|
|
20
30
|
log_prefix_with '[SplitProcessor]'
|
21
31
|
end
|
@@ -73,7 +83,7 @@ module EXEL
|
|
73
83
|
end
|
74
84
|
|
75
85
|
def chunk_size
|
76
|
-
DEFAULT_CHUNK_SIZE
|
86
|
+
@context[:chunk_size] || DEFAULT_CHUNK_SIZE
|
77
87
|
end
|
78
88
|
|
79
89
|
def chunk_filename
|
@@ -87,7 +97,7 @@ module EXEL
|
|
87
97
|
|
88
98
|
def finish(callback)
|
89
99
|
process_line(:eof, callback)
|
90
|
-
File.delete(@file.path)
|
100
|
+
File.delete(@file.path) if @context[:delete_resource]
|
91
101
|
end
|
92
102
|
end
|
93
103
|
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
module EXEL
|
2
2
|
module Providers
|
3
|
+
# The default remote provider. Doesn't actually upload and download files to and from remote storage, but rather
|
4
|
+
# just works with local files.
|
3
5
|
class LocalFileProvider
|
4
6
|
def upload(file)
|
5
7
|
"file://#{file.path}"
|
6
8
|
end
|
7
9
|
|
8
10
|
def download(uri)
|
9
|
-
|
11
|
+
raise 'URI must begin with "file://"' unless uri.start_with? 'file://'
|
10
12
|
File.open(uri.split('file://').last)
|
11
13
|
end
|
12
14
|
|
data/lib/exel/sequence_node.rb
CHANGED
data/lib/exel/value.rb
CHANGED
data/lib/exel/version.rb
CHANGED
data/lib/exel.rb
CHANGED
@@ -2,31 +2,50 @@ require 'exel/version'
|
|
2
2
|
require 'exel/logging'
|
3
3
|
require 'ostruct'
|
4
4
|
|
5
|
+
# Provides methods to configure EXEL
|
5
6
|
module EXEL
|
7
|
+
# @return The currently set logger
|
6
8
|
def self.logger
|
7
9
|
EXEL::Logging.logger
|
8
10
|
end
|
9
11
|
|
12
|
+
# Sets the logger to be used.
|
13
|
+
#
|
14
|
+
# @param [Logger] The logger to set. Must comply with the Ruby Logger interface
|
10
15
|
def self.logger=(logger)
|
11
16
|
EXEL::Logging.logger = logger
|
12
17
|
end
|
13
18
|
|
19
|
+
# @return The current configuration
|
14
20
|
def self.configuration
|
15
21
|
@config ||= OpenStruct.new
|
16
22
|
end
|
17
23
|
|
24
|
+
# Yields the configuration object to the given block. Configuration can include:
|
25
|
+
# * +async_provider+ Set an async provider. Defaults to EXEL::Providers::ThreadedAsyncProvider
|
26
|
+
# * +remote_provider+ Set a remote provider. Defaults to EXEL::Providers::LocalFileProvider
|
27
|
+
# * Any configuration required by the async/remote providers
|
28
|
+
#
|
29
|
+
# Typically, async_provider and remote_provider will be automatically set upon requiring those gems.
|
30
|
+
#
|
31
|
+
# Example:
|
32
|
+
# EXEL.configure do |config|
|
33
|
+
# config.s3_bucket = 'my_bucket'
|
34
|
+
# end
|
18
35
|
def self.configure
|
19
36
|
yield configuration
|
20
37
|
end
|
21
38
|
|
39
|
+
# @return The currently configured async provider. Defaults to EXEL::Providers::ThreadedAsyncProvider
|
22
40
|
def self.async_provider
|
23
41
|
configuration.async_provider || Providers::ThreadedAsyncProvider
|
24
42
|
end
|
25
43
|
|
44
|
+
# @return The currently configured remote provider. Defaults to EXEL::Providers::LocalFileProvider
|
26
45
|
def self.remote_provider
|
27
46
|
configuration.remote_provider || Providers::LocalFileProvider
|
28
47
|
end
|
29
48
|
|
30
49
|
root = File.expand_path('../..', __FILE__)
|
31
|
-
Dir[File.join(root, 'lib/exel/**/*.rb')].each { |file| require file }
|
50
|
+
Dir[File.join(root, 'lib/exel/**/*.rb')].reject { |file| file.include?('old_context') }.each { |file| require file }
|
32
51
|
end
|
data/spec/exel/ast_node_spec.rb
CHANGED
@@ -10,23 +10,23 @@ module EXEL
|
|
10
10
|
|
11
11
|
describe '#start' do
|
12
12
|
context 'when an JobTermination error bubbles up' do
|
13
|
-
it '
|
13
|
+
it 'ensures the process fails silently' do
|
14
14
|
node = TestNode.new(instruction)
|
15
15
|
allow(node).to receive(:run).and_raise(EXEL::Error::JobTermination, 'Error')
|
16
16
|
expect(EXEL.logger).to receive(:error).with('JobTerminationError: Error')
|
17
|
-
expect { node.start(context) }.
|
17
|
+
expect { node.start(context) }.not_to raise_error
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
describe '#run' do
|
23
|
-
it '
|
23
|
+
it 'raises an error if not implemented' do
|
24
24
|
expect { TestNode.new(instruction).run(context) }.to raise_error 'EXEL::TestNode does not implement #process'
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
28
|
describe '#add_child' do
|
29
|
-
it '
|
29
|
+
it 'adds the given node to its children' do
|
30
30
|
root = ASTNode.new(instruction)
|
31
31
|
child_node = ASTNode.new(instruction)
|
32
32
|
child_node2 = ASTNode.new(instruction)
|
data/spec/exel/context_spec.rb
CHANGED
@@ -2,10 +2,13 @@ module EXEL
|
|
2
2
|
describe Context do
|
3
3
|
subject(:context) { EXEL::Context.new(key1: '1', key2: 2) }
|
4
4
|
|
5
|
+
it { is_expected.to be_a(Hash) }
|
6
|
+
|
5
7
|
describe '#initialize' do
|
6
|
-
it '
|
7
|
-
expect(context
|
8
|
-
expect(context
|
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
|
9
12
|
end
|
10
13
|
end
|
11
14
|
|
@@ -15,7 +18,7 @@ module EXEL
|
|
15
18
|
|
16
19
|
dup = context.deep_dup
|
17
20
|
expect(context).to eq(dup)
|
18
|
-
expect(context).
|
21
|
+
expect(context).not_to be_equal(dup)
|
19
22
|
|
20
23
|
dup[:a][:nested] << 1
|
21
24
|
expect(context[:a][:nested]).to be_empty
|
@@ -25,7 +28,7 @@ module EXEL
|
|
25
28
|
describe '#serialize' do
|
26
29
|
before { allow(Value).to receive(:upload) }
|
27
30
|
|
28
|
-
it '
|
31
|
+
it 'writes the serialized context to a file and upload it' do
|
29
32
|
expect(Value).to receive(:remotize).with(context[:key1]).and_return('remote_value1')
|
30
33
|
expect(Value).to receive(:remotize).with(context[:key2]).and_return('remote_value2')
|
31
34
|
|
@@ -40,15 +43,16 @@ module EXEL
|
|
40
43
|
expect(context.serialize).to eq('file_uri')
|
41
44
|
end
|
42
45
|
|
43
|
-
it '
|
44
|
-
|
46
|
+
it 'does not mutate the current context' do
|
47
|
+
allow(Value).to receive(:remotize).and_return('remote_value')
|
48
|
+
original_table = context.dup
|
45
49
|
context.serialize
|
46
|
-
expect(context
|
50
|
+
expect(context).to eq(original_table)
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
50
54
|
describe '.deserialize' do
|
51
|
-
it '
|
55
|
+
it 'deserializes a given uri' do
|
52
56
|
file = StringIO.new(Marshal.dump(context))
|
53
57
|
expect(Value).to receive(:localize).with('uri').and_return(file)
|
54
58
|
|
@@ -58,124 +62,46 @@ module EXEL
|
|
58
62
|
end
|
59
63
|
end
|
60
64
|
|
61
|
-
|
65
|
+
shared_examples 'a reader method' do
|
62
66
|
subject(:context) { EXEL::Context.new(key: 'value') }
|
63
67
|
|
64
|
-
it '
|
65
|
-
expect(context
|
68
|
+
it 'returns the value' do
|
69
|
+
expect(context.send(method, :key)).to eq('value')
|
66
70
|
end
|
67
71
|
|
68
|
-
it '
|
72
|
+
it 'localizes the returned value' do
|
69
73
|
expect(Value).to receive(:localize).with('value').and_return('localized')
|
70
|
-
expect(context
|
74
|
+
expect(context.send(method, :key)).to eq('localized')
|
71
75
|
end
|
72
76
|
|
73
|
-
it '
|
77
|
+
it 'stores the localized value' do
|
74
78
|
allow(Value).to receive(:localize).with('value').and_return('localized')
|
75
|
-
context
|
76
|
-
|
79
|
+
context.send(method, :key)
|
80
|
+
allow(Value).to receive(:localize).with('localized').and_return('localized')
|
81
|
+
context.send(method, :key)
|
77
82
|
end
|
78
83
|
|
79
|
-
|
80
|
-
context
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
expect(context[:deferred_value]).to eq(context[:key])
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
context 'in an array' do
|
89
|
-
it 'should return the lookup value from the context' do
|
90
|
-
deferred_context_value = DeferredContextValue.new[:key]
|
91
|
-
context[:array] = [1, 2, deferred_context_value]
|
92
|
-
expect(context[:array]).to eq([1, 2, context[:key]])
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
context 'in a hash' do
|
97
|
-
it 'should return the lookup value from the context' do
|
98
|
-
deferred_context_value = DeferredContextValue.new[:key]
|
99
|
-
context[:hash] = {hash_key: deferred_context_value}
|
100
|
-
expect(context[:hash]).to eq(hash_key: context[:key])
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
context 'in nested arrays and hashes' do
|
105
|
-
it 'should lookup a deferred context value in a hash nested in an array' do
|
106
|
-
deferred_context_value = DeferredContextValue.new[:key]
|
107
|
-
context[:nested] = [{}, {hash_key: deferred_context_value}]
|
108
|
-
expect(context[:nested]).to eq([{}, {hash_key: context[:key]}])
|
109
|
-
end
|
110
|
-
|
111
|
-
it 'should lookup a deferred context value in an array nested in a hash' do
|
112
|
-
deferred_context_value = DeferredContextValue.new[:key]
|
113
|
-
context[:nested] = {hash_key: [1, deferred_context_value]}
|
114
|
-
expect(context[:nested]).to eq(hash_key: [1, context[:key]])
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
describe '#[]=' do
|
121
|
-
it 'should add the key/value pair to table' do
|
122
|
-
context[:new_key] = 'new_value'
|
123
|
-
expect(context.table[:new_key]).to eq('new_value')
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
describe '#delete' do
|
128
|
-
it 'should delete the key/value pair from the table' do
|
129
|
-
context[:key] = 'value'
|
130
|
-
context[:key2] = 'value2'
|
131
|
-
context.delete(:key)
|
132
|
-
expect(context.table.keys).to_not include(:key)
|
133
|
-
expect(context.table.keys).to include(:key2)
|
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')
|
134
89
|
end
|
135
90
|
end
|
136
91
|
|
137
|
-
describe '#
|
138
|
-
|
139
|
-
|
140
|
-
context.table[:existing] = 'existing'
|
141
|
-
|
142
|
-
context.merge!(overwrite: 'changed', new: 'new')
|
143
|
-
|
144
|
-
expect(context.table[:overwrite]).to eq('changed')
|
145
|
-
expect(context.table[:existing]).to eq('existing')
|
146
|
-
expect(context.table[:new]).to eq('new')
|
147
|
-
end
|
148
|
-
|
149
|
-
it 'should return itself' do
|
150
|
-
expect(context.merge!(key: 'value')).to eq(context)
|
92
|
+
describe '#[]' do
|
93
|
+
it_behaves_like 'a reader method' do
|
94
|
+
let(:method) { :[] }
|
151
95
|
end
|
152
96
|
end
|
153
97
|
|
154
|
-
describe '
|
155
|
-
it
|
156
|
-
|
157
|
-
it { is_expected.to eq(context) }
|
158
|
-
|
159
|
-
it { is_expected.to_not eq(42) }
|
160
|
-
|
161
|
-
it { is_expected.to_not eq(Context.new(other_key: 'value')) }
|
162
|
-
|
163
|
-
it { is_expected.to eq(context.dup) }
|
164
|
-
end
|
165
|
-
|
166
|
-
describe 'include?' do
|
167
|
-
subject(:context) { EXEL::Context.new(key1: 1, key2: 2, key3: 3) }
|
168
|
-
|
169
|
-
context 'context contains all key value pairs' do
|
170
|
-
it 'should return true' do
|
171
|
-
expect(context).to include(key1: 1, key2: 2)
|
172
|
-
end
|
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)
|
173
101
|
end
|
174
102
|
|
175
|
-
|
176
|
-
|
177
|
-
expect(context).not_to include(foo: 'bar', key2: 2)
|
178
|
-
end
|
103
|
+
it_behaves_like 'a reader method' do
|
104
|
+
let(:method) { :fetch }
|
179
105
|
end
|
180
106
|
end
|
181
107
|
end
|
@@ -1,20 +1,64 @@
|
|
1
1
|
module EXEL
|
2
2
|
describe DeferredContextValue do
|
3
|
-
subject(:
|
3
|
+
subject(:deferred_value) { DeferredContextValue.new }
|
4
|
+
|
5
|
+
describe '.resolve' do
|
6
|
+
subject(:deferred_value) { DeferredContextValue.new[:key] }
|
7
|
+
|
8
|
+
context 'at the top level' do
|
9
|
+
let(:context) { {key: 'value', deferred_value: deferred_value} }
|
10
|
+
|
11
|
+
it 'returns the lookup value from the context' do
|
12
|
+
expect(DeferredContextValue.resolve(context[:deferred_value], context)).to eq(context[:key])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'in an array' do
|
17
|
+
let(:context) { {key: 'value', array: [1, 2, deferred_value]} }
|
18
|
+
|
19
|
+
it 'returns the lookup value from the context' do
|
20
|
+
expect(DeferredContextValue.resolve(context[:array], context)).to eq([1, 2, context[:key]])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'in a hash' do
|
25
|
+
let(:context) { {key: 'value', hash: {hash_key: deferred_value}} }
|
26
|
+
|
27
|
+
it 'returns the lookup value from the context' do
|
28
|
+
expect(DeferredContextValue.resolve(context[:hash], context)).to eq(hash_key: context[:key])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'in a hash nested in an array' do
|
33
|
+
let(:context) { {key: 'value', nested: [{}, {hash_key: deferred_value}]} }
|
34
|
+
|
35
|
+
it 'looks up a deferred context value in a hash nested in an array' do
|
36
|
+
expect(DeferredContextValue.resolve(context[:nested], context)).to eq([{}, {hash_key: context[:key]}])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'in an array nested in a hash' do
|
41
|
+
let(:context) { {key: 'value', nested: {hash_key: [1, deferred_value]}} }
|
42
|
+
|
43
|
+
it 'looks up a deferred context value in an array nested in a hash' do
|
44
|
+
expect(DeferredContextValue.resolve(context[:nested], context)).to eq(hash_key: [1, context[:key]])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
4
48
|
|
5
49
|
describe '#[]' do
|
6
|
-
it '
|
7
|
-
|
8
|
-
expect(
|
50
|
+
it 'stores the given key in the keys attribute' do
|
51
|
+
deferred_value[:top_level_key]['sub_key']
|
52
|
+
expect(deferred_value.keys).to eq([:top_level_key, 'sub_key'])
|
9
53
|
end
|
10
54
|
end
|
11
55
|
|
12
56
|
describe '#get' do
|
13
|
-
it '
|
14
|
-
allow(
|
57
|
+
it 'looks up the value of the keys attribute in the passed-in context' do
|
58
|
+
allow(deferred_value).to receive(:keys).and_return([:top_level_key, 'sub_key'])
|
15
59
|
value = 'example_value'
|
16
60
|
context = Context.new(top_level_key: {'sub_key' => value})
|
17
|
-
expect(
|
61
|
+
expect(deferred_value.get(context)).to eq(value)
|
18
62
|
end
|
19
63
|
end
|
20
64
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module EXEL
|
2
|
+
describe Events do
|
3
|
+
class EventTest
|
4
|
+
include Events
|
5
|
+
end
|
6
|
+
|
7
|
+
subject(:events) { EventTest.new }
|
8
|
+
let(:event_listener) { double(:event_listener) }
|
9
|
+
let(:context) { EXEL::Context.new(Events::LISTENERS_KEY => {event: [event_listener]}) }
|
10
|
+
|
11
|
+
describe '#register_listener' do
|
12
|
+
context 'when no listeners have been defined' do
|
13
|
+
let(:context) { EXEL::Context.new }
|
14
|
+
|
15
|
+
it 'adds a new listener to the context' do
|
16
|
+
events.register_listener(context, :event, event_listener)
|
17
|
+
expect(context[Events::LISTENERS_KEY].fetch(:event)).to contain_exactly(event_listener)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'registers multiple listeners for the same event' do
|
22
|
+
new_listener = double(:event_listener2)
|
23
|
+
events.register_listener(context, :event, new_listener)
|
24
|
+
expect(context[Events::LISTENERS_KEY].fetch(:event)).to contain_exactly(event_listener, new_listener)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#trigger' do
|
29
|
+
let(:context) { EXEL::Context.new }
|
30
|
+
let(:data) { {foo: 1} }
|
31
|
+
|
32
|
+
before { allow(events).to receive(:context).and_return(context) }
|
33
|
+
|
34
|
+
context 'when no events have been registered' do
|
35
|
+
it 'does not trigger anything' do
|
36
|
+
expect(event_listener).not_to receive(:event)
|
37
|
+
|
38
|
+
events.trigger(:event, data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'with a single listener registered for the event' do
|
43
|
+
before do
|
44
|
+
events.register_listener(context, :event, event_listener)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'calls the listener with the context and event data' do
|
48
|
+
expect(event_listener).to receive(:event).with(context, data)
|
49
|
+
|
50
|
+
events.trigger(:event, data)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'passes an empty hash if no data was given' do
|
54
|
+
expect(event_listener).to receive(:event).with(context, {})
|
55
|
+
|
56
|
+
events.trigger(:event)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with no listeners registered for the event' do
|
61
|
+
before do
|
62
|
+
events.register_listener(context, :other_event, event_listener)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'does not trigger anything' do
|
66
|
+
expect(event_listener).not_to receive(:event)
|
67
|
+
|
68
|
+
events.trigger(:event, data)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'with multiple listeners registered for the event' do
|
73
|
+
let(:event_listener2) { double(:event_listener2) }
|
74
|
+
|
75
|
+
before do
|
76
|
+
events.register_listener(context, :event, event_listener)
|
77
|
+
events.register_listener(context, :event, event_listener2)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'calls each listener with the context and event data' do
|
81
|
+
expect(event_listener).to receive(:event).with(context, data)
|
82
|
+
expect(event_listener2).to receive(:event).with(context, data)
|
83
|
+
|
84
|
+
events.trigger(:event, data)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -8,13 +8,13 @@ module EXEL
|
|
8
8
|
it { is_expected.to be_kind_of(ASTNode) }
|
9
9
|
|
10
10
|
describe '#run' do
|
11
|
-
it '
|
11
|
+
it 'only executes the instruction' do
|
12
12
|
expect(instruction).to receive(:execute).with(context).once
|
13
13
|
node.run(context)
|
14
14
|
end
|
15
15
|
|
16
|
-
it '
|
17
|
-
expect(child).
|
16
|
+
it 'does not run it`s children' do
|
17
|
+
expect(child).not_to receive(:run)
|
18
18
|
node.run(context)
|
19
19
|
end
|
20
20
|
end
|
@@ -1,20 +1,20 @@
|
|
1
1
|
module EXEL
|
2
2
|
describe Instruction do
|
3
|
-
subject(:instruction) { EXEL::Instruction.new(
|
3
|
+
subject(:instruction) { EXEL::Instruction.new(processor_class, args) }
|
4
4
|
let(:processor_class) { double(:processor_class, new: processor_instance) }
|
5
5
|
let(:processor_instance) { double(:processor_instance, process: nil) }
|
6
6
|
let(:args) { {arg1: 'arg_value1', arg2: {}} }
|
7
7
|
let(:context) { {context_key: 'context_value'} }
|
8
8
|
|
9
|
-
describe '#
|
10
|
-
it '
|
9
|
+
describe '#execute' do
|
10
|
+
it 'calls process on an instance of the processor class' do
|
11
11
|
expect(processor_class).to receive(:new).and_return(processor_instance)
|
12
12
|
expect(processor_instance).to receive(:process)
|
13
13
|
|
14
14
|
instruction.execute(context)
|
15
15
|
end
|
16
16
|
|
17
|
-
it '
|
17
|
+
it 'does not pass a copy of the context' do
|
18
18
|
allow(processor_class).to receive(:new) do |context_arg|
|
19
19
|
expect(context_arg).to be(context)
|
20
20
|
processor_instance
|
@@ -23,13 +23,13 @@ module EXEL
|
|
23
23
|
instruction.execute(context)
|
24
24
|
end
|
25
25
|
|
26
|
-
it '
|
26
|
+
it 'adds args to the context' do
|
27
27
|
instruction.execute(context)
|
28
28
|
expect(context.keys).to include(*args.keys)
|
29
29
|
end
|
30
30
|
|
31
31
|
context 'with args' do
|
32
|
-
it '
|
32
|
+
it 'passes the args to the processor' do
|
33
33
|
expect(processor_class).to receive(:new).with(hash_including(args))
|
34
34
|
instruction.execute(context)
|
35
35
|
end
|
@@ -38,7 +38,7 @@ module EXEL
|
|
38
38
|
context 'without args' do
|
39
39
|
let(:args) { nil }
|
40
40
|
|
41
|
-
it '
|
41
|
+
it 'passes only the context to the processor' do
|
42
42
|
expect(processor_class).to receive(:new).with(context)
|
43
43
|
instruction.execute(context)
|
44
44
|
end
|
@@ -46,9 +46,9 @@ module EXEL
|
|
46
46
|
|
47
47
|
context 'with a subtree' do
|
48
48
|
let(:subtree) { double(:subtree) }
|
49
|
-
subject(:instruction) { EXEL::Instruction.new(
|
49
|
+
subject(:instruction) { EXEL::Instruction.new(processor_class, args, subtree) }
|
50
50
|
|
51
|
-
it '
|
51
|
+
it 'passes the subtree to the processor' do
|
52
52
|
expect(processor_instance).to receive(:process).with(subtree)
|
53
53
|
instruction.execute(context)
|
54
54
|
end
|