shoryuken 6.2.1 → 7.0.2

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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push.yml +36 -0
  3. data/.github/workflows/specs.yml +49 -44
  4. data/.github/workflows/verify-action-pins.yml +16 -0
  5. data/.gitignore +4 -1
  6. data/.rspec +3 -1
  7. data/.rubocop.yml +6 -1
  8. data/.ruby-version +1 -0
  9. data/.yard-lint.yml +279 -0
  10. data/CHANGELOG.md +308 -139
  11. data/Gemfile +1 -8
  12. data/Gemfile.lint +9 -0
  13. data/Gemfile.lint.lock +69 -0
  14. data/README.md +16 -33
  15. data/Rakefile +6 -10
  16. data/bin/clean_sqs +52 -0
  17. data/bin/cli/base.rb +22 -2
  18. data/bin/cli/sqs.rb +74 -7
  19. data/bin/integrations +275 -0
  20. data/bin/scenario +154 -0
  21. data/bin/shoryuken +3 -2
  22. data/docker-compose.yml +6 -0
  23. data/lib/{shoryuken/extensions/active_job_extensions.rb → active_job/extensions.rb} +20 -6
  24. data/lib/active_job/queue_adapters/shoryuken_adapter.rb +208 -0
  25. data/lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter.rb +78 -0
  26. data/lib/shoryuken/active_job/current_attributes.rb +139 -0
  27. data/lib/shoryuken/active_job/job_wrapper.rb +28 -0
  28. data/lib/shoryuken/body_parser.rb +11 -1
  29. data/lib/shoryuken/client.rb +16 -0
  30. data/lib/shoryuken/default_exception_handler.rb +11 -0
  31. data/lib/shoryuken/default_worker_registry.rb +39 -11
  32. data/lib/shoryuken/environment_loader.rb +85 -15
  33. data/lib/shoryuken/errors.rb +36 -0
  34. data/lib/shoryuken/fetcher.rb +41 -3
  35. data/lib/shoryuken/helpers/atomic_boolean.rb +58 -0
  36. data/lib/shoryuken/helpers/atomic_counter.rb +104 -0
  37. data/lib/shoryuken/helpers/atomic_hash.rb +182 -0
  38. data/lib/shoryuken/helpers/hash_utils.rb +56 -0
  39. data/lib/shoryuken/helpers/string_utils.rb +65 -0
  40. data/lib/shoryuken/helpers/timer_task.rb +80 -0
  41. data/lib/shoryuken/inline_message.rb +22 -0
  42. data/lib/shoryuken/launcher.rb +55 -0
  43. data/lib/shoryuken/logging/base.rb +26 -0
  44. data/lib/shoryuken/logging/pretty.rb +25 -0
  45. data/lib/shoryuken/logging/without_timestamp.rb +25 -0
  46. data/lib/shoryuken/logging.rb +43 -15
  47. data/lib/shoryuken/manager.rb +84 -5
  48. data/lib/shoryuken/message.rb +116 -1
  49. data/lib/shoryuken/middleware/chain.rb +141 -43
  50. data/lib/shoryuken/middleware/entry.rb +30 -0
  51. data/lib/shoryuken/middleware/server/active_record.rb +10 -0
  52. data/lib/shoryuken/middleware/server/auto_delete.rb +12 -0
  53. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +37 -11
  54. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +34 -3
  55. data/lib/shoryuken/middleware/server/non_retryable_exception.rb +95 -0
  56. data/lib/shoryuken/middleware/server/timing.rb +13 -0
  57. data/lib/shoryuken/options.rb +154 -13
  58. data/lib/shoryuken/polling/base_strategy.rb +127 -0
  59. data/lib/shoryuken/polling/queue_configuration.rb +103 -0
  60. data/lib/shoryuken/polling/strict_priority.rb +41 -0
  61. data/lib/shoryuken/polling/weighted_round_robin.rb +44 -0
  62. data/lib/shoryuken/processor.rb +37 -3
  63. data/lib/shoryuken/queue.rb +99 -8
  64. data/lib/shoryuken/runner.rb +54 -16
  65. data/lib/shoryuken/util.rb +32 -7
  66. data/lib/shoryuken/version.rb +4 -1
  67. data/lib/shoryuken/worker/default_executor.rb +23 -1
  68. data/lib/shoryuken/worker/inline_executor.rb +33 -2
  69. data/lib/shoryuken/worker.rb +224 -0
  70. data/lib/shoryuken/worker_registry.rb +35 -0
  71. data/lib/shoryuken.rb +27 -38
  72. data/renovate.json +62 -0
  73. data/shoryuken.gemspec +8 -4
  74. data/spec/integration/.rspec +1 -0
  75. data/spec/integration/active_job/adapter_configuration/configuration_spec.rb +26 -0
  76. data/spec/integration/active_job/bulk_enqueue/bulk_enqueue_spec.rb +53 -0
  77. data/spec/integration/active_job/current_attributes/bulk_enqueue_spec.rb +50 -0
  78. data/spec/integration/active_job/current_attributes/complex_types_spec.rb +55 -0
  79. data/spec/integration/active_job/current_attributes/empty_context_spec.rb +41 -0
  80. data/spec/integration/active_job/current_attributes/full_context_spec.rb +63 -0
  81. data/spec/integration/active_job/current_attributes/partial_context_spec.rb +57 -0
  82. data/spec/integration/active_job/custom_attributes/number_attributes_spec.rb +37 -0
  83. data/spec/integration/active_job/custom_attributes/string_attributes_spec.rb +39 -0
  84. data/spec/integration/active_job/error_handling/job_wrapper_spec.rb +53 -0
  85. data/spec/integration/active_job/fifo_and_attributes/deduplication_spec.rb +86 -0
  86. data/spec/integration/active_job/keyword_arguments/keyword_arguments_spec.rb +63 -0
  87. data/spec/integration/active_job/retry/discard_on_spec.rb +43 -0
  88. data/spec/integration/active_job/retry/retry_on_spec.rb +36 -0
  89. data/spec/integration/active_job/roundtrip/roundtrip_spec.rb +52 -0
  90. data/spec/integration/active_job/scheduled/scheduled_spec.rb +76 -0
  91. data/spec/integration/active_record_middleware/active_record_middleware_spec.rb +84 -0
  92. data/spec/integration/auto_delete/auto_delete_spec.rb +53 -0
  93. data/spec/integration/auto_extend_visibility/auto_extend_visibility_spec.rb +57 -0
  94. data/spec/integration/aws_config/aws_config_spec.rb +59 -0
  95. data/spec/integration/batch_processing/batch_processing_spec.rb +37 -0
  96. data/spec/integration/body_parser/json_parser_spec.rb +45 -0
  97. data/spec/integration/body_parser/proc_parser_spec.rb +54 -0
  98. data/spec/integration/body_parser/text_parser_spec.rb +43 -0
  99. data/spec/integration/concurrent_processing/concurrent_processing_spec.rb +45 -0
  100. data/spec/integration/custom_group_polling_strategy/custom_group_polling_strategy_spec.rb +87 -0
  101. data/spec/integration/dead_letter_queue/dead_letter_queue_spec.rb +91 -0
  102. data/spec/integration/exception_handlers/exception_handlers_spec.rb +69 -0
  103. data/spec/integration/exponential_backoff/exponential_backoff_spec.rb +67 -0
  104. data/spec/integration/fifo_ordering/fifo_ordering_spec.rb +44 -0
  105. data/spec/integration/large_payloads/large_payloads_spec.rb +30 -0
  106. data/spec/integration/launcher/launcher_spec.rb +40 -0
  107. data/spec/integration/message_attributes/message_attributes_spec.rb +54 -0
  108. data/spec/integration/message_operations/message_operations_spec.rb +59 -0
  109. data/spec/integration/middleware_chain/empty_chain_spec.rb +11 -0
  110. data/spec/integration/middleware_chain/execution_order_spec.rb +33 -0
  111. data/spec/integration/middleware_chain/removal_spec.rb +31 -0
  112. data/spec/integration/middleware_chain/short_circuit_spec.rb +40 -0
  113. data/spec/integration/non_retryable_exception/non_retryable_exception_spec.rb +149 -0
  114. data/spec/integration/polling_strategies/polling_strategies_spec.rb +46 -0
  115. data/spec/integration/queue_operations/queue_operations_spec.rb +84 -0
  116. data/spec/integration/rails/rails_72/Gemfile +6 -0
  117. data/spec/integration/rails/rails_72/activejob_adapter_spec.rb +98 -0
  118. data/spec/integration/rails/rails_80/Gemfile +6 -0
  119. data/spec/integration/rails/rails_80/activejob_adapter_spec.rb +98 -0
  120. data/spec/integration/rails/rails_80/continuation_spec.rb +79 -0
  121. data/spec/integration/rails/rails_81/Gemfile +6 -0
  122. data/spec/integration/rails/rails_81/activejob_adapter_spec.rb +98 -0
  123. data/spec/integration/rails/rails_81/continuation_spec.rb +79 -0
  124. data/spec/integration/retry_behavior/retry_behavior_spec.rb +45 -0
  125. data/spec/integration/spec_helper.rb +7 -0
  126. data/spec/integration/strict_priority_polling/strict_priority_polling_spec.rb +58 -0
  127. data/spec/integration/visibility_timeout/visibility_timeout_spec.rb +37 -0
  128. data/spec/integration/worker_enqueueing/worker_enqueueing_spec.rb +60 -0
  129. data/spec/integration/worker_groups/worker_groups_spec.rb +79 -0
  130. data/spec/integration/worker_lifecycle/worker_lifecycle_spec.rb +33 -0
  131. data/spec/integrations_helper.rb +243 -0
  132. data/spec/lib/active_job/extensions_spec.rb +225 -0
  133. data/spec/lib/active_job/queue_adapters/shoryuken_adapter_spec.rb +29 -0
  134. data/spec/{shoryuken/extensions/active_job_concurrent_send_adapter_spec.rb → lib/active_job/queue_adapters/shoryuken_concurrent_send_adapter_spec.rb} +5 -4
  135. data/spec/{shoryuken/extensions/active_job_wrapper_spec.rb → lib/shoryuken/active_job/job_wrapper_spec.rb} +6 -5
  136. data/spec/{shoryuken → lib/shoryuken}/body_parser_spec.rb +2 -4
  137. data/spec/{shoryuken → lib/shoryuken}/client_spec.rb +1 -1
  138. data/spec/{shoryuken → lib/shoryuken}/default_exception_handler_spec.rb +9 -10
  139. data/spec/{shoryuken → lib/shoryuken}/default_worker_registry_spec.rb +1 -2
  140. data/spec/{shoryuken → lib/shoryuken}/environment_loader_spec.rb +10 -9
  141. data/spec/{shoryuken → lib/shoryuken}/fetcher_spec.rb +23 -26
  142. data/spec/lib/shoryuken/helpers/atomic_boolean_spec.rb +196 -0
  143. data/spec/lib/shoryuken/helpers/atomic_counter_spec.rb +177 -0
  144. data/spec/lib/shoryuken/helpers/atomic_hash_spec.rb +307 -0
  145. data/spec/lib/shoryuken/helpers/hash_utils_spec.rb +145 -0
  146. data/spec/lib/shoryuken/helpers/string_utils_spec.rb +124 -0
  147. data/spec/lib/shoryuken/helpers/timer_task_spec.rb +298 -0
  148. data/spec/lib/shoryuken/helpers_integration_spec.rb +96 -0
  149. data/spec/lib/shoryuken/inline_message_spec.rb +196 -0
  150. data/spec/{shoryuken → lib/shoryuken}/launcher_spec.rb +23 -2
  151. data/spec/lib/shoryuken/logging_spec.rb +242 -0
  152. data/spec/{shoryuken → lib/shoryuken}/manager_spec.rb +1 -2
  153. data/spec/lib/shoryuken/message_spec.rb +109 -0
  154. data/spec/{shoryuken → lib/shoryuken}/middleware/chain_spec.rb +1 -1
  155. data/spec/lib/shoryuken/middleware/entry_spec.rb +68 -0
  156. data/spec/lib/shoryuken/middleware/server/active_record_spec.rb +133 -0
  157. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_delete_spec.rb +1 -1
  158. data/spec/{shoryuken → lib/shoryuken}/middleware/server/auto_extend_visibility_spec.rb +51 -1
  159. data/spec/{shoryuken → lib/shoryuken}/middleware/server/exponential_backoff_retry_spec.rb +1 -1
  160. data/spec/lib/shoryuken/middleware/server/non_retryable_exception_spec.rb +214 -0
  161. data/spec/{shoryuken → lib/shoryuken}/middleware/server/timing_spec.rb +1 -1
  162. data/spec/{shoryuken → lib/shoryuken}/options_spec.rb +49 -6
  163. data/spec/lib/shoryuken/polling/base_strategy_spec.rb +280 -0
  164. data/spec/lib/shoryuken/polling/queue_configuration_spec.rb +195 -0
  165. data/spec/{shoryuken → lib/shoryuken}/polling/strict_priority_spec.rb +1 -1
  166. data/spec/{shoryuken → lib/shoryuken}/polling/weighted_round_robin_spec.rb +1 -1
  167. data/spec/{shoryuken → lib/shoryuken}/processor_spec.rb +1 -1
  168. data/spec/{shoryuken → lib/shoryuken}/queue_spec.rb +2 -3
  169. data/spec/{shoryuken → lib/shoryuken}/runner_spec.rb +1 -3
  170. data/spec/{shoryuken → lib/shoryuken}/util_spec.rb +2 -2
  171. data/spec/lib/shoryuken/version_spec.rb +17 -0
  172. data/spec/{shoryuken → lib/shoryuken}/worker/default_executor_spec.rb +1 -1
  173. data/spec/lib/shoryuken/worker/inline_executor_spec.rb +105 -0
  174. data/spec/lib/shoryuken/worker_registry_spec.rb +63 -0
  175. data/spec/{shoryuken → lib/shoryuken}/worker_spec.rb +15 -11
  176. data/spec/{shoryuken_spec.rb → lib/shoryuken_spec.rb} +1 -1
  177. data/spec/shared_examples_for_active_job.rb +40 -15
  178. data/spec/spec_helper.rb +48 -2
  179. metadata +295 -101
  180. data/.codeclimate.yml +0 -20
  181. data/.devcontainer/Dockerfile +0 -17
  182. data/.devcontainer/base.Dockerfile +0 -43
  183. data/.devcontainer/devcontainer.json +0 -35
  184. data/.github/FUNDING.yml +0 -12
  185. data/.github/dependabot.yml +0 -6
  186. data/.github/workflows/stale.yml +0 -20
  187. data/.reek.yml +0 -5
  188. data/Appraisals +0 -42
  189. data/gemfiles/.gitignore +0 -1
  190. data/gemfiles/aws_sdk_core_2.gemfile +0 -21
  191. data/gemfiles/rails_4_2.gemfile +0 -20
  192. data/gemfiles/rails_5_2.gemfile +0 -21
  193. data/gemfiles/rails_6_0.gemfile +0 -21
  194. data/gemfiles/rails_6_1.gemfile +0 -21
  195. data/gemfiles/rails_7_0.gemfile +0 -22
  196. data/lib/shoryuken/core_ext.rb +0 -69
  197. data/lib/shoryuken/extensions/active_job_adapter.rb +0 -103
  198. data/lib/shoryuken/extensions/active_job_concurrent_send_adapter.rb +0 -50
  199. data/lib/shoryuken/polling/base.rb +0 -67
  200. data/shoryuken.jpg +0 -0
  201. data/spec/integration/launcher_spec.rb +0 -128
  202. data/spec/shoryuken/core_ext_spec.rb +0 -40
  203. data/spec/shoryuken/extensions/active_job_adapter_spec.rb +0 -7
  204. data/spec/shoryuken/extensions/active_job_base_spec.rb +0 -84
  205. data/spec/shoryuken/worker/inline_executor_spec.rb +0 -49
