bug_bunny 3.0.6 → 3.1.1

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.
@@ -1,82 +1,132 @@
1
- # lib/bug_bunny/session.rb
1
+ # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
4
  # Clase interna que encapsula una unidad de trabajo sobre una conexión RabbitMQ.
5
5
  #
6
- # Su responsabilidad principal es gestionar el ciclo de vida de un `Bunny::Channel`.
7
- # En RabbitMQ, las conexiones TCP son costosas, pero los canales son ligeros.
8
- # Esta clase toma una conexión abierta del Pool, abre un canal exclusivo para esta sesión,
9
- # configura el QoS y facilita la creación de Exchanges y Colas.
6
+ # Implementa la lógica de "Configuración en Cascada" para Exchanges y Colas,
7
+ # gestionando el ciclo de vida de un `Bunny::Channel` con resiliencia y carga perezosa.
10
8
  #
11
9
  # @api private
12
10
  class Session
13
- # Opciones por defecto para Exchanges: No durables, No auto-borrables.
11
+ # @!group Opciones por Defecto (Nivel 1: Gema)
12
+
13
+ # Opciones predeterminadas de la gema para Exchanges.
14
14
  DEFAULT_EXCHANGE_OPTIONS = { durable: false, auto_delete: false }.freeze
15
15
 
16
- # Opciones por defecto para Colas: No exclusivas, No durables, Auto-borrables.
17
- # @note Por defecto las colas son volátiles (`auto_delete: true`). Para workers persistentes,
18
- # se debe pasar explícitamente `durable: true, auto_delete: false`.
16
+ # Opciones predeterminadas de la gema para Colas.
19
17
  DEFAULT_QUEUE_OPTIONS = { exclusive: false, durable: false, auto_delete: true }.freeze
20
18
 
19
+ # @!endgroup
20
+
21
21
  # @return [Bunny::Session] La conexión TCP subyacente.
22
22
  attr_reader :connection
23
23
 
24
- # @return [Bunny::Channel] El canal AMQP abierto para esta sesión.
25
- attr_reader :channel
24
+ # Inicializa una nueva sesión sin abrir canales todavía.
25
+ #
26
+ # @param connection [Bunny::Session] Una conexión (puede estar abierta o cerrada temporalmente).
27
+ def initialize(connection)
28
+ @connection = connection
29
+ @channel = nil
30
+ end
26
31
 
27
- # Inicializa una nueva sesión.
32
+ # Obtiene el canal actual o crea uno nuevo si es necesario.
28
33
  #
29
- # 1. Verifica que la conexión esté viva.
30
- # 2. Abre un nuevo canal.
31
- # 3. Habilita "Publisher Confirms" para garantizar que los mensajes lleguen al broker.
32
- # 4. Configura el "Prefetch" (QoS) global para este canal.
34
+ # Este método es el punto central de la robustez. Verifica la salud
35
+ # de la conexión y del canal antes de devolverlo.
33
36
  #
34
- # @param connection [Bunny::Session] Una conexión abierta.
35
- # @raise [BugBunny::Error] Si la conexión es nil o está cerrada.
36
- def initialize(connection)
37
- raise BugBunny::Error, "Connection is closed or nil" unless connection&.open?
37
+ # @return [Bunny::Channel] Un canal abierto y configurado.
38
+ # @raise [BugBunny::CommunicationError] Si no se puede restablecer la conexión.
39
+ def channel
40
+ # Si el canal existe y está abierto, lo devolvemos rápido.
41
+ return @channel if @channel&.open?
38
42
 
39
- @connection = connection
40
- # Creamos canal nuevo para esta sesión (Thread-safe dentro del contexto del Pool)
41
- @channel = connection.create_channel
42
- @channel.confirm_select
43
- @channel.prefetch(BugBunny.configuration.channel_prefetch)
43
+ # Si no, intentamos asegurar la conexión y crear el canal.
44
+ ensure_connection!
45
+ create_channel!
46
+
47
+ @channel
44
48
  end
45
49
 
46
- # Factory method para declarar o recuperar un Exchange.
50
+ # Factory method para declarar o recuperar un Exchange aplicando la cascada de configuración.
51
+ #
52
+ # Jerarquía de fusión:
53
+ # 1. Defaults de la gema (`DEFAULT_EXCHANGE_OPTIONS`)
54
+ # 2. Configuración global (`BugBunny.configuration.exchange_options`)
55
+ # 3. Opciones específicas pasadas como argumento (`opts`)
47
56
  #
