shoryuken 7.0.0.alpha1 → 7.0.0.rc1

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +3 -3
  3. data/.github/workflows/specs.yml +27 -17
  4. data/.github/workflows/verify-action-pins.yml +1 -1
  5. data/.rspec +2 -1
  6. data/.ruby-version +1 -1
  7. data/Appraisals +6 -18
  8. data/CHANGELOG.md +200 -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 +6 -5
  13. data/bin/shoryuken +3 -2
  14. data/gemfiles/rails_7_2.gemfile +1 -0
  15. data/gemfiles/rails_8_0.gemfile +1 -0
  16. data/gemfiles/{rails_7_1.gemfile → rails_8_1.gemfile} +2 -1
  17. data/lib/shoryuken/body_parser.rb +3 -1
  18. data/lib/shoryuken/client.rb +2 -0
  19. data/lib/shoryuken/default_exception_handler.rb +2 -0
  20. data/lib/shoryuken/default_worker_registry.rb +11 -11
  21. data/lib/shoryuken/environment_loader.rb +6 -6
  22. data/lib/shoryuken/extensions/active_job_adapter.rb +21 -6
  23. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +5 -5
  24. data/lib/shoryuken/extensions/active_job_extensions.rb +2 -0
  25. data/lib/shoryuken/fetcher.rb +4 -2
  26. data/lib/shoryuken/helpers/atomic_boolean.rb +44 -0
  27. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  28. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  29. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  30. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  31. data/lib/shoryuken/helpers/timer_task.rb +66 -0
  32. data/lib/shoryuken/inline_message.rb +22 -0
  33. data/lib/shoryuken/launcher.rb +16 -0
  34. data/lib/shoryuken/logging/base.rb +26 -0
  35. data/lib/shoryuken/logging/pretty.rb +25 -0
  36. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  37. data/lib/shoryuken/logging.rb +6 -12
  38. data/lib/shoryuken/manager.rb +6 -4
  39. data/lib/shoryuken/message.rb +116 -1
  40. data/lib/shoryuken/middleware/chain.rb +140 -43
  41. data/lib/shoryuken/middleware/entry.rb +30 -0
  42. data/lib/shoryuken/middleware/server/active_record.rb +2 -0
  43. data/lib/shoryuken/middleware/server/auto_delete.rb +2 -0
  44. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +11 -11
  45. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +5 -3
  46. data/lib/shoryuken/middleware/server/timing.rb +2 -0
  47. data/lib/shoryuken/options.rb +9 -5
  48. data/lib/shoryuken/polling/base_strategy.rb +126 -0
  49. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  50. data/lib/shoryuken/polling/strict_priority.rb +2 -0
  51. data/lib/shoryuken/polling/weighted_round_robin.rb +2 -0
  52. data/lib/shoryuken/processor.rb +5 -2
  53. data/lib/shoryuken/queue.rb +6 -4
  54. data/lib/shoryuken/runner.rb +12 -12
  55. data/lib/shoryuken/util.rb +6 -6
  56. data/lib/shoryuken/version.rb +3 -1
  57. data/lib/shoryuken/worker/default_executor.rb +2 -0
  58. data/lib/shoryuken/worker/inline_executor.rb +3 -1
  59. data/lib/shoryuken/worker.rb +173 -0
  60. data/lib/shoryuken/worker_registry.rb +2 -0
  61. data/lib/shoryuken.rb +8 -28
  62. data/shoryuken.gemspec +6 -6
  63. data/spec/integration/active_job_continuation_spec.rb +145 -0
  64. data/spec/integration/launcher_spec.rb +2 -3
  65. data/spec/shared_examples_for_active_job.rb +13 -8
  66. data/spec/shoryuken/body_parser_spec.rb +1 -2
  67. data/spec/shoryuken/client_spec.rb +1 -1
  68. data/spec/shoryuken/default_exception_handler_spec.rb +9 -10
  69. data/spec/shoryuken/default_worker_registry_spec.rb +1 -2
  70. data/spec/shoryuken/environment_loader_spec.rb +9 -8
  71. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +2 -1
  72. data/spec/shoryuken/extensions/active_job_base_spec.rb +2 -1
  73. data/spec/shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb +2 -1
  74. data/spec/shoryuken/extensions/active_job_continuation_spec.rb +110 -0
  75. data/spec/shoryuken/extensions/active_job_wrapper_spec.rb +2 -1
  76. data/spec/shoryuken/fetcher_spec.rb +23 -26
  77. data/spec/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  78. data/spec/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  79. data/spec/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  80. data/spec/shoryuken/helpers/hash_utils_spec.rb +145 -0
  81. data/spec/shoryuken/helpers/string_utils_spec.rb +124 -0
  82. data/spec/shoryuken/helpers/timer_task_spec.rb +298 -0
  83. data/spec/shoryuken/helpers_integration_spec.rb +96 -0
  84. data/spec/shoryuken/inline_message_spec.rb +196 -0
  85. data/spec/shoryuken/launcher_spec.rb +23 -2
  86. data/spec/shoryuken/manager_spec.rb +1 -2
  87. data/spec/shoryuken/middleware/chain_spec.rb +1 -1
  88. data/spec/shoryuken/middleware/server/auto_delete_spec.rb +1 -1
  89. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +1 -1
  90. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  91. data/spec/shoryuken/middleware/server/timing_spec.rb +1 -1
  92. data/spec/shoryuken/options_spec.rb +4 -4
  93. data/spec/shoryuken/polling/base_strategy_spec.rb +280 -0
  94. data/spec/shoryuken/polling/queue_configuration_spec.rb +195 -0
  95. data/spec/shoryuken/polling/strict_priority_spec.rb +1 -1
  96. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +1 -1
  97. data/spec/shoryuken/processor_spec.rb +1 -1
  98. data/spec/shoryuken/queue_spec.rb +2 -3
  99. data/spec/shoryuken/runner_spec.rb +1 -3
  100. data/spec/shoryuken/util_spec.rb +1 -1
  101. data/spec/shoryuken/worker/default_executor_spec.rb +1 -1
  102. data/spec/shoryuken/worker/inline_executor_spec.rb +1 -1
  103. data/spec/shoryuken/worker_spec.rb +15 -11
  104. data/spec/shoryuken_spec.rb +1 -1
  105. data/spec/spec_helper.rb +16 -0
  106. metadata +72 -29
  107. data/.github/FUNDING.yml +0 -12
  108. data/gemfiles/rails_6_1.gemfile +0 -18
  109. data/gemfiles/rails_7_0.gemfile +0 -19
  110. data/lib/shoryuken/core_ext.rb +0 -69
  111. data/lib/shoryuken/polling/base.rb +0 -67
  112. data/shoryuken.jpg +0 -0
  113. data/spec/shoryuken/core_ext_spec.rb +0 -40