data/bin/integrations ADDED
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Shoryuken integration test runner
5
+ #
6
+ # Usage:
7
+ # bin/integrations # Run all integration tests
8
+ # bin/integrations fifo # Run tests with 'fifo' in path
9
+ # bin/integrations rails/rails_72 # Run Rails 7.2 tests
10
+ # bin/integrations batch retry # Run tests matching 'batch' OR 'retry'
11
+ # bin/integrations -v fifo # Run with verbose output
12
+
13
+ require 'bundler'
14
+ require 'fileutils'
15
+ require 'timeout'
16
+
17
+ TIMEOUT = 300 # 5 minutes per scenario
18
+ SPEC_DIR = File.expand_path('../spec/integration', __dir__)
19
+ ROOT_DIR = File.expand_path('..', __dir__)
20
+
21
+ class IntegrationRunner
22
+ def initialize(args)
23
+ @verbose = args.delete('-v') || args.delete('--verbose')
24
+ @filters = args.reject { |a| a.start_with?('-') }
25
+ end
26
+
27
+ def run
28
+ specs = find_specs
29
+ specs = filter_specs(specs) if @filters.any?
30
+
31
+ if specs.empty?
32
+ puts 'No specs found matching filters'
33
+ exit 1
34
+ end
35
+
36
+ puts "Running #{specs.size} integration specs..."
37
+ puts
38
+
39
+ results = run_specs(specs)
40
+ report_results(results)
41
+ end
42
+
43
+ private
44
+
45
+ def find_specs
46
+ Dir.glob(File.join(SPEC_DIR, '**/*_spec.rb')).reject do |path|
47
+ # Exclude vendor and .bundle directories
48
+ path.include?('/vendor/') || path.include?('/.bundle/')
49
+ end.map do |path|
50
+ relative_path = path.sub("#{SPEC_DIR}/", '')
51
+ dir = File.dirname(path)
52
+ gemfile = File.exist?(File.join(dir, 'Gemfile')) ? File.join(dir, 'Gemfile') : File.join(ROOT_DIR, 'Gemfile')
53
+
54
+ {
55
+ name: relative_path.sub('_spec.rb', '').gsub('/', ' / '),
56
+ path: path,
57
+ relative_path: relative_path,
58
+ directory: dir,
59
+ gemfile: gemfile
60
+ }
61
+ end.sort_by { |s| s[:relative_path] }
62
+ end
63
+
64
+ def filter_specs(specs)
65
+ specs.select do |spec|
66
+ @filters.any? { |filter| spec[:relative_path].include?(filter) }
67
+ end
68
+ end
69
+
70
+ def run_specs(specs)
71
+ results = []
72
+
73
+ specs.each do |spec|
74
+ result = run_spec(spec)
75
+ results << result
76
+
77
+ if result[:skipped]
78
+ print 'S'
79
+ elsif result[:success]
80
+ print '.'
81
+ else
82
+ print 'F'
83
+ end
84
+ $stdout.flush
85
+ end
86
+
87
+ puts
88
+ results
89
+ end
90
+
91
+ def run_spec(spec)
92
+ # Start with a clean bundler environment to prevent pollution between tests
93
+ env = {
94
+ 'BUNDLE_GEMFILE' => spec[:gemfile],
95
+ 'RAILS_ENV' => 'test',
96
+ 'RUBYOPT' => nil # Clear any -rbundler/setup from CI or previous tests
97
+ }
98
+
99
+ # Install dependencies if using a local Gemfile
100
+ uses_local_gemfile = spec[:gemfile] != File.join(ROOT_DIR, 'Gemfile')
101
+ if uses_local_gemfile
102
+ install_result = install_bundle(spec, env)
103
+ unless install_result[:success]
104
+ # Skip test if bundle install fails (e.g., gems not available in CI)
105
+ return {
106
+ spec: spec,
107
+ success: true,
108
+ skipped: true,
109
+ skip_reason: 'Bundle install failed (dependencies not available)',
110
+ output: install_result[:output]
111
+ }
112
+ end
113
+
114
+ # Use isolated bundle config to match install_bundle
115
+ bundle_path = File.join(spec[:directory], 'vendor', 'bundle')
116
+ bundle_config = File.join(spec[:directory], '.bundle')
117
+ env['BUNDLE_PATH'] = bundle_path
118
+ env['BUNDLE_FROZEN'] = 'false'
119
+ env['BUNDLE_APP_CONFIG'] = bundle_config
120
+ end
121
+
122
+ # Run the spec
123
+ # For local gemfiles, use standalone bundle setup which doesn't need bundler at runtime
124
+ # This avoids issues with bundle exec inheriting the wrong config
125
+ cmd = if uses_local_gemfile
126
+ standalone_setup = File.join(bundle_path, 'bundler', 'setup.rb')
127
+ ['ruby', "-r#{standalone_setup}", File.join(ROOT_DIR, 'bin/scenario'), spec[:path]]
128
+ else
129
+ ['bundle', 'exec', 'ruby', File.join(ROOT_DIR, 'bin/scenario'), spec[:path]]
130
+ end
131
+
132
+ output = []
133
+ start_time = Time.now
134
+
135
+ begin
136
+ Timeout.timeout(TIMEOUT) do
137
+ # Use unbundled env to prevent pollution from previous test runs
138
+ # This is especially important after Rails integration tests that use
139
+ # bundle install --standalone with different gem versions
140
+ Bundler.with_unbundled_env do
141
+ IO.popen(env, cmd, chdir: spec[:directory], err: [:child, :out]) do |io|
142
+ io.each_line { |line| output << line }
143
+ end
144
+ end
145
+ end
146
+
147
+ {
148
+ spec: spec,
149
+ success: $?.success?,
150
+ exit_code: $?.exitstatus,
151
+ duration: Time.now - start_time,
152
+ output: output.join
153
+ }
154
+ rescue Timeout::Error
155
+ {
156
+ spec: spec,
157
+ success: false,
158
+ exit_code: -1,
159
+ error: 'Timeout',
160
+ duration: Time.now - start_time,
161
+ output: output.join
162
+ }
163
+ end
164
+ end
165
+
166
+ def install_bundle(spec, env)
167
+ return { success: true } if @bundle_installed&.include?(spec[:gemfile])
168
+
169
+ output = []
170
+
171
+ # Create isolated bundle environment to avoid CI cache interference
172
+ # Use a unique path per Gemfile to avoid conflicts
173
+ bundle_path = File.join(spec[:directory], 'vendor', 'bundle')
174
+ bundle_config = File.join(spec[:directory], '.bundle')
175
+
176
+ # Create local .bundle/config to override project-level config
177
+ FileUtils.mkdir_p(bundle_config)
178
+ File.write(File.join(bundle_config, 'config'), <<~CONFIG)
179
+ ---
180
+ BUNDLE_PATH: "#{bundle_path}"
181
+ BUNDLE_FROZEN: "false"
182
+ CONFIG
183
+
184
+ clean_env = env.merge(
185
+ 'BUNDLE_PATH' => bundle_path,
186
+ 'BUNDLE_FROZEN' => 'false',
187
+ 'BUNDLE_DEPLOYMENT' => nil,
188
+ 'BUNDLE_WITHOUT' => nil,
189
+ 'BUNDLE_CACHE_PATH' => nil,
190
+ 'BUNDLE_BIN' => nil,
191
+ 'BUNDLE_APP_CONFIG' => bundle_config,
192
+ 'RUBYOPT' => nil # Clear any -rbundler/setup from CI
193
+ )
194
+
195
+ # Use --standalone to generate a setup.rb that doesn't need bundler at runtime
196
+ # Run in a completely unbundled environment using Bundler API
197
+ cmd_script = <<~RUBY
198
+ require 'bundler'
199
+ Bundler.with_unbundled_env do
200
+ system({'BUNDLE_GEMFILE' => '#{spec[:gemfile]}', 'BUNDLE_PATH' => '#{bundle_path}', 'BUNDLE_FROZEN' => 'false', 'BUNDLE_APP_CONFIG' => '#{bundle_config}'}, 'bundle', 'install', '--standalone')
201
+ end
202
+ exit($?.success? ? 0 : 1)
203
+ RUBY
204
+
205
+ IO.popen(['ruby', '-e', cmd_script], chdir: spec[:directory], err: [:child, :out]) do |io|
206
+ io.each_line { |line| output << line }
207
+ end
208
+
209
+ @bundle_installed ||= []
210
+ @bundle_installed << spec[:gemfile] if $?.success?
211
+
212
+ {
213
+ spec: spec,
214
+ success: $?.success?,
215
+ output: output.join,
216
+ error: $?.success? ? nil : 'Bundle install failed',
217
+ bundle_config: bundle_config
218
+ }
219
+ end
220
+
221
+ def report_results(results)
222
+ skipped = results.select { |r| r[:skipped] }
223
+ failed = results.reject { |r| r[:success] || r[:skipped] }
224
+ passed = results.count { |r| r[:success] && !r[:skipped] }
225
+ total = results.size
226
+
227
+ puts
228
+ summary = "#{passed}/#{total} passed"
229
+ summary += ", #{skipped.size} skipped" if skipped.any?
230
+ puts summary
231
+
232
+ if skipped.any?
233
+ puts
234
+ puts 'Skipped:'
235
+ puts
236
+ skipped.each do |result|
237
+ puts " - #{result[:spec][:name]}"
238
+ puts " Reason: #{result[:skip_reason]}" if result[:skip_reason]
239
+ if result[:output] && !result[:output].strip.empty?
240
+ lines = result[:output].lines.last(15)
241
+ lines.each { |line| puts " #{line}" }
242
+ end
243
+ end
244
+ end
245
+
246
+ if failed.any?
247
+ puts
248
+ puts 'Failures:'
249
+ puts
250
+
251
+ failed.each_with_index do |result, idx|
252
+ puts " #{idx + 1}) #{result[:spec][:name]}"
253
+ if result[:error]
254
+ puts " Error: #{result[:error]}"
255
+ end
256
+ if result[:output] && !result[:output].strip.empty?
257
+ # Show last 30 lines of output for context
258
+ lines = result[:output].lines
259
+ if lines.size > 30
260
+ puts " ... (#{lines.size - 30} lines truncated)"
261
+ lines = lines.last(30)
262
+ end
263
+ lines.each { |line| puts " #{line}" }
264
+ end
265
+ puts
266
+ end
267
+ end
268
+
269
+ exit(failed.empty? ? 0 : 1)
270
+ end
271
+ end
272
+
273
+ if __FILE__ == $0
274
+ IntegrationRunner.new(ARGV.dup).run
275
+ end
data/bin/scenario ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Individual scenario runner for integration testing
5
+ # This script runs a single integration test file in complete isolation
6
+
7
+ require 'bundler/setup'
8
+
9
+ # Exit codes
10
+ EXIT_SUCCESS = 0
11
+ EXIT_FAILURE = 1
12
+ EXIT_TIMEOUT = 2
13
+ EXIT_SETUP_ERROR = 3
14
+
15
+ class ScenarioRunner
16
+ attr_reader :test_file
17
+
18
+ def initialize(test_file)
19
+ @test_file = test_file
20
+ @exit_code = EXIT_SUCCESS
21
+ end
22
+
23
+ def run
24
+ puts "Running: #{File.basename(test_file)}" if ENV['VERBOSE']
25
+
26
+ # Set up the scenario-specific environment
27
+ setup_scenario
28
+
29
+ # Load and run the test file
30
+ load_and_run_test
31
+
32
+ exit EXIT_SUCCESS
33
+ rescue => e
34
+ puts "FAILED: #{File.basename(test_file)} - #{e.message}" if ENV['VERBOSE']
35
+ puts e.backtrace.first(5).join("\n") if ENV['VERBOSE']
36
+ exit EXIT_FAILURE
37
+ end
38
+
39
+ private
40
+
41
+ def setup_scenario
42
+ # Each test handles its own specific requirements
43
+ require 'bundler/setup'
44
+
45
+ puts "Setting up isolated test environment" if ENV['VERBOSE']
46
+ end
47
+
48
+
49
+ def load_and_run_test
50
+ # Test file might be relative to current directory
51
+ if File.exist?(test_file)
52
+ absolute_test_path = File.expand_path(test_file)
53
+ else
54
+ # Fallback to project root resolution
55
+ project_root = File.expand_path('..', __dir__)
56
+ absolute_test_path = File.join(project_root, test_file)
57
+ end
58
+
59
+ puts "Current directory: #{Dir.pwd}" if ENV['VERBOSE']
60
+ puts "Loading test file: #{absolute_test_path}" if ENV['VERBOSE']
61
+
62
+ unless File.exist?(absolute_test_path)
63
+ raise "Test file not found: #{absolute_test_path}"
64
+ end
65
+
66
+ # Check if this is an RSpec file (contains RSpec.describe or describe)
67
+ file_content = File.read(absolute_test_path, encoding: 'UTF-8')
68
+ if file_content.match?(/\b(?:RSpec\.describe|describe)\b/)
69
+ puts "Running as RSpec test" if ENV['VERBOSE']
70
+ run_rspec_test(absolute_test_path)
71
+ else
72
+ puts "Running as plain Ruby test" if ENV['VERBOSE']
73
+ # Load integrations_helper for plain Ruby integration tests
74
+ if absolute_test_path.include?('spec/integration')
75
+ project_root = File.expand_path('..', __dir__)
76
+ integrations_helper = File.join(project_root, 'spec', 'integrations_helper.rb')
77
+ require integrations_helper
78
+ end
79
+ # Load as plain Ruby test
80
+ load absolute_test_path
81
+ end
82
+ end
83
+
84
+ def run_rspec_test(test_file_path)
85
+ # Change to project root for RSpec to find spec_helper
86
+ project_root = File.expand_path('..', __dir__)
87
+ Dir.chdir(project_root) do
88
+ # Disable SimpleCov for integration tests to avoid coverage failures
89
+ ENV['SIMPLECOV_DISABLED'] = 'true'
90
+
91
+ # Make the test file path relative to project root for RSpec
92
+ relative_test_path = test_file_path.sub("#{project_root}/", '')
93
+
94
+ puts "Running RSpec with file: #{relative_test_path}" if ENV['VERBOSE']
95
+ puts "Working directory: #{Dir.pwd}" if ENV['VERBOSE']
96
+
97
+ # Check if this test requires Rails but Rails is not available
98
+ if requires_rails?(test_file_path) && !rails_available?
99
+ puts "Skipping #{File.basename(test_file_path)} - Rails not available"
100
+ return
101
+ end
102
+
103
+ # Run RSpec with the specific test file
104
+ require 'rspec/core'
105
+
106
+ # Load integration spec_helper for integration tests
107
+ if relative_test_path.include?('spec/integration/')
108
+ require_relative '../spec/integration/spec_helper'
109
+ end
110
+
111
+ result = RSpec::Core::Runner.run([relative_test_path], $stderr, $stdout)
112
+
113
+ if result != 0
114
+ raise "RSpec failed with exit code #{result}"
115
+ end
116
+ ensure
117
+ # Clean up environment
118
+ ENV.delete('SIMPLECOV_DISABLED')
119
+ end
120
+ end
121
+
122
+ def requires_rails?(test_file_path)
123
+ # Check if the test file mentions Rails dependencies
124
+ content = File.read(test_file_path)
125
+ content.match?(/require.*rails|Rails::|ActiveJob::|ActionController::/)
126
+ end
127
+
128
+ def rails_available?
129
+ begin
130
+ require 'rails'
131
+ true
132
+ rescue LoadError
133
+ false
134
+ end
135
+ end
136
+ end
137
+
138
+ # Validate arguments
139
+ if ARGV.empty?
140
+ puts "Usage: bin/scenario <test_file>"
141
+ puts "Example: bin/scenario spec/integration/rails_integration_spec.rb"
142
+ exit EXIT_SETUP_ERROR
143
+ end
144
+
145
+ test_file = ARGV[0]
146
+
147
+ unless File.exist?(test_file)
148
+ puts "Test file not found: #{test_file}"
149
+ exit EXIT_SETUP_ERROR
150
+ end
151
+
152
+
153
+ # Run the scenario
154
+ ScenarioRunner.new(test_file).run
data/bin/shoryuken CHANGED
@@ -26,9 +26,10 @@ module Shoryuken
26
26
  method_option :logfile, aliases: '-L', type: :string, desc: 'Path to logfile'
