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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +3 -3
  3. data/.github/workflows/specs.yml +6 -9
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.rspec +2 -1
  6. data/.ruby-version +1 -1
  7. data/Appraisals +0 -6
  8. data/CHANGELOG.md +186 -142
  9. data/Gemfile +1 -0
  10. data/README.md +12 -13
  11. data/bin/cli/base.rb +1 -2
  12. data/bin/cli/sqs.rb +5 -4
  13. data/bin/shoryuken +2 -1
  14. data/gemfiles/rails_7_0.gemfile +10 -10
  15. data/gemfiles/rails_7_1.gemfile +10 -9
  16. data/gemfiles/rails_7_2.gemfile +10 -9
  17. data/gemfiles/rails_8_0.gemfile +10 -9
  18. data/lib/shoryuken/body_parser.rb +3 -1
  19. data/lib/shoryuken/client.rb +2 -0
  20. data/lib/shoryuken/default_exception_handler.rb +2 -0
  21. data/lib/shoryuken/default_worker_registry.rb +11 -11
  22. data/lib/shoryuken/environment_loader.rb +6 -6
  23. data/lib/shoryuken/extensions/active_job_adapter.rb +8 -6
  24. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +5 -5
  25. data/lib/shoryuken/extensions/active_job_extensions.rb +2 -0
  26. data/lib/shoryuken/fetcher.rb +4 -2
  27. data/lib/shoryuken/helpers/atomic_boolean.rb +44 -0
  28. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  29. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  30. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  31. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  32. data/lib/shoryuken/inline_message.rb +22 -0
  33. data/lib/shoryuken/launcher.rb +2 -0
  34. data/lib/shoryuken/logging.rb +19 -5
  35. data/lib/shoryuken/manager.rb +6 -4
  36. data/lib/shoryuken/message.rb +2 -0
  37. data/lib/shoryuken/middleware/chain.rb +2 -0
  38. data/lib/shoryuken/middleware/server/active_record.rb +2 -0
  39. data/lib/shoryuken/middleware/server/auto_delete.rb +2 -0
  40. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +10 -10
  41. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +5 -3
  42. data/lib/shoryuken/middleware/server/timing.rb +2 -0
  43. data/lib/shoryuken/options.rb +9 -5
  44. data/lib/shoryuken/polling/base_strategy.rb +126 -0
  45. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  46. data/lib/shoryuken/polling/strict_priority.rb +2 -0
  47. data/lib/shoryuken/polling/weighted_round_robin.rb +2 -0
  48. data/lib/shoryuken/processor.rb +5 -2
  49. data/lib/shoryuken/queue.rb +6 -4
  50. data/lib/shoryuken/runner.rb +9 -12
  51. data/lib/shoryuken/util.rb +6 -6
  52. data/lib/shoryuken/version.rb +3 -1
  53. data/lib/shoryuken/worker/default_executor.rb +2 -0
  54. data/lib/shoryuken/worker/inline_executor.rb +3 -1
  55. data/lib/shoryuken/worker.rb +2 -0
  56. data/lib/shoryuken/worker_registry.rb +2 -0
  57. data/lib/shoryuken.rb +8 -28
  58. data/shoryuken.gemspec +6 -6
  59. data/spec/integration/launcher_spec.rb +2 -3
  60. data/spec/shared_examples_for_active_job.rb +13 -8
  61. data/spec/shoryuken/body_parser_spec.rb +1 -2
  62. data/spec/shoryuken/client_spec.rb +1 -1
  63. data/spec/shoryuken/default_exception_handler_spec.rb +9 -10
  64. data/spec/shoryuken/default_worker_registry_spec.rb +1 -2
  65. data/spec/shoryuken/environment_loader_spec.rb +9 -8
  66. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +2 -1
  67. data/spec/shoryuken/extensions/active_job_base_spec.rb +2 -1
  68. data/spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb +2 -1
  69. data/spec/shoryuken/extensions/active_job_wrapper_spec.rb +2 -1
  70. data/spec/shoryuken/fetcher_spec.rb +23 -26
  71. data/spec/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  72. data/spec/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  73. data/spec/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  74. data/spec/shoryuken/helpers/hash_utils_spec.rb +145 -0
  75. data/spec/shoryuken/helpers/string_utils_spec.rb +124 -0
  76. data/spec/shoryuken/helpers_integration_spec.rb +96 -0
  77. data/spec/shoryuken/inline_message_spec.rb +196 -0
  78. data/spec/shoryuken/launcher_spec.rb +1 -2
  79. data/spec/shoryuken/manager_spec.rb +1 -2
  80. data/spec/shoryuken/middleware/chain_spec.rb +1 -1
  81. data/spec/shoryuken/middleware/server/auto_delete_spec.rb +1 -1
  82. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
  83. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  84. data/spec/shoryuken/middleware/server/timing_spec.rb +1 -1
  85. data/spec/shoryuken/options_spec.rb +4 -4
  86. data/spec/shoryuken/polling/base_strategy_spec.rb +280 -0
  87. data/spec/shoryuken/polling/queue_configuration_spec.rb +195 -0
  88. data/spec/shoryuken/polling/strict_priority_spec.rb +1 -1
  89. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +1 -1
  90. data/spec/shoryuken/processor_spec.rb +1 -1
  91. data/spec/shoryuken/queue_spec.rb +2 -3
  92. data/spec/shoryuken/runner_spec.rb +1 -3
  93. data/spec/shoryuken/util_spec.rb +1 -1
  94. data/spec/shoryuken/worker/default_executor_spec.rb +1 -1
  95. data/spec/shoryuken/worker/inline_executor_spec.rb +1 -1
  96. data/spec/shoryuken/worker_spec.rb +15 -11
  97. data/spec/shoryuken_spec.rb +1 -1
  98. data/spec/spec_helper.rb +16 -0
  99. metadata +60 -27
  100. data/.github/FUNDING.yml +0 -12
  101. data/gemfiles/rails_6_1.gemfile +0 -18
  102. data/lib/shoryuken/core_ext.rb +0 -69
  103. data/lib/shoryuken/polling/base.rb +0 -67
  104. data/shoryuken.jpg +0 -0
  105. 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