@@ -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,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Shoryuken::Helpers::TimerTask do
6
+ let(:execution_interval) { 0.1 }
7
+ let!(:timer_task) do
8
+ described_class.new(execution_interval: execution_interval) do
9
+ @execution_count = (@execution_count || 0) + 1
10
+ end
11
+ end
12
+
13
+ describe '#initialize' do
14
+ it 'creates a timer task with the specified interval' do
15
+ timer = described_class.new(execution_interval: 5) {}
16
+ expect(timer).to be_a(described_class)
17
+ end
18
+
19
+ it 'requires a block' do
20
+ expect { described_class.new(execution_interval: 5) }.to raise_error(ArgumentError, 'A block must be provided')
21
+ end
22
+
23
+ it 'requires a positive execution_interval' do
24
+ expect { described_class.new(execution_interval: 0) {} }.to raise_error(ArgumentError, 'execution_interval must be positive')
25
+ expect { described_class.new(execution_interval: -1) {} }.to raise_error(ArgumentError, 'execution_interval must be positive')
26
+ end
27
+
28
+ it 'accepts string numbers as execution_interval' do
29
+ timer = described_class.new(execution_interval: '5.5') {}
30
+ expect(timer.instance_variable_get(:@execution_interval)).to eq(5.5)
31
+ end
32
+
33
+ it 'raises ArgumentError for non-numeric execution_interval' do
34
+ expect { described_class.new(execution_interval: 'invalid') {} }.to raise_error(ArgumentError)
35
+ expect { described_class.new(execution_interval: nil) {} }.to raise_error(TypeError)
36
+ expect { described_class.new(execution_interval: {}) {} }.to raise_error(TypeError)
37
+ end
38
+
39
+ it 'stores the task block in @task instance variable' do
40
+ task_proc = proc { puts 'test' }
41
+ timer = described_class.new(execution_interval: 1, &task_proc)
42
+ expect(timer.instance_variable_get(:@task)).to eq(task_proc)
43
+ end
44
+
45
+ it 'stores the execution interval' do
46
+ timer = described_class.new(execution_interval: 5) {}
47
+ expect(timer.instance_variable_get(:@execution_interval)).to eq(5)
48
+ end
49
+
50
+ it 'initializes state variables correctly' do
51
+ timer = described_class.new(execution_interval: 1) {}
52
+ expect(timer.instance_variable_get(:@running)).to be false
53
+ expect(timer.instance_variable_get(:@killed)).to be false
54
+ expect(timer.instance_variable_get(:@thread)).to be_nil
55
+ end
56
+ end
57
+
58
+ describe '#execute' do
59
+ it 'returns self for method chaining' do
60
+ result = timer_task.execute
61
+ expect(result).to eq(timer_task)
62
+ timer_task.kill
63
+ end
64
+
65
+ it 'sets @running to true when executed' do
66
+ timer_task.execute
67
+ expect(timer_task.instance_variable_get(:@running)).to be true
68
+ timer_task.kill
69
+ end
70
+
71
+ it 'creates a new thread' do
72
+ timer_task.execute
73
+ thread = timer_task.instance_variable_get(:@thread)
74
+ expect(thread).to be_a(Thread)
75
+ timer_task.kill
76
+ end
77
+
78
+ it 'does not start multiple times' do
79
+ timer_task.execute
80
+ first_thread = timer_task.instance_variable_get(:@thread)
81
+ timer_task.execute
82
+ second_thread = timer_task.instance_variable_get(:@thread)
83
+ expect(first_thread).to eq(second_thread)
84
+ timer_task.kill
85
+ end
86
+
87
+ it 'does not execute if already killed' do
88
+ timer_task.instance_variable_set(:@killed, true)
89
+ result = timer_task.execute
90
+ expect(result).to eq(timer_task)
91
+ expect(timer_task.instance_variable_get(:@thread)).to be_nil
92
+ end
93
+ end
94
+
95
+ describe '#kill' do
96
+ it 'returns true when successfully killed' do
97
+ timer_task.execute
98
+ expect(timer_task.kill).to be true
99
+ end
100
+
101
+ it 'returns false when already killed' do
102
+ timer_task.execute
103
+ timer_task.kill
104
+ expect(timer_task.kill).to be false
105
+ end
106
+
107
+ it 'sets @killed to true' do
108
+ timer_task.execute
109
+ timer_task.kill
110
+ expect(timer_task.instance_variable_get(:@killed)).to be true
111
+ end
112
+
113
+ it 'sets @running to false' do
114
+ timer_task.execute
115
+ timer_task.kill
116
+ expect(timer_task.instance_variable_get(:@running)).to be false
117
+ end
118
+
119
+ it 'kills the thread if alive' do
120
+ timer_task.execute
121
+ thread = timer_task.instance_variable_get(:@thread)
122
+ timer_task.kill
123
+ sleep(0.01) # Give time for thread to be killed
124
+ expect(thread.alive?).to be false
125
+ end
126
+
127
+ it 'is safe to call multiple times' do
128
+ timer_task.execute
129
+ expect { timer_task.kill }.not_to raise_error
130
+ expect { timer_task.kill }.not_to raise_error
131
+ end
132
+
133
+ it 'handles case when thread is nil' do
134
+ timer = described_class.new(execution_interval: 1) {}
135
+ result = nil
136
+ expect { result = timer.kill }.not_to raise_error
137
+ expect(result).to be true
138
+ end
139
+ end
140
+
141
+ describe 'execution behavior' do
142
+ it 'executes the task at the specified interval' do
143
+ execution_count = 0
144
+ timer = described_class.new(execution_interval: 0.05) do
145
+ execution_count += 1
146
+ end
147
+
148
+ timer.execute
149
+ sleep(0.15) # Should allow for ~3 executions
150
+ timer.kill
151
+
152
+ expect(execution_count).to be >= 2
153
+ expect(execution_count).to be <= 4 # Allow some timing variance
154
+ end
155
+
156
+ it 'calls the task block correctly' do
157
+ task_called = false
158
+ timer = described_class.new(execution_interval: 0.05) do
159
+ task_called = true
160
+ end
161
+
162
+ timer.execute
163
+ sleep(0.1)
164
+ timer.kill
165
+
166
+ expect(task_called).to be true
167
+ end
168
+
169
+ it 'handles exceptions in the task gracefully' do
170
+ error_count = 0
171
+ timer = described_class.new(execution_interval: 0.05) do
172
+ error_count += 1
173
+ raise StandardError, 'Test error'
174
+ end
175
+
176
+ # Capture stderr to check for error messages
177
+ original_stderr = $stderr
178
+ captured_stderr = StringIO.new
179
+ $stderr = captured_stderr
180
+
181
+ # Mock warn method to prevent warning gem from raising exceptions
182
+ # but still capture the output
183
+ allow_any_instance_of(Object).to receive(:warn) do |*args|
184
+ captured_stderr.puts(*args)
185
+ end
186
+
187
+ timer.execute
188
+ sleep(0.15)
189
+ timer.kill
190
+
191
+ error_output = captured_stderr.string
192
+ $stderr = original_stderr
193
+
194
+ expect(error_count).to be >= 2
195
+ expect(error_output).to include('Test error')
196
+ end
197
+
198
+ it 'continues execution after exceptions' do
199
+ execution_count = 0
200
+ timer = described_class.new(execution_interval: 0.05) do
201
+ execution_count += 1
202
+ raise StandardError, 'Test error' if execution_count == 1
203
+ end
204
+
205
+ # Mock warn method to prevent warning gem from raising exceptions
206
+ allow_any_instance_of(Object).to receive(:warn)
207
+
208
+ timer.execute
209
+ sleep(0.15)
210
+ timer.kill
211
+
212
+ expect(execution_count).to be >= 2 # Should continue after first error
213
+ end
214
+
215
+ it 'stops execution when killed' do
216
+ execution_count = 0
217
+ timer = described_class.new(execution_interval: 0.05) do
218
+ execution_count += 1
219
+ end
220
+
221
+ timer.execute
222
+ sleep(0.1)
223
+ initial_count = execution_count
224
+ timer.kill
225
+ sleep(0.1)
226
+ final_count = execution_count
227
+
228
+ expect(final_count).to eq(initial_count)
229
+ end
230
+
231
+ it 'respects the execution interval' do
232
+ execution_times = []
233
+ timer = described_class.new(execution_interval: 0.1) do
234
+ execution_times << Time.now
235
+ end
236
+
237
+ timer.execute
238
+ sleep(0.35) # Allow for ~3 executions
239
+ timer.kill
240
+
241
+ expect(execution_times.length).to be >= 2
242
+ if execution_times.length >= 2
243
+ interval = execution_times[1] - execution_times[0]
244
+ expect(interval).to be_within(0.05).of(0.1)
245
+ end
246
+ end
247
+ end
248
+
249
+ describe 'thread safety' do
250
+ it 'can be safely accessed from multiple threads' do
251
+ timer = described_class.new(execution_interval: 0.1) {}
252
+
253
+ threads = 10.times.map do
254
+ Thread.new do
255
+ timer.execute
256
+ sleep(0.01)
257
+ timer.kill
258
+ end
259
+ end
260
+
261
+ threads.each(&:join)
262
+ # Timer should be stopped after all threads complete
263
+ expect(timer.instance_variable_get(:@killed)).to be true
264
+ end
265
+
266
+ it 'handles concurrent execute calls safely' do
267
+ timer = described_class.new(execution_interval: 0.1) {}
268
+
269
+ threads = 5.times.map do
270
+ Thread.new { timer.execute }
271
+ end
272
+
273
+ threads.each(&:join)
274
+
275
+ # Should only have one thread created
276
+ expect(timer.instance_variable_get(:@thread)).to be_a(Thread)
277
+ timer.kill
278
+ end
279
+
280
+ it 'handles concurrent kill calls safely' do
281
+ timer = described_class.new(execution_interval: 0.1) {}
282
+ timer.execute
283
+
284
+ threads = 5.times.map do
285
+ Thread.new { timer.kill }
286
+ end
287
+
288
+ results = threads.map(&:value)
289
+
290
+ # Only one kill should return true, others should return false
291
+ true_count = results.count(true)
292
+ false_count = results.count(false)
293
+
294
+ expect(true_count).to eq(1)
295
+ expect(false_count).to eq(4)
296
+ end
297
+ end
298
+ 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