27
27
  method_option :pidfile, aliases: '-P', type: :string, desc: 'Path to pidfile'
28
28
  method_option :verbose, aliases: '-v', type: :boolean, desc: 'Print more verbose output'
29
- method_option :delay, aliases: '-D', type: :numeric, desc: 'Number of seconds to pause fetching from an empty queue'
29
+ method_option :delay, aliases: '-D', type: :numeric,
30
+ desc: 'Number of seconds to pause fetching from an empty queue'
30
31
  def start
31
- opts = options.to_h.symbolize_keys
32
+ opts = options.to_h.transform_keys(&:to_sym)
32
33
 
33
34
  say '[DEPRECATED] Please use --config instead of --config-file', :yellow if opts[:config_file]
34
35
 
@@ -0,0 +1,6 @@
1
+ services:
2
+ elasticmq:
3
+ image: softwaremill/elasticmq-native:1.7.1
4
+ container_name: elasticmq
5
+ ports:
6
+ - "9324:9324"
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shoryuken
2
- module ActiveJobExtensions
4
+ module ActiveJob
3
5
  # Adds an accessor for SQS SendMessage parameters on ActiveJob jobs
4
6
  # (instances of ActiveJob::Base). Shoryuken ActiveJob queue adapters use
5
7
  # these parameters when enqueueing jobs; other adapters can ignore them.