48
- # @param name [String, nil] El nombre del exchange. Si es nil/vacío, retorna el Default Exchange.
49
- # @param type [String, Symbol] El tipo de exchange (:direct, :topic, :fanout, :headers).
50
- # @param opts [Hash] Opciones de configuración (durable, auto_delete, arguments).
51
- # @return [Bunny::Exchange] La instancia del exchange.
57
+ # @param name [String, nil] Nombre del exchange.
58
+ # @param type [String, Symbol] Tipo de exchange ('direct', 'topic', 'fanout').
59
+ # @param opts [Hash] Opciones específicas de infraestructura para este intercambio.
60
+ # @return [Bunny::Exchange] El objeto exchange de Bunny configurado.
52
61
  def exchange(name: nil, type: 'direct', opts: {})
53
62
  return channel.default_exchange if name.nil? || name.empty?
54
63
 
55
- merged_opts = DEFAULT_EXCHANGE_OPTIONS.merge(opts)
56
- case type.to_sym
57
- when :topic then channel.topic(name, merged_opts)
58
- when :direct then channel.direct(name, merged_opts)
59
- when :fanout then channel.fanout(name, merged_opts)
60
- when :headers then channel.headers(name, merged_opts)
61
- else channel.direct(name, merged_opts)
62
- end
64
+ # Aplicación de la lógica de fusión en cascada
65
+ merged_opts = DEFAULT_EXCHANGE_OPTIONS
66
+ .merge(BugBunny.configuration.exchange_options || {})
67
+ .merge(opts)
68
+
69
+ # public_send permite llamar a :topic, :direct, etc. dinámicamente según el tipo
70
+ channel.public_send(type, name, merged_opts)
63
71
  end
64
72
 
65
- # Factory method para declarar o recuperar una Cola.
73
+ # Factory method para declarar o recuperar una Cola aplicando la cascada de configuración.
74
+ #
75
+ # Jerarquía de fusión:
76
+ # 1. Defaults de la gema (`DEFAULT_QUEUE_OPTIONS`)
77
+ # 2. Configuración global (`BugBunny.configuration.queue_options`)
78
+ # 3. Opciones específicas pasadas como argumento (`opts`)
66
79
  #
67
- # @param name [String] El nombre de la cola.
68
- # @param opts [Hash] Opciones de configuración (durable, auto_delete, exclusive, arguments).
69
- # @return [Bunny::Queue] La instancia de la cola.
80
+ # @param name [String] Nombre de la cola.
81
+ # @param opts [Hash] Opciones específicas de infraestructura para esta cola.
82
+ # @return [Bunny::Queue] El objeto cola de Bunny configurado.
70
83
  def queue(name, opts = {})
71
- channel.queue(name.to_s, DEFAULT_QUEUE_OPTIONS.merge(opts))
84
+ # Aplicación de la lógica de fusión en cascada
85
+ merged_opts = DEFAULT_QUEUE_OPTIONS
86
+ .merge(BugBunny.configuration.queue_options || {})
87
+ .merge(opts)
88
+
89
+ channel.queue(name.to_s, merged_opts)
72
90
  end
73
91
 
74
- # Cierra el canal asociado a esta sesión.
75
- # No cierra la conexión TCP (ya que esta pertenece al Pool), solo libera el canal virtual.
76
- #
92
+ # Cierra el canal asociado a esta sesión de forma segura.
77
93
  # @return [void]
78
94
  def close
