shoryuken 7.0.0.alpha1 → 7.0.0.alpha2
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/.github/workflows/push.yml +3 -3
- data/.github/workflows/specs.yml +6 -9
- data/.github/workflows/verify-action-pins.yml +1 -1
- data/.rspec +2 -1
- data/.ruby-version +1 -1
- data/Appraisals +0 -6
- data/CHANGELOG.md +186 -142
- data/Gemfile +1 -0
- data/README.md +12 -13
- data/bin/cli/base.rb +1 -2
- data/bin/cli/sqs.rb +5 -4
- data/bin/shoryuken +2 -1
- data/gemfiles/rails_7_0.gemfile +10 -10
- data/gemfiles/rails_7_1.gemfile +10 -9
- data/gemfiles/rails_7_2.gemfile +10 -9
- data/gemfiles/rails_8_0.gemfile +10 -9
- data/lib/shoryuken/body_parser.rb +3 -1
- data/lib/shoryuken/client.rb +2 -0
- data/lib/shoryuken/default_exception_handler.rb +2 -0
- data/lib/shoryuken/default_worker_registry.rb +11 -11
- data/lib/shoryuken/environment_loader.rb +6 -6
- data/lib/shoryuken/extensions/active_job_adapter.rb +8 -6
- data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +5 -5
- data/lib/shoryuken/extensions/active_job_extensions.rb +2 -0
- data/lib/shoryuken/fetcher.rb +4 -2
- data/lib/shoryuken/helpers/atomic_boolean.rb +44 -0
- data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
- data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
- data/lib/shoryuken/helpers/hash_utils.rb +56 -0
- data/lib/shoryuken/helpers/string_utils.rb +65 -0
- data/lib/shoryuken/inline_message.rb +22 -0
- data/lib/shoryuken/launcher.rb +2 -0
- data/lib/shoryuken/logging.rb +19 -5
- data/lib/shoryuken/manager.rb +6 -4
- data/lib/shoryuken/message.rb +2 -0
- data/lib/shoryuken/middleware/chain.rb +2 -0
- data/lib/shoryuken/middleware/server/active_record.rb +2 -0
- data/lib/shoryuken/middleware/server/auto_delete.rb +2 -0
- data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +10 -10
- data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +5 -3
- data/lib/shoryuken/middleware/server/timing.rb +2 -0
- data/lib/shoryuken/options.rb +9 -5
- data/lib/shoryuken/polling/base_strategy.rb +126 -0
- data/lib/shoryuken/polling/queue_configuration.rb +103 -0
- data/lib/shoryuken/polling/strict_priority.rb +2 -0
- data/lib/shoryuken/polling/weighted_round_robin.rb +2 -0
- data/lib/shoryuken/processor.rb +5 -2
- data/lib/shoryuken/queue.rb +6 -4
- data/lib/shoryuken/runner.rb +9 -12
- data/lib/shoryuken/util.rb +6 -6
- data/lib/shoryuken/version.rb +3 -1
- data/lib/shoryuken/worker/default_executor.rb +2 -0
- data/lib/shoryuken/worker/inline_executor.rb +3 -1
- data/lib/shoryuken/worker.rb +2 -0
- data/lib/shoryuken/worker_registry.rb +2 -0
- data/lib/shoryuken.rb +8 -28
- data/shoryuken.gemspec +6 -6
- data/spec/integration/launcher_spec.rb +2 -3
- data/spec/shared_examples_for_active_job.rb +13 -8
- data/spec/shoryuken/body_parser_spec.rb +1 -2
- data/spec/shoryuken/client_spec.rb +1 -1
- data/spec/shoryuken/default_exception_handler_spec.rb +9 -10
- data/spec/shoryuken/default_worker_registry_spec.rb +1 -2
- data/spec/shoryuken/environment_loader_spec.rb +9 -8
- data/spec/shoryuken/extensions/active_job_adapter_spec.rb +2 -1
- data/spec/shoryuken/extensions/active_job_base_spec.rb +2 -1
- data/spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb +2 -1
- data/spec/shoryuken/extensions/active_job_wrapper_spec.rb +2 -1
- data/spec/shoryuken/fetcher_spec.rb +23 -26
- data/spec/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
- data/spec/shoryuken/helpers/atomic_counter_spec.rb +177 -0
- data/spec/shoryuken/helpers/atomic_hash_spec.rb +307 -0
- data/spec/shoryuken/helpers/hash_utils_spec.rb +145 -0
- data/spec/shoryuken/helpers/string_utils_spec.rb +124 -0
- data/spec/shoryuken/helpers_integration_spec.rb +96 -0
- data/spec/shoryuken/inline_message_spec.rb +196 -0
- data/spec/shoryuken/launcher_spec.rb +1 -2
- data/spec/shoryuken/manager_spec.rb +1 -2
- data/spec/shoryuken/middleware/chain_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/auto_delete_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
- data/spec/shoryuken/middleware/server/timing_spec.rb +1 -1
- data/spec/shoryuken/options_spec.rb +4 -4
- data/spec/shoryuken/polling/base_strategy_spec.rb +280 -0
- data/spec/shoryuken/polling/queue_configuration_spec.rb +195 -0
- data/spec/shoryuken/polling/strict_priority_spec.rb +1 -1
- data/spec/shoryuken/polling/weighted_round_robin_spec.rb +1 -1
- data/spec/shoryuken/processor_spec.rb +1 -1
- data/spec/shoryuken/queue_spec.rb +2 -3
- data/spec/shoryuken/runner_spec.rb +1 -3
- data/spec/shoryuken/util_spec.rb +1 -1
- data/spec/shoryuken/worker/default_executor_spec.rb +1 -1
- data/spec/shoryuken/worker/inline_executor_spec.rb +1 -1
- data/spec/shoryuken/worker_spec.rb +15 -11
- data/spec/shoryuken_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -0
- metadata +60 -27
- data/.github/FUNDING.yml +0 -12
- data/gemfiles/rails_6_1.gemfile +0 -18
- data/lib/shoryuken/core_ext.rb +0 -69
- data/lib/shoryuken/polling/base.rb +0 -67
- data/shoryuken.jpg +0 -0
- data/spec/shoryuken/core_ext_spec.rb +0 -40
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Shoryuken::Helpers::AtomicHash do
|
|
4
|
+
subject { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'creates an empty hash' do
|
|
8
|
+
hash = described_class.new
|
|
9
|
+
expect(hash.keys).to eq([])
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#[]' do
|
|
14
|
+
it 'returns nil for missing keys' do
|
|
15
|
+
expect(subject['missing']).to be_nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'returns stored values' do
|
|
19
|
+
subject['key'] = 'value'
|
|
20
|
+
expect(subject['key']).to eq('value')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'works with different key types' do
|
|
24
|
+
subject[:symbol] = 'symbol_value'
|
|
25
|
+
subject[42] = 'number_value'
|
|
26
|
+
|
|
27
|
+
expect(subject[:symbol]).to eq('symbol_value')
|
|
28
|
+
expect(subject[42]).to eq('number_value')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#[]=' do
|
|
33
|
+
it 'stores values' do
|
|
34
|
+
subject['key'] = 'value'
|
|
35
|
+
expect(subject['key']).to eq('value')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'overwrites existing values' do
|
|
39
|
+
subject['key'] = 'old_value'
|
|
40
|
+
subject['key'] = 'new_value'
|
|
41
|
+
expect(subject['key']).to eq('new_value')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns the assigned value' do
|
|
45
|
+
result = (subject['key'] = 'value')
|
|
46
|
+
expect(result).to eq('value')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#clear' do
|
|
51
|
+
it 'removes all entries' do
|
|
52
|
+
subject['key1'] = 'value1'
|
|
53
|
+
subject['key2'] = 'value2'
|
|
54
|
+
|
|
55
|
+
expect(subject.keys.size).to eq(2)
|
|
56
|
+
|
|
57
|
+
subject.clear
|
|
58
|
+
|
|
59
|
+
expect(subject.keys).to eq([])
|
|
60
|
+
expect(subject['key1']).to be_nil
|
|
61
|
+
expect(subject['key2']).to be_nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'returns the hash itself' do
|
|
65
|
+
subject['key'] = 'value'
|
|
66
|
+
result = subject.clear
|
|
67
|
+
expect(result).to eq({})
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#keys' do
|
|
72
|
+
it 'returns empty array for empty hash' do
|
|
73
|
+
expect(subject.keys).to eq([])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'returns all keys' do
|
|
77
|
+
subject['key1'] = 'value1'
|
|
78
|
+
subject[:key2] = 'value2'
|
|
79
|
+
subject[42] = 'value3'
|
|
80
|
+
|
|
81
|
+
keys = subject.keys
|
|
82
|
+
expect(keys).to contain_exactly('key1', :key2, 42)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'reflects changes after modifications' do
|
|
86
|
+
subject['key'] = 'value'
|
|
87
|
+
expect(subject.keys).to eq(['key'])
|
|
88
|
+
|
|
89
|
+
subject.clear
|
|
90
|
+
expect(subject.keys).to eq([])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe '#fetch' do
|
|
95
|
+
it 'returns value for existing key' do
|
|
96
|
+
subject['key'] = 'value'
|
|
97
|
+
expect(subject.fetch('key')).to eq('value')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns default for missing key' do
|
|
101
|
+
expect(subject.fetch('missing', 'default')).to eq('default')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns nil as default when no default provided' do
|
|
105
|
+
expect(subject.fetch('missing')).to be_nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'works with different default types' do
|
|
109
|
+
expect(subject.fetch('missing', [])).to eq([])
|
|
110
|
+
expect(subject.fetch('missing', {})).to eq({})
|
|
111
|
+
expect(subject.fetch('missing', 42)).to eq(42)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe 'thread safety' do
|
|
116
|
+
it 'handles concurrent writes correctly' do
|
|
117
|
+
hash = described_class.new
|
|
118
|
+
threads = []
|
|
119
|
+
|
|
120
|
+
# 10 threads, each writing 100 different keys
|
|
121
|
+
10.times do |thread_id|
|
|
122
|
+
threads << Thread.new do
|
|
123
|
+
100.times do |i|
|
|
124
|
+
key = "thread_#{thread_id}_key_#{i}"
|
|
125
|
+
hash[key] = "value_#{i}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
threads.each(&:join)
|
|
131
|
+
|
|
132
|
+
# Verify all 1000 keys were written
|
|
133
|
+
expect(hash.keys.size).to eq(1000)
|
|
134
|
+
|
|
135
|
+
# Verify a sample of values
|
|
136
|
+
expect(hash['thread_0_key_0']).to eq('value_0')
|
|
137
|
+
expect(hash['thread_9_key_99']).to eq('value_99')
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'handles concurrent reads correctly' do
|
|
141
|
+
hash = described_class.new
|
|
142
|
+
|
|
143
|
+
# Pre-populate hash
|
|
144
|
+
100.times { |i| hash["key_#{i}"] = "value_#{i}" }
|
|
145
|
+
|
|
146
|
+
read_results = []
|
|
147
|
+
threads = []
|
|
148
|
+
|
|
149
|
+
# 10 threads, each reading 100 times
|
|
150
|
+
10.times do
|
|
151
|
+
threads << Thread.new do
|
|
152
|
+
100.times do |i|
|
|
153
|
+
key = "key_#{i % 100}"
|
|
154
|
+
read_results << hash[key]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
threads.each(&:join)
|
|
160
|
+
|
|
161
|
+
# All reads should succeed
|
|
162
|
+
expect(read_results.size).to eq(1000)
|
|
163
|
+
expect(read_results.compact.size).to eq(1000)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'handles mixed concurrent read/write operations' do
|
|
167
|
+
hash = described_class.new
|
|
168
|
+
threads = []
|
|
169
|
+
|
|
170
|
+
# Writer threads
|
|
171
|
+
5.times do |thread_id|
|
|
172
|
+
threads << Thread.new do
|
|
173
|
+
50.times do |i|
|
|
174
|
+
hash["writer_#{thread_id}_#{i}"] = "value_#{i}"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Reader threads
|
|
180
|
+
5.times do
|
|
181
|
+
threads << Thread.new do
|
|
182
|
+
100.times do |i|
|
|
183
|
+
# Read existing keys
|
|
184
|
+
hash["writer_0_#{i % 50}"]
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Clear operations
|
|
190
|
+
2.times do
|
|
191
|
+
threads << Thread.new do
|
|
192
|
+
sleep(0.001) # Let some writes happen first
|
|
193
|
+
hash.clear
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
threads.each(&:join)
|
|
198
|
+
|
|
199
|
+
# The hash should be valid (not corrupted)
|
|
200
|
+
# After clear operations, it might be empty or have some keys
|
|
201
|
+
expect(hash.keys).to be_an(Array)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it 'provides thread-safe key enumeration' do
|
|
205
|
+
hash = described_class.new
|
|
206
|
+
threads = []
|
|
207
|
+
keys_snapshots = []
|
|
208
|
+
|
|
209
|
+
# Writer thread
|
|
210
|
+
writer = Thread.new do
|
|
211
|
+
100.times { |i| hash["key_#{i}"] = i }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Reader threads taking snapshots of keys
|
|
215
|
+
5.times do
|
|
216
|
+
threads << Thread.new do
|
|
217
|
+
20.times do
|
|
218
|
+
keys_snapshots << hash.keys.dup
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
[writer, *threads].each(&:join)
|
|
224
|
+
|
|
225
|
+
# All snapshots should be valid arrays
|
|
226
|
+
keys_snapshots.each do |snapshot|
|
|
227
|
+
expect(snapshot).to be_an(Array)
|
|
228
|
+
# Keys should be valid
|
|
229
|
+
snapshot.each { |key| expect(key).to be_a(String) }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
describe 'mutex synchronization' do
|
|
235
|
+
it 'ensures all operations are atomic' do
|
|
236
|
+
hash = described_class.new
|
|
237
|
+
|
|
238
|
+
# Verify basic operations work correctly
|
|
239
|
+
hash['key'] = 'value'
|
|
240
|
+
expect(hash['key']).to eq('value')
|
|
241
|
+
expect(hash.keys).to eq(['key'])
|
|
242
|
+
|
|
243
|
+
hash.clear
|
|
244
|
+
expect(hash.keys).to eq([])
|
|
245
|
+
expect(hash['key']).to be_nil
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
describe 'drop-in replacement for Concurrent::Hash' do
|
|
250
|
+
it 'provides the same basic API' do
|
|
251
|
+
# Test that our implementation has the same methods as Concurrent::Hash
|
|
252
|
+
expect(subject).to respond_to(:[])
|
|
253
|
+
expect(subject).to respond_to(:[]=)
|
|
254
|
+
expect(subject).to respond_to(:clear)
|
|
255
|
+
expect(subject).to respond_to(:keys)
|
|
256
|
+
expect(subject).to respond_to(:fetch)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'behaves identically to Concurrent::Hash for basic operations' do
|
|
260
|
+
# This test documents the expected behavior that matches Concurrent::Hash
|
|
261
|
+
hash = described_class.new
|
|
262
|
+
|
|
263
|
+
# Test assignment and retrieval
|
|
264
|
+
hash['queue1'] = 'Worker1'
|
|
265
|
+
expect(hash['queue1']).to eq('Worker1')
|
|
266
|
+
|
|
267
|
+
# Test keys
|
|
268
|
+
expect(hash.keys).to eq(['queue1'])
|
|
269
|
+
|
|
270
|
+
# Test fetch with default
|
|
271
|
+
expect(hash.fetch('queue1')).to eq('Worker1')
|
|
272
|
+
expect(hash.fetch('missing', 'default')).to eq('default')
|
|
273
|
+
|
|
274
|
+
# Test clear
|
|
275
|
+
hash.clear
|
|
276
|
+
expect(hash.keys).to eq([])
|
|
277
|
+
expect(hash['queue1']).to be_nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'matches DefaultWorkerRegistry usage patterns' do
|
|
281
|
+
# Test the exact patterns used in DefaultWorkerRegistry
|
|
282
|
+
hash = described_class.new
|
|
283
|
+
|
|
284
|
+
# Pattern from register_worker method
|
|
285
|
+
queue = 'test_queue'
|
|
286
|
+
clazz = 'TestWorker'
|
|
287
|
+
hash[queue] = clazz
|
|
288
|
+
|
|
289
|
+
# Pattern from batch_receive_messages? method
|
|
290
|
+
expect(hash[queue]).to eq(clazz)
|
|
291
|
+
|
|
292
|
+
# Pattern from fetch_worker method
|
|
293
|
+
expect(hash[queue]).to eq(clazz)
|
|
294
|
+
|
|
295
|
+
# Pattern from queues method
|
|
296
|
+
expect(hash.keys).to eq([queue])
|
|
297
|
+
|
|
298
|
+
# Pattern from workers method (with fetch and default)
|
|
299
|
+
expect(hash.fetch(queue, [])).to eq(clazz)
|
|
300
|
+
expect(hash.fetch('missing', [])).to eq([])
|
|
301
|
+
|
|
302
|
+
# Pattern from clear method
|
|
303
|
+
hash.clear
|
|
304
|
+
expect(hash.keys).to eq([])
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Shoryuken::Helpers::HashUtils do
|
|
6
|
+
describe '.deep_symbolize_keys' do
|
|
7
|
+
it 'converts string keys to symbols' do
|
|
8
|
+
input = { 'key1' => 'value1', 'key2' => 'value2' }
|
|
9
|
+
expected = { key1: 'value1', key2: 'value2' }
|
|
10
|
+
|
|
11
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'leaves symbol keys unchanged' do
|
|
15
|
+
input = { key1: 'value1', key2: 'value2' }
|
|
16
|
+
expected = { key1: 'value1', key2: 'value2' }
|
|
17
|
+
|
|
18
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'handles mixed key types' do
|
|
22
|
+
input = { 'string_key' => 'value1', :symbol_key => 'value2' }
|
|
23
|
+
expected = { string_key: 'value1', symbol_key: 'value2' }
|
|
24
|
+
|
|
25
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'converts keys recursively in nested hashes' do
|
|
29
|
+
input = {
|
|
30
|
+
'level1' => {
|
|
31
|
+
'level2' => {
|
|
32
|
+
'level3' => 'deep_value'
|
|
33
|
+
},
|
|
34
|
+
'other_level2' => 'value'
|
|
35
|
+
},
|
|
36
|
+
'top_level' => 'value'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expected = {
|
|
40
|
+
level1: {
|
|
41
|
+
level2: {
|
|
42
|
+
level3: 'deep_value'
|
|
43
|
+
},
|
|
44
|
+
other_level2: 'value'
|
|
45
|
+
},
|
|
46
|
+
top_level: 'value'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'preserves non-hash values in nested structures' do
|
|
53
|
+
input = {
|
|
54
|
+
'config' => {
|
|
55
|
+
'timeout' => 30,
|
|
56
|
+
'enabled' => true,
|
|
57
|
+
'tags' => ['tag1', 'tag2'],
|
|
58
|
+
'metadata' => nil
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
expected = {
|
|
63
|
+
config: {
|
|
64
|
+
timeout: 30,
|
|
65
|
+
enabled: true,
|
|
66
|
+
tags: ['tag1', 'tag2'],
|
|
67
|
+
metadata: nil
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'handles empty hash' do
|
|
75
|
+
expect(described_class.deep_symbolize_keys({})).to eq({})
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'handles hash with empty nested hash' do
|
|
79
|
+
input = { 'key' => {} }
|
|
80
|
+
expected = { key: {} }
|
|
81
|
+
|
|
82
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns non-hash input unchanged' do
|
|
86
|
+
expect(described_class.deep_symbolize_keys('string')).to eq('string')
|
|
87
|
+
expect(described_class.deep_symbolize_keys(123)).to eq(123)
|
|
88
|
+
expect(described_class.deep_symbolize_keys([])).to eq([])
|
|
89
|
+
expect(described_class.deep_symbolize_keys(nil)).to be_nil
|
|
90
|
+
expect(described_class.deep_symbolize_keys(true)).to be true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'handles keys that cannot be converted to symbols' do
|
|
94
|
+
# Create a key that will raise an exception when converted to symbol
|
|
95
|
+
problematic_key = Object.new
|
|
96
|
+
allow(problematic_key).to receive(:to_sym).and_raise(StandardError)
|
|
97
|
+
|
|
98
|
+
input = { problematic_key => 'value', 'normal_key' => 'normal_value' }
|
|
99
|
+
result = described_class.deep_symbolize_keys(input)
|
|
100
|
+
|
|
101
|
+
# The problematic key should remain as-is, normal key should be symbolized
|
|
102
|
+
expect(result[problematic_key]).to eq('value')
|
|
103
|
+
expect(result[:normal_key]).to eq('normal_value')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'does not modify the original hash' do
|
|
107
|
+
input = { 'key' => { 'nested' => 'value' } }
|
|
108
|
+
original_input = input.dup
|
|
109
|
+
|
|
110
|
+
described_class.deep_symbolize_keys(input)
|
|
111
|
+
|
|
112
|
+
expect(input).to eq(original_input)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
context 'with configuration-like data' do
|
|
116
|
+
it 'processes typical YAML configuration' do
|
|
117
|
+
input = {
|
|
118
|
+
'database' => {
|
|
119
|
+
'host' => 'localhost',
|
|
120
|
+
'port' => 5432,
|
|
121
|
+
'ssl' => true
|
|
122
|
+
},
|
|
123
|
+
'queues' => {
|
|
124
|
+
'default' => { 'concurrency' => 5 },
|
|
125
|
+
'mailers' => { 'concurrency' => 2 }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
expected = {
|
|
130
|
+
database: {
|
|
131
|
+
host: 'localhost',
|
|
132
|
+
port: 5432,
|
|
133
|
+
ssl: true
|
|
134
|
+
},
|
|
135
|
+
queues: {
|
|
136
|
+
default: { concurrency: 5 },
|
|
137
|
+
mailers: { concurrency: 2 }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expect(described_class.deep_symbolize_keys(input)).to eq(expected)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Shoryuken::Helpers::StringUtils do
|
|
6
|
+
describe '.constantize' do
|
|
7
|
+
it 'returns a simple constant' do
|
|
8
|
+
expect(described_class.constantize('Object')).to eq(Object)
|
|
9
|
+
expect(described_class.constantize('String')).to eq(String)
|
|
10
|
+
expect(described_class.constantize('Hash')).to eq(Hash)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'returns nested constants' do
|
|
14
|
+
expect(described_class.constantize('Shoryuken::Helpers::StringUtils')).to eq(Shoryuken::Helpers::StringUtils)
|
|
15
|
+
expect(described_class.constantize('Shoryuken::Helpers::AtomicCounter')).to eq(Shoryuken::Helpers::AtomicCounter)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'handles leading double colon' do
|
|
19
|
+
expect(described_class.constantize('::Object')).to eq(Object)
|
|
20
|
+
expect(described_class.constantize('::String')).to eq(String)
|
|
21
|
+
expect(described_class.constantize('::Shoryuken::Helpers::StringUtils')).to eq(Shoryuken::Helpers::StringUtils)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'handles empty string' do
|
|
25
|
+
expect(described_class.constantize('')).to eq(Object)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'handles string with only double colons' do
|
|
29
|
+
expect(described_class.constantize('::')).to eq(Object)
|
|
30
|
+
expect(described_class.constantize('::::')).to eq(Object)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'raises NameError for non-existent constants' do
|
|
34
|
+
expect { described_class.constantize('NonExistentClass') }.to raise_error(NameError)
|
|
35
|
+
expect { described_class.constantize('Shoryuken::NonExistentClass') }.to raise_error(NameError)
|
|
36
|
+
expect { described_class.constantize('NonExistent::AlsoNonExistent') }.to raise_error(NameError)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'raises NameError for partially invalid nested constants' do
|
|
40
|
+
expect { described_class.constantize('Shoryuken::NonExistent::AlsoNonExistent') }.to raise_error(NameError)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context 'with dynamically defined constants' do
|
|
44
|
+
before do
|
|
45
|
+
# Define a temporary constant for testing
|
|
46
|
+
Object.const_set('TempTestClass', Class.new)
|
|
47
|
+
TempTestClass.const_set('NestedClass', Class.new)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
after do
|
|
51
|
+
# Clean up the temporary constant
|
|
52
|
+
Object.send(:remove_const, 'TempTestClass') if Object.const_defined?('TempTestClass')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'finds dynamically defined constants' do
|
|
56
|
+
expect(described_class.constantize('TempTestClass')).to eq(TempTestClass)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'finds nested dynamically defined constants' do
|
|
60
|
+
expect(described_class.constantize('TempTestClass::NestedClass')).to eq(TempTestClass::NestedClass)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context 'with module constants' do
|
|
65
|
+
it 'returns module constants' do
|
|
66
|
+
expect(described_class.constantize('Shoryuken')).to eq(Shoryuken)
|
|
67
|
+
expect(described_class.constantize('Shoryuken::Helpers')).to eq(Shoryuken::Helpers)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'with worker class scenarios' do
|
|
72
|
+
before do
|
|
73
|
+
# Simulate typical worker class scenarios
|
|
74
|
+
unless Object.const_defined?('MyApp')
|
|
75
|
+
Object.const_set('MyApp', Module.new)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless MyApp.const_defined?('EmailWorker')
|
|
79
|
+
MyApp.const_set('EmailWorker', Class.new)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
after do
|
|
84
|
+
# Clean up
|
|
85
|
+
MyApp.send(:remove_const, 'EmailWorker') if MyApp.const_defined?('EmailWorker')
|
|
86
|
+
Object.send(:remove_const, 'MyApp') if Object.const_defined?('MyApp')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'loads worker classes from string names' do
|
|
90
|
+
worker_class = described_class.constantize('MyApp::EmailWorker')
|
|
91
|
+
expect(worker_class).to eq(MyApp::EmailWorker)
|
|
92
|
+
expect(worker_class.new).to be_an_instance_of(MyApp::EmailWorker)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context 'edge cases' do
|
|
97
|
+
it 'handles constants with numbers' do
|
|
98
|
+
# Using existing constants that might have numbers
|
|
99
|
+
expect(described_class.constantize('Encoding::UTF_8')).to eq(Encoding::UTF_8)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'is case sensitive' do
|
|
103
|
+
expect { described_class.constantize('object') }.to raise_error(NameError)
|
|
104
|
+
expect { described_class.constantize('STRING') }.to raise_error(NameError)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'handles single character constant names' do
|
|
108
|
+
# Define a single character constant for testing
|
|
109
|
+
Object.const_set('A', Class.new) unless Object.const_defined?('A')
|
|
110
|
+
|
|
111
|
+
expect(described_class.constantize('A')).to eq(A)
|
|
112
|
+
|
|
113
|
+
# Clean up
|
|
114
|
+
Object.send(:remove_const, 'A') if Object.const_defined?('A')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
context 'error messages' do
|
|
119
|
+
it 'provides meaningful error messages' do
|
|
120
|
+
expect { described_class.constantize('NonExistent') }.to raise_error(NameError, /NonExistent/)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe 'Helpers Integration' do
|
|
4
|
+
# Integration tests for helper utility methods that replaced core extensions
|
|
5
|
+
|
|
6
|
+
describe Shoryuken::Helpers::HashUtils do
|
|
7
|
+
describe '.deep_symbolize_keys' do
|
|
8
|
+
it 'converts keys into symbols recursively' do
|
|
9
|
+
input = { :key1 => 'value1',
|
|
10
|
+
'key2' => 'value2',
|
|
11
|
+
'key3' => {
|
|
12
|
+
'key31' => { 'key311' => 'value311' },
|
|
13
|
+
'key32' => 'value32'
|
|
14
|
+
} }
|
|
15
|
+
|
|
16
|
+
expected = { key1: 'value1',
|
|
17
|
+
key2: 'value2',
|
|
18
|
+
key3: { key31: { key311: 'value311' },
|
|
19
|
+
key32: 'value32' } }
|
|
20
|
+
|
|
21
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(input)).to eq(expected)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'handles non-hash input gracefully' do
|
|
25
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys('string')).to eq('string')
|
|
26
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(123)).to eq(123)
|
|
27
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(nil)).to be_nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'handles empty hash' do
|
|
31
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys({})).to eq({})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'handles mixed value types' do
|
|
35
|
+
input = { 'key1' => 'string', 'key2' => 123, 'key3' => { 'nested' => true } }
|
|
36
|
+
expected = { key1: 'string', key2: 123, key3: { nested: true } }
|
|
37
|
+
|
|
38
|
+
expect(Shoryuken::Helpers::HashUtils.deep_symbolize_keys(input)).to eq(expected)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe Shoryuken::Helpers::StringUtils do
|
|
44
|
+
describe '.constantize' do
|
|
45
|
+
class HelloWorld; end
|
|
46
|
+
|
|
47
|
+
it 'returns a class from a string' do
|
|
48
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('HelloWorld')).to eq(HelloWorld)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'handles nested constants' do
|
|
52
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('Shoryuken::Helpers::StringUtils')).to eq(Shoryuken::Helpers::StringUtils)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'raises NameError for non-existent constants' do
|
|
56
|
+
expect { Shoryuken::Helpers::StringUtils.constantize('NonExistentClass') }.to raise_error(NameError)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'handles empty string' do
|
|
60
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('')).to eq(Object)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'handles leading double colon' do
|
|
64
|
+
expect(Shoryuken::Helpers::StringUtils.constantize('::Object')).to eq(Object)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe 'Integration scenarios' do
|
|
70
|
+
it 'processes configuration data end-to-end' do
|
|
71
|
+
# Simulate loading YAML config and converting worker class names
|
|
72
|
+
config_data = {
|
|
73
|
+
'queues' => {
|
|
74
|
+
'default' => { 'worker_class' => 'Object' },
|
|
75
|
+
'mailers' => { 'worker_class' => 'String' }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
symbolized = Shoryuken::Helpers::HashUtils.deep_symbolize_keys(config_data)
|
|
80
|
+
|
|
81
|
+
expect(symbolized).to eq({
|
|
82
|
+
queues: {
|
|
83
|
+
default: { worker_class: 'Object' },
|
|
84
|
+
mailers: { worker_class: 'String' }
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Test constantizing the worker classes
|
|
89
|
+
default_worker = Shoryuken::Helpers::StringUtils.constantize(symbolized[:queues][:default][:worker_class])
|
|
90
|
+
mailer_worker = Shoryuken::Helpers::StringUtils.constantize(symbolized[:queues][:mailers][:worker_class])
|
|
91
|
+
|
|
92
|
+
expect(default_worker).to eq(Object)
|
|
93
|
+
expect(mailer_worker).to eq(String)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|