@@ -7,6 +9,7 @@ module Shoryuken
7
9
  extend ActiveSupport::Concern
8
10
 
9
11
  included do
12
+ # @return [Hash] the SQS send message parameters
10
13
  attr_accessor :sqs_send_message_parameters
11
14
  end
12
15
  end
@@ -15,12 +18,23 @@ module Shoryuken
15
18
  # to the empty hash, and populates it whenever `#enqueue` is called, such
16
19
  # as when using ActiveJob::Base.set.
17
20
  module SQSSendMessageParametersSupport
18
- def initialize(*arguments)
19
- super(*arguments)
21
+ # Initializes a new ActiveJob instance with empty SQS parameters
22
+ #
23
+ # Uses argument forwarding (...) to properly pass all arguments including
24
+ # keyword arguments to the base class.
25
+ def initialize(...)
26
+ super(...)
20
27
  self.sqs_send_message_parameters = {}
21
28
  end
22
- ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
23
29
 
30
+ # Enqueues the job with optional SQS-specific parameters
31
+ #
32
+ # @param options [Hash] enqueue options
33
+ # @option options [Hash] :message_attributes custom SQS message attributes
34
+ # @option options [Hash] :message_system_attributes system attributes
35
+ # @option options [String] :message_deduplication_id FIFO deduplication ID
36
+ # @option options [String] :message_group_id FIFO message group ID
37
+ # @return [Object] the enqueue result
24
38
  def enqueue(options = {})
25
39
  sqs_options = options.extract! :message_attributes,
26
40
  :message_system_attributes,
@@ -34,5 +48,5 @@ module Shoryuken
34
48
  end
35
49
  end
36
50
 
37
- ActiveJob::Base.include Shoryuken::ActiveJobExtensions::SQSSendMessageParametersAccessor
38
- ActiveJob::Base.prepend Shoryuken::ActiveJobExtensions::SQSSendMessageParametersSupport
51
+ ActiveJob::Base.include Shoryuken::ActiveJob::SQSSendMessageParametersAccessor
52
+ ActiveJob::Base.prepend Shoryuken::ActiveJob::SQSSendMessageParametersSupport