79
- @channel.close if @channel&.open?
95
+ @channel&.close if @channel&.open?
96
+ @channel = nil
97
+ end
98
+
99
+ private
100
+
101
+ # Crea y configura un nuevo canal con las preferencias globales.
102
+ # Asume que la conexión ya ha sido verificada por `ensure_connection!`.
103
+ #
104
+ # @raise [BugBunny::CommunicationError] Si falla la creación del canal.
105
+ def create_channel!
106
+ @channel = @connection.create_channel
107
+
108
+ # Configuraciones globales de BugBunny
109
+ @channel.confirm_select
110
+
111
+ if BugBunny.configuration.channel_prefetch
112
+ @channel.prefetch(BugBunny.configuration.channel_prefetch)
113
+ end
114
+ rescue StandardError => e
115
+ raise BugBunny::CommunicationError, "Failed to create channel: #{e.message}"
116
+ end
117
+
118
+ # Garantiza que la conexión TCP esté abierta.
119
+ # Si está cerrada, intenta reconectarla (Reconexión Transparente).
120
+ #
121
+ # @raise [BugBunny::CommunicationError] Si falla la reconexión.
122
+ def ensure_connection!
123
+ return if @connection.open?
124
+
125
+ BugBunny.configuration.logger.warn("[BugBunny::Session] ⚠️ Connection lost. Attempting to reconnect...")
126
+ @connection.start
127
+ rescue StandardError => e
128
+ BugBunny.configuration.logger.error("[BugBunny::Session] ❌ Critical connection failure: #{e.message}")
129
+ raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
80
130
  end
81
131
  end
82
132
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "3.0.6"
4
+ VERSION = "3.1.1"
5
5
  end
data/lib/bug_bunny.rb CHANGED
@@ -75,7 +75,7 @@ module BugBunny
75
75
 
76
76
  @global_connection.close if @global_connection.open?
77
77
  @global_connection = nil
78
- configuration.logger.info('[BugBunny] Global connection closed.')
78
+ configuration.logger.info('[BugBunny] 🔌 Global connection closed.')
79
79
  end
80
80
 
81
81
  # @api private
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+
5
+ # --- CLASES DE PRUEBA (Namespace Aislado) ---
6
+ module InfraTest
7
+ class PingController < BugBunny::Controller
8
+ # Agregamos SHOW para soportar el .find del test
9
+ def show
10
+ render status: 200, json: { id: params[:id], message: 'pong', namespace: 'InfraTest' }
11
+ end
12
+
13
+ def index
14
+ render status: 200, json: { message: 'pong_index', namespace: 'InfraTest' }
15
+ end
16
+ end
17
+ end
18
+
19
+ class InfraResource < BugBunny::Resource
20
+ self.resource_name = 'ping'
21
+ self.exchange = 'test_infra_exchange'
22
+ self.exchange_type = 'topic'
23
+ end
24
+
25
+ # --- SUITE DE INFRAESTRUCTURA ---
26
+ class InfrastructureTest < Minitest::Test
27
+ include IntegrationHelper
28
+
29
+ def setup
30
+ skip "RabbitMQ no disponible" unless IntegrationHelper.rabbitmq_available?
31
+ @queue = "test_infra_queue_#{SecureRandom.hex(4)}"
32
+ @exchange = "test_infra_exchange"
33
+ end
34
+
35
+ def test_00_worker_lifecycle
36
+ with_running_worker(queue: @queue, exchange: @exchange) do
37
+ assert true, "El worker levantó y cedió el control al bloque"
38
+ end
39
+ end
40
+
41
+ def test_01_dynamic_namespace_resolution
42
+ BugBunny.configure do |c|
43
+ c.controller_namespace = 'InfraTest'
44
+ end
45
+
46
+ with_running_worker(queue: @queue, exchange: @exchange) do
47
+ # Enviamos GET ping/123 -> InfraTest::PingController#show
48
+ resource = InfraResource.find('123')
49
+
50
+ # Verificamos que volvió el objeto construido
51
+ assert_equal '123', resource.id
52
+ assert_equal 'InfraTest', resource.namespace
53
+ assert_equal 'pong', resource.message
54
+ end
55
+
56
+ ensure
57
+ BugBunny.configure do |c|
58
+ c.controller_namespace = 'Rabbit::Controllers'
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_helper'
4
+ require 'timeout'
5
+
6
+ module ManualTest
7
+ class EchoController < BugBunny::Controller
8
+ def index
9
+ render status: 200, json: {
10
+ received: params[:message],
11
+ type: headers[:type],
12
+ via: 'ManualTest::EchoController'
13
+ }
14
+ end
15
+ end
16
+ end
17
+
18
+ class ManualClientTest < Minitest::Test
19
+ include IntegrationHelper
20
+
21
+ def setup
22
+ skip "RabbitMQ no disponible" unless IntegrationHelper.rabbitmq_available?
23
+
24
+ # 1. NOMBRES ÚNICOS: Evitamos colisiones entre tests de diferentes tipos (Direct/Fanout)
25
+ @queue = "test_manual_q_#{SecureRandom.hex(4)}"
26
+ @exchange = "test_manual_x_#{SecureRandom.hex(4)}"
27
+
28
+ @client = BugBunny::Client.new(pool: TEST_POOL)
29
+ BugBunny.configure { |c| c.controller_namespace = 'ManualTest' }
30
+ end
31
+
32
+ def teardown
33
+ BugBunny.configure { |c| c.controller_namespace = 'Rabbit::Controllers' }
34
+ end
35
+
36
+ # ==========================================
37
+ # GRUPO 1: PUBLICACIÓN ASÍNCRONA (PUBLISH)
38
+ # ==========================================
39
+
40
+ def test_publish_topic
41
+ puts "\n -> [Manual] Publish (Topic)..."
42
+ with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'topic', routing_key: 'logs.#') do |messages|
43
+
44
+ @client.publish('logs.error', exchange: @exchange, exchange_type: 'topic', body: { a: 1 })
45
+
46
+ msg = wait_for_message(messages)
47
+ assert_equal 'logs.error', msg[:routing_key]
48
+ end
49
+ end
50
+
51
+ def test_publish_direct
52
+ puts " -> [Manual] Publish (Direct)..."
53
+ with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'direct', routing_key: 'alert') do |messages|
54
+
55
+ @client.publish('alert', exchange: @exchange, exchange_type: 'direct', body: { a: 1 })
56
+
57
+ msg = wait_for_message(messages)
58
+ assert_equal 'alert', msg[:routing_key]
59
+ end
60
+ end
61
+
62
+ def test_publish_fanout
63
+ puts " -> [Manual] Publish (Fanout)..."
64
+ with_spy_worker(queue: @queue, exchange: @exchange, exchange_type: 'fanout', routing_key: '') do |messages|
65
+
66
+ @client.publish('ignored.key', exchange: @exchange, exchange_type: 'fanout', body: { a: 1 })
67
+
68
+ msg = wait_for_message(messages)
69
+ assert_equal 'ignored.key', msg[:routing_key]
70
+ end
71
+ end
72
+
73
+ # ==========================================
74
+ # GRUPO 2: RPC SÍNCRONO (REQUEST)
75
+ # ==========================================
76
+
77
+ def test_request_topic
78
+ puts " -> [Manual] RPC (Topic)..."
79
+ with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'topic', routing_key: 'echo') do
80
+
81
+ response = @client.request('echo',
82
+ method: :get, exchange: @exchange, exchange_type: 'topic',
83
+ body: { message: 'topic_rpc' }
84
+ )
85
+ assert_equal 'topic_rpc', response['body']['received']
86
+ end
87
+ end
88
+
89
+ def test_request_direct
90
+ puts " -> [Manual] RPC (Direct)..."
91
+ direct_key = 'rpc.direct'
92
+
93
+ with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'direct', routing_key: direct_key) do
94
+
95
+ response = @client.request('echo',
96
+ method: :get,
97
+ routing_key: direct_key,
98
+ exchange: @exchange,
99
+ exchange_type: 'direct',
100
+ body: { message: 'direct_rpc' }
101
+ )
102
+
103
+ assert_equal 200, response['status']
104
+ assert_equal 'direct_rpc', response['body']['received']
105
+ end
106
+ end
107
+
108
+ def test_request_fanout
109
+ puts " -> [Manual] RPC (Fanout)..."
110
+ with_running_worker(queue: @queue, exchange: @exchange, exchange_type: 'fanout', routing_key: '') do
111
+
112
+ response = @client.request('echo',
113
+ method: :get,
114
+ routing_key: 'random.ignored',
115
+ exchange: @exchange,
116
+ exchange_type: 'fanout',
117
+ body: { message: 'fanout_rpc' }
118
+ )
119
+
120
+ assert_equal 200, response['status']
121
+ assert_equal 'fanout_rpc', response['body']['received']
122
+ end
123
+ end
124
+
125
+ # ==========================================
126
+ # GRUPO 3: OPCIONES DE INFRAESTRUCTURA (CASCADA NIVEL 3)
127
+ # ==========================================
128
+
129
+ def test_publish_with_custom_exchange_options
130
+ puts " -> [Manual] Publish (Custom Options Nivel 3)..."
131
+ custom_exchange = "custom_opts_x_#{SecureRandom.hex(4)}"
132
+
133
+ # 1. Pre-creamos el exchange exigiendo que sea DURABLE (contrario a la config global)
134
+ conn = BugBunny.create_connection
135
+ ch = conn.create_channel
136
+ ch.topic(custom_exchange, durable: true, auto_delete: true)
137
+
138
+ begin
139
+ # 2. El cliente publica inyectando opciones dinámicas.
140
+ # Si esto no funcionara, RabbitMQ nos tiraría PRECONDITION_FAILED
141
+ # porque la configuración global (Nivel 2) dice durable: false.
142
+ @client.publish('logs',
143
+ exchange: custom_exchange,
144
+ exchange_type: 'topic',
145
+ exchange_options: { durable: true, auto_delete: true }, # Nivel 3 sobrescribe Nivel 2
146
+ body: { test: 'options' }
147
+ )
148
+
149
+ # Si la ejecución llega aquí, significa que la Cascada funcionó perfecto.
150
+ assert true
151
+ ensure
152
+ ch&.exchange_delete(custom_exchange) rescue nil
153
+ conn&.close
154
+ end
155
+ end
156
+
157
+ def test_request_with_custom_exchange_options
158
+ puts " -> [Manual] RPC (Custom Options Nivel 3)..."
159
+ custom_exchange = "custom_opts_rpc_x_#{SecureRandom.hex(4)}"
160
+
161
+ # 1. Exigimos DURABLE
162
+ conn = BugBunny.create_connection
163
+ ch = conn.create_channel
164
+ ch.direct(custom_exchange, durable: true, auto_delete: true)
165
+
166
+ # 2. Levantamos un worker usando esas configuraciones nativas
167
+ worker_thread = Thread.new do
168
+ q = ch.queue('', exclusive: true)
169
+ q.bind(custom_exchange, routing_key: 'custom.rpc')
170
+ q.subscribe(block: true) do |delivery, props, _body|
171
+ # Respuesta manual
172
+ ch.default_exchange.publish('{"status":200, "body":"ok"}', routing_key: props.reply_to, correlation_id: props.correlation_id)
173
+ end
174
+ end
175
+ sleep 0.5
176
+
177
+ begin
178
+ # 3. El cliente hace el request inyectando las opciones
179
+ response = @client.request('test',
180
+ exchange: custom_exchange,
181
+ exchange_type: 'direct',
182
+ routing_key: 'custom.rpc',
183
+ exchange_options: { durable: true, auto_delete: true }, # Nivel 3
184
+ body: { req: 'data' }
185
+ )
186
+
187
+ assert_equal 200, response['status']
188
+ assert_equal 'ok', response['body']
189
+ ensure
190
+ worker_thread&.kill
191
+ ch&.exchange_delete(custom_exchange) rescue nil
192
+ conn&.close
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ def wait_for_message(queue, timeout_sec = 2)
199
+ Timeout.timeout(timeout_sec) { queue.pop }
200
+ rescue Timeout::Error
201
+ flunk "Timeout: No llegó el mensaje al Worker en #{timeout_sec}s"
202
+ end
203
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'minitest/autorun'
5
+ # require 'minitest/reporters'
6
+ require 'bug_bunny'
7
+ require 'connection_pool'
8
+ require 'securerandom'
9
+ require 'socket'
10
+
11
+ BugBunny.configure do |config|
12
+ config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
13
+ config.username = ENV.fetch('RABBITMQ_USER', 'guest')
14
+ config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
15
+ config.vhost = '/'
16
+ config.logger = Logger.new($stdout)
17
+ config.logger.level = Logger::WARN
18
+
19
+ # ========================================================
20
+ # LA MAGIA DE LA CASCADA (Nivel 2: Configuración Global)
21
+ # ========================================================
22
+ # Para los tests, queremos que los exchanges y queues sean efímeros
23
+ config.exchange_options = { durable: false, auto_delete: true }
24
+ config.queue_options = { exclusive: false, durable: false, auto_delete: true }
25
+ end
26
+
27
+ TEST_POOL = ConnectionPool.new(size: 5, timeout: 5) { BugBunny.create_connection }
28
+ BugBunny::Resource.connection_pool = TEST_POOL
29
+
30
+ module IntegrationHelper
31
+ def self.rabbitmq_available?
32
+ socket = TCPSocket.new(BugBunny.configuration.host, 5672)
33
+ socket.close
34
+ true
35
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
36
+ false
37
+ end
38
+
39
+ def with_running_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
40
+ conn = BugBunny.create_connection
41
+
42
+ worker_thread = Thread.new do
43
+ ch = conn.create_channel
44
+
45
+ x_opts = BugBunny.configuration.exchange_options || {}
46
+ q_opts = BugBunny.configuration.queue_options || {}
47
+
48
+ # FIX: Usamos la API de alto nivel (public_send) idéntica a BugBunny::Session
49
+ ch.public_send(exchange_type, exchange, x_opts)
50
+ ch.close
51
+
52
+ BugBunny::Consumer.subscribe(
53
+ connection: conn,
54
+ queue_name: queue,
55
+ exchange_name: exchange,
56
+ exchange_type: exchange_type,
57
+ routing_key: routing_key,
58
+ queue_opts: q_opts,
59
+ block: true
60
+ )
61
+ rescue => e
62
+ puts "❌ WORKER CRASHED: #{e.message}"
63
+ puts e.backtrace.join("\n")
64
+ end
65
+
66
+ sleep 0.5
67
+ yield
68
+ ensure
69
+ conn&.close
70
+ worker_thread&.kill
71
+ sleep 0.1
72
+ end
73
+
74
+ def with_spy_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
75
+ captured_messages = Thread::Queue.new
76
+ conn = BugBunny.create_connection
77
+
78
+ worker_thread = Thread.new do
79
+ ch = conn.create_channel
80
+
81
+ x_opts = BugBunny.configuration.exchange_options || {}
82
+ q_opts = BugBunny.configuration.queue_options || {}
83
+
84
+ # FIX DEFINITIVO: Ahora 'x' es un hermoso objeto Bunny::Exchange
85
+ # que la función .bind() entiende perfectamente.
86
+ x = ch.public_send(exchange_type, exchange, x_opts)
87
+ q = ch.queue(queue, q_opts)
88
+
89
+ # Bindeamos el objeto al queue
90
+ q.bind(x, routing_key: routing_key)
91
+
92
+ q.subscribe(block: true) do |delivery, props, body|
93
+ captured_messages << {
94
+ body: body,
95
+ routing_key: delivery.routing_key,
96
+ headers: props.headers
97
+ }
98
+ end
99
+ rescue => e
100
+ puts "SPY WORKER ERROR: #{e.message}"
101
+ end
102
+
103
+ sleep 0.5
104
+ yield(captured_messages)
105
+ ensure
106
+ conn&.close
107
+ worker_thread&.kill
108
+ end
109
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.6
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-17 00:00:00.000000000 Z
11
+ date: 2026-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -178,6 +178,48 @@ dependencies:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0.9'
181
+ - !ruby/object:Gem::Dependency
182
+ name: minitest
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '5.0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '5.0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: mocha
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '2.0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '2.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: minitest-reporters
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '1.6'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '1.6'
181
223
  description: BugBunny is a lightweight RPC framework for Ruby on Rails over RabbitMQ.
182
224
  It simulates a RESTful architecture with an intelligent router, Active Record-like
183
225
  resources, and middleware support.
@@ -190,9 +232,6 @@ files:
190
232
  - CHANGELOG.md
191
233
  - README.md
192
234
  - Rakefile
193
- - bin_client.rb
194
- - bin_suite.rb
195
- - bin_worker.rb
196
235
  - initializer_example.rb
197
236
  - lib/bug_bunny.rb
198
237
  - lib/bug_bunny/client.rb
@@ -213,9 +252,9 @@ files:
213
252
  - lib/generators/bug_bunny/install/install_generator.rb
214
253
  - lib/generators/bug_bunny/install/templates/initializer.rb
215
254
  - sig/bug_bunny.rbs
216
- - test_controller.rb
217
- - test_helper.rb
218
- - test_resource.rb
255
+ - test/integration/infrastructure_test.rb
256
+ - test/integration/manual_client_test.rb
257
+ - test/test_helper.rb
219
258
  homepage: https://github.com/gedera/bug_bunny
220
259
  licenses:
221
260
  - MIT