pgbus 0.7.2 → 0.7.4

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.
@@ -66,6 +66,12 @@ pt:
66
66
  index:
67
67
  discard_all: Descartar Tudo
68
68
  discard_all_confirm: Descartar permanentemente todas as mensagens DLQ?
69
+ discard_selected: Descartar Selecionados
70
+ discard_selected_confirm: Descartar mensagens DLQ selecionadas?
71
+ discarded_selected:
72
+ one: 1 mensagem DLQ descartada.
73
+ other: "%{count} mensagens DLQ descartadas."
74
+ none_selected: Nenhuma mensagem selecionada.
69
75
  retry_all: Tentar Novamente Tudo
70
76
  retry_all_confirm: Tentar novamente todas as mensagens DLQ?
71
77
  title: Fila de Mensagens Mortas
@@ -113,7 +119,34 @@ pt:
113
119
  delete_title: Excluir
114
120
  ok: OK
115
121
  events:
122
+ flash:
123
+ discard_failed: Não foi possível descartar o evento.
124
+ discarded: Evento descartado.
125
+ discarded_selected:
126
+ one: 1 evento descartado.
127
+ other: "%{count} eventos descartados."
128
+ mark_handled_failed: Não foi possível marcar o evento como tratado.
129
+ marked_handled: Evento marcado como tratado.
130
+ none_selected: Nenhum evento selecionado.
131
+ payload_update_failed: Não foi possível atualizar o payload. Verifique se o JSON é válido.
132
+ payload_updated: Payload atualizado e evento reenfileirado.
133
+ replay_failed: Evento não encontrado ou não pôde ser reproduzido.
134
+ replayed: Evento reproduzido.
135
+ reroute_failed: Não foi possível redirecionar o evento.
136
+ rerouted: Evento redirecionado para o manipulador alvo.
116
137
  index:
138
+ discard_all: Descartar Todos
139
+ discard_all_confirm: Descartar todos os eventos pendentes? Esta ação não pode ser desfeita.
140
+ discard_selected: Descartar Selecionados
141
+ discard_selected_confirm: Descartar eventos selecionados?
142
+ pending_empty: Nenhum evento pendente
143
+ pending_headers:
144
+ enqueued: Enfileirado
145
+ handler_queue: Fila do Manipulador
146
+ id: ID
147
+ reads: Leituras
148
+ routing_key: Chave de Roteamento
149
+ pending_title: Eventos Pendentes
117
150
  processed_empty: Nenhum evento processado ainda
118
151
  processed_headers:
119
152
  event_id: ID do evento
@@ -127,6 +160,26 @@ pt:
127
160
  queue: Fila
128
161
  subscribers_title: Assinantes registrados
129
162
  title: Eventos
163
+ pending_table:
164
+ arguments: Payload
165
+ discard: Descartar
166
+ discard_confirm: Descartar este evento? Ele será arquivado.
167
+ edit_payload: Editar e Tentar Novamente
168
+ edit_payload_confirm: Atualizar payload e reenfileirar este evento?
169
+ edit_payload_label: 'Payload JSON:'
170
+ event_id: 'ID do Evento:'
171
+ full_json_payload: Payload JSON Completo
172
+ headers_section: Cabeçalhos
173
+ mark_handled: Marcar como Tratado
174
+ mark_handled_confirm: Marcar este evento como tratado? Ele será arquivado e ignorado na reprodução.
175
+ metadata: Metadados
176
+ metadata_labels:
177
+ last_read: 'Última leitura:'
178
+ read_count: 'Contagem de leituras:'
179
+ visible_at: 'Visível em:'
180
+ reroute: Redirecionar
181
+ reroute_confirm: Redirecionar este evento para um manipulador diferente?
182
+ reroute_label: 'Manipulador alvo:'
130
183
  show:
131
184
  back: Voltar para eventos
132
185
  labels:
@@ -136,6 +189,9 @@ pt:
136
189
  not_found: Evento não encontrado
137
190
  title: Evento %{event_id}
138
191
  helpers:
192
+ bulk_select_all: Selecionar todos
193
+ bulk_select_row: Selecionar %{id}
194
+ bulk_selected: selecionado
139
195
  paused_badge: Pausado
140
196
  queue_badge:
141
197
  dlq: DLQ
@@ -150,11 +206,22 @@ pt:
150
206
  show:
151
207
  charts:
152
208
  failed_to_load: Falha ao carregar dados do gráfico
209
+ latency: Latência da Fila (ms)
210
+ latency_avg: Média
211
+ latency_p95: P95
153
212
  no_data: Sem dados ainda
154
213
  series_name: Tarefas/min
155
214
  status_distribution: Distribuição de status
156
215
  throughput: Taxa de transferência (tarefas/min)
157
216
  description_html: Métricas de desempenho de trabalho para os últimos %{range}
217
+ latency_by_queue:
218
+ empty: Ainda não há dados de latência
219
+ headers:
220
+ avg: Média (ms)
221
+ count: Contagem
222
+ p95: P95 (ms)
223
+ queue: Fila
224
+ title: Latência por Fila
158
225
  slowest:
159
226
  empty: Nenhuma estatística de tarefa ainda
160
227
  headers:
@@ -163,11 +230,32 @@ pt:
163
230
  job_class: Classe da tarefa
164
231
  max: Máximo
165
232
  title: Classes de Trabalho Mais Lentas (duração média)
233
+ streams:
234
+ summary:
235
+ active: Ativo
236
+ avg_fanout: Média de Ramificações
237
+ broadcasts: Transmissões
238
+ connects: Conexões
239
+ disconnects: Desconexões
240
+ title: Streams em Tempo Real
241
+ top:
242
+ empty: Nenhuma atividade de stream registrada na janela selecionada
243
+ headers:
244
+ avg_fanout: Média de Ramificações
245
+ avg_ms: Média de Despacho
246
+ broadcasts: Transmissões
247
+ stream: Stream
248
+ title: Principais Streams por Volume de Transmissão
166
249
  summary:
167
250
  avg_duration: Duração Média
251
+ avg_latency: Latência Média
252
+ avg_retries: Média de Tentativas
168
253
  dead_lettered: Cartas Mortas
169
254
  failed: Falhou
170
255
  max_duration: Duração Máxima
256
+ p50_latency: Latência P50
257
+ p95_latency: Latência P95
258
+ p99_latency: Latência P99
171
259
  succeeded: Bem-sucedido
172
260
  total_jobs: Total de Trabalhos
173
261
  time_ranges:
@@ -222,6 +310,12 @@ pt:
222
310
  discard_all: Descartar Todos
223
311
  discard_all_confirm: Descartar todos os trabalhos falhados?
224
312
  discard_all_enqueued_notice: Descartados %{count} trabalhos na fila e bloqueios liberados.
313
+ discard_selected: Descartar Selecionados
314
+ discard_selected_confirm: Descartar itens selecionados?
315
+ discarded_selected:
316
+ one: 1 item selecionado descartado.
317
+ other: "%{count} itens selecionados descartados."
318
+ none_selected: Nenhum item selecionado.
225
319
  retry_all: Tentar Novamente Todos
226
320
  retry_all_confirm: Tentar novamente todos os trabalhos falhados?
227
321
  title: Trabalhos
@@ -259,13 +353,28 @@ pt:
259
353
  toggle_menu: Alternar menu
260
354
  locks:
261
355
  index:
356
+ all_locks_discarded:
357
+ one: 1 bloqueio descartado.
358
+ other: "%{count} bloqueios descartados."
262
359
  description: Bloqueios de exclusividade ativos impedindo a execução duplicada do trabalho
360
+ discard: Descartar
361
+ discard_all: Descartar Todos
362
+ discard_all_confirm: Descartar permanentemente todos os bloqueios? Isso pode permitir a execução duplicada de trabalhos.
363
+ discard_confirm: Descartar este bloqueio? O trabalho associado pode ser enfileirado novamente.
364
+ discard_selected: Descartar Selecionados
365
+ discard_selected_confirm: Descartar bloqueios selecionados?
263
366
  empty: Nenhum bloqueio ativo
264
367
  headers:
265
368
  age: Idade
266
369
  lock_key: Chave de Bloqueio
267
370
  msg_id: ID da mensagem
268
371
  queue_name: Fila
372
+ lock_discard_failed: Não foi possível descartar o bloqueio.
373
+ lock_discarded: Bloqueio descartado.
374
+ locks_discarded:
375
+ one: 1 bloqueio descartado.
376
+ other: "%{count} bloqueios descartados."
377
+ none_selected: Nenhum bloqueio selecionado.
269
378
  title: Chaves de unicidade
270
379
  outbox:
271
380
  index:
@@ -66,6 +66,12 @@ sv:
66
66
  index:
67
67
  discard_all: Kassera alla
68
68
  discard_all_confirm: Kassera permanent alla DLQ-meddelanden?
69
+ discard_selected: Kassera valda
70
+ discard_selected_confirm: Kassera valda DLQ-meddelanden?
71
+ discarded_selected:
72
+ one: Kasserade 1 DLQ-meddelande.
73
+ other: Kasserade %{count} DLQ-meddelanden.
74
+ none_selected: Inga meddelanden valda.
69
75
  retry_all: Försök igen alla
70
76
  retry_all_confirm: Försök igen alla DLQ-meddelanden?
71
77
  title: Dead Letter-kö
@@ -113,7 +119,34 @@ sv:
113
119
  delete_title: Ta bort
114
120
  ok: OK
115
121
  events:
122
+ flash:
123
+ discard_failed: Kunde inte kassera händelsen.
124
+ discarded: Händelse kasserad.
125
+ discarded_selected:
126
+ one: Kasserade 1 händelse.
127
+ other: Kasserade %{count} händelser.
128
+ mark_handled_failed: Kunde inte markera händelsen som hanterad.
129
+ marked_handled: Händelse markerad som hanterad.
130
+ none_selected: Inga händelser valda.
131
+ payload_update_failed: Kunde inte uppdatera payload. Kontrollera att JSON är giltig.
132
+ payload_updated: Payload uppdaterad och händelsen återköad.
133
+ replay_failed: Händelsen hittades inte eller kunde inte spelas upp.
134
+ replayed: Händelsen spelades upp.
135
+ reroute_failed: Kunde inte omdirigera händelsen.
136
+ rerouted: Händelsen omdirigerades till målhanteraren.
116
137
  index:
138
+ discard_all: Kassera alla
139
+ discard_all_confirm: Kassera alla väntande händelser? Detta kan inte ångras.
140
+ discard_selected: Kassera valda
141
+ discard_selected_confirm: Kassera valda händelser?
142
+ pending_empty: Inga väntande händelser
143
+ pending_headers:
144
+ enqueued: Köad
145
+ handler_queue: Handler-kö
146
+ id: ID
147
+ reads: Läsningar
148
+ routing_key: Routing-nyckel
149
+ pending_title: Väntande händelser
117
150
  processed_empty: Inga händelser behandlade än
118
151
  processed_headers:
119
152
  event_id: Händelse-ID
@@ -127,6 +160,26 @@ sv:
127
160
  queue: Kö
128
161
  subscribers_title: Registrerade prenumeranter
129
162
  title: Händelser
163
+ pending_table:
164
+ arguments: Payload
165
+ discard: Kassera
166
+ discard_confirm: Kassera denna händelse? Den kommer att arkiveras.
167
+ edit_payload: Redigera & Försök igen
168
+ edit_payload_confirm: Uppdatera payload och återköa denna händelse?
169
+ edit_payload_label: 'JSON Payload:'
170
+ event_id: 'Händelse-ID:'
171
+ full_json_payload: Fullständig JSON Payload
172
+ headers_section: Headers
173
+ mark_handled: Markera som hanterad
174
+ mark_handled_confirm: Markera denna händelse som hanterad? Den kommer att arkiveras och hoppas över vid uppspelning.
175
+ metadata: Metadata
176
+ metadata_labels:
177
+ last_read: 'Senast läst:'
178
+ read_count: 'Läsantal:'
179
+ visible_at: 'Synlig vid:'
180
+ reroute: Omdirigera
181
+ reroute_confirm: Omdirigera denna händelse till en annan hanterare?
182
+ reroute_label: 'Målhanterare:'
130
183
  show:
131
184
  back: Tillbaka till händelser
132
185
  labels:
@@ -136,6 +189,9 @@ sv:
136
189
  not_found: Händelse hittades inte
137
190
  title: Händelse %{event_id}
138
191
  helpers:
192
+ bulk_select_all: Markera alla
193
+ bulk_select_row: Markera %{id}
194
+ bulk_selected: valda
139
195
  paused_badge: Pausad
140
196
  queue_badge:
141
197
  dlq: DLQ
@@ -150,11 +206,22 @@ sv:
150
206
  show:
151
207
  charts:
152
208
  failed_to_load: Misslyckades att ladda diagramdata
209
+ latency: Köfördröjning (ms)
210
+ latency_avg: Genomsnitt
211
+ latency_p95: P95
153
212
  no_data: Inga data än
154
213
  series_name: Jobb/min
155
214
  status_distribution: Statusfördelning
156
215
  throughput: Genomströmning (jobb/min)
157
216
  description_html: Jobbprestandamått för de senaste %{range}
217
+ latency_by_queue:
218
+ empty: Ingen fördröjningsdata än
219
+ headers:
220
+ avg: Genomsnitt (ms)
221
+ count: Antal
222
+ p95: P95 (ms)
223
+ queue: Kö
224
+ title: Fördröjning per kö
158
225
  slowest:
159
226
  empty: Inga jobbstatistik än
160
227
  headers:
@@ -163,11 +230,32 @@ sv:
163
230
  job_class: Jobbklass
164
231
  max: Max
165
232
  title: Långsammaste jobbklasser (genomsnittlig varaktighet)
233
+ streams:
234
+ summary:
235
+ active: Aktiv
236
+ avg_fanout: Genomsnittlig spridning
237
+ broadcasts: Sändningar
238
+ connects: Anslutningar
239
+ disconnects: Frånkopplingar
240
+ title: Strömmar i realtid
241
+ top:
242
+ empty: Ingen strömaktivitet registrerad i det valda fönstret
243
+ headers:
244
+ avg_fanout: Genomsnittlig spridning
245
+ avg_ms: Genomsnittlig utsändning
246
+ broadcasts: Sändningar
247
+ stream: Ström
248
+ title: Toppströmmar efter sändningsvolym
166
249
  summary:
167
250
  avg_duration: Genomsnittlig varaktighet
251
+ avg_latency: Genomsnittlig fördröjning
252
+ avg_retries: Genomsnittliga omförsök
168
253
  dead_lettered: Dead Lettered
169
254
  failed: Misslyckades
170
255
  max_duration: Maximal varaktighet
256
+ p50_latency: P50-fördröjning
257
+ p95_latency: P95-fördröjning
258
+ p99_latency: P99-fördröjning
171
259
  succeeded: Lyckades
172
260
  total_jobs: Totalt antal jobb
173
261
  time_ranges:
@@ -222,6 +310,12 @@ sv:
222
310
  discard_all: Kassera alla
223
311
  discard_all_confirm: Kassera alla misslyckade jobb?
224
312
  discard_all_enqueued_notice: Kasserade %{count} köade jobb och frigjorde deras lås.
313
+ discard_selected: Kassera valda
314
+ discard_selected_confirm: Kassera valda objekt?
315
+ discarded_selected:
316
+ one: Kasserade 1 valt objekt.
317
+ other: Kasserade %{count} valda objekt.
318
+ none_selected: Inga objekt valda.
225
319
  retry_all: Försök igen alla
226
320
  retry_all_confirm: Försök igen alla misslyckade jobb?
227
321
  title: Jobb
@@ -259,13 +353,28 @@ sv:
259
353
  toggle_menu: Växla meny
260
354
  locks:
261
355
  index:
356
+ all_locks_discarded:
357
+ one: Kasserade 1 lås.
358
+ other: Kasserade %{count} lås.
262
359
  description: Aktiva unika lås som förhindrar duplicerad jobbexekvering
360
+ discard: Kassera
361
+ discard_all: Kassera alla
362
+ discard_all_confirm: Kassera alla lås permanent? Detta kan tillåta duplicerad jobbexekvering.
363
+ discard_confirm: Kassera detta lås? Det associerade jobbet kan köas igen.
364
+ discard_selected: Kassera valda
365
+ discard_selected_confirm: Kassera valda lås?
263
366
  empty: Inga aktiva lås
264
367
  headers:
265
368
  age: Ålder
266
369
  lock_key: Låsningsnyckel
267
370
  msg_id: Meddelande-ID
268
371
  queue_name: Kö
372
+ lock_discard_failed: Kunde inte kassera lås.
373
+ lock_discarded: Lås kasserat.
374
+ locks_discarded:
375
+ one: Kasserade 1 lås.
376
+ other: Kasserade %{count} lås.
377
+ none_selected: Inga lås valda.
269
378
  title: Unikhetsnycklar
270
379
  outbox:
271
380
  index:
data/config/routes.rb CHANGED
@@ -50,6 +50,13 @@ Pgbus::Engine.routes.draw do
50
50
  resources :events, only: %i[index show] do
51
51
  member do
52
52
  post :replay
53
+ post :discard
54
+ post :mark_handled
55
+ post :edit_payload
56
+ post :reroute
57
+ end
58
+ collection do
59
+ post :discard_selected
53
60
  end
54
61
  end
55
62
 
@@ -103,7 +103,7 @@ module Pgbus
103
103
  :streams_default_retention, :streams_retention, :streams_heartbeat_interval,
104
104
  :streams_max_connections, :streams_idle_timeout, :streams_listen_health_check_ms,
105
105
  :streams_write_deadline_ms, :streams_falcon_streaming_body,
106
- :streams_stats_enabled
106
+ :streams_stats_enabled, :streams_test_mode
107
107
 
108
108
  def initialize
109
109
  @database_url = nil
@@ -209,6 +209,7 @@ module Pgbus
209
209
  # gates pgbus_job_stats recording) on purpose — operators
210
210
  # usually want job stats on and stream stats off, or vice versa.
211
211
  @streams_stats_enabled = false
212
+ @streams_test_mode = false
212
213
  end
213
214
 
214
215
  def queue_name(name)
@@ -1,12 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+
3
5
  module Pgbus
4
6
  module EventBus
5
7
  module Publisher
6
8
  module_function
7
9
 
8
10
  def publish(routing_key, payload, headers: nil, delay: 0)
9
- event_data = build_event_data(payload)
11
+ event_data = build_event_data(payload, routing_key: routing_key)
12
+
13
+ if defined?(Pgbus::Testing) && !Pgbus::Testing.disabled?
14
+ event = Pgbus::Event.new(
15
+ event_id: event_data["event_id"],
16
+ payload: event_data["payload"],
17
+ published_at: event_data["published_at"] ? Time.parse(event_data["published_at"]) : nil,
18
+ routing_key: routing_key,
19
+ headers: headers
20
+ )
21
+
22
+ Pgbus::Testing.store.push_event(event)
23
+
24
+ if Pgbus::Testing.inline? && delay.to_i <= 0
25
+ Pgbus::EventBus::Registry.instance.handlers_for(routing_key).each do |subscriber|
26
+ subscriber.handler_class.new.handle(event)
27
+ end
28
+ end
29
+
30
+ return event_data
31
+ end
32
+
10
33
  Pgbus.client.publish_to_topic(routing_key, event_data, headers: headers, delay: delay)
11
34
  end
12
35
 
@@ -14,7 +37,7 @@ module Pgbus
14
37
  publish(routing_key, payload, headers: headers, delay: delay)
15
38
  end
16
39
 
17
- def build_event_data(payload)
40
+ def build_event_data(payload, routing_key: nil)
18
41
  event_id = SecureRandom.uuid
19
42
 
20
43
  serialized_payload = if payload.respond_to?(:to_global_id)
@@ -25,11 +48,13 @@ module Pgbus
25
48
  { "value" => payload }
26
49
  end
27
50
 
28
- {
51
+ data = {
29
52
  "event_id" => event_id,
30
53
  "payload" => serialized_payload,
31
54
  "published_at" => Time.now.utc.iso8601(6)
32
55
  }
56
+ data["routing_key"] = routing_key if routing_key
57
+ data
33
58
  end
34
59
  end
35
60
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ module Testing
5
+ # Framework-agnostic assertion helpers. Included by both RSpec and Minitest
6
+ # integrations. Can also be included directly in any test class.
7
+ #
8
+ # include Pgbus::Testing::Assertions
9
+ #
10
+ # assert_pgbus_published(count: 1, routing_key: "orders.created") do
11
+ # Order.create!(...)
12
+ # end
13
+ module Assertions
14
+ def pgbus_published_events(routing_key: nil)
15
+ Pgbus::Testing.store.events(routing_key: routing_key)
16
+ end
17
+
18
+ def assert_pgbus_published(count:, routing_key: nil)
19
+ before = pgbus_published_events(routing_key: routing_key).size
20
+ yield
21
+ after = pgbus_published_events(routing_key: routing_key).size
22
+ actual = after - before
23
+
24
+ return if actual == count
25
+
26
+ suffix = routing_key ? " matching #{routing_key.inspect}" : ""
27
+ raise_assertion("Expected #{count} event(s) published#{suffix}, got #{actual}")
28
+ end
29
+
30
+ def assert_no_pgbus_published(routing_key: nil)
31
+ before = pgbus_published_events(routing_key: routing_key).size
32
+ yield
33
+ after = pgbus_published_events(routing_key: routing_key).size
34
+ actual = after - before
35
+
36
+ return if actual.zero?
37
+
38
+ suffix = routing_key ? " matching #{routing_key.inspect}" : ""
39
+ raise_assertion("Expected no events published#{suffix}, got #{actual}")
40
+ end
41
+
42
+ # Execute the block, capturing events, then dispatch all captured events
43
+ # to their registered handlers.
44
+ def perform_published_events
45
+ yield
46
+ Pgbus::Testing.store.drain!
47
+ end
48
+
49
+ private
50
+
51
+ def raise_assertion(message)
52
+ # Use Minitest::Assertion if available, otherwise a generic RuntimeError
53
+ raise Minitest::Assertion, message if defined?(Minitest::Assertion)
54
+
55
+ raise message
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../testing"
4
+
5
+ module Pgbus
6
+ module Testing
7
+ # Minitest integration for Pgbus test helpers.
8
+ #
9
+ # Include in your test_helper.rb:
10
+ #
11
+ # require "pgbus/testing/minitest"
12
+ #
13
+ # class ActiveSupport::TestCase
14
+ # include Pgbus::Testing::MinitestHelpers
15
+ # end
16
+ #
17
+ # This provides:
18
+ # - Automatic fake mode + store clearing per test
19
+ # - assert_pgbus_published / assert_no_pgbus_published
20
+ # - perform_published_events
21
+ # - pgbus_published_events
22
+ module MinitestHelpers
23
+ include Pgbus::Testing::Assertions
24
+
25
+ def before_setup
26
+ Pgbus::Testing.fake!
27
+ Pgbus::Testing.store.clear!
28
+ super
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../testing"
4
+
5
+ RSpec::Matchers.define :have_published_event do |expected_routing_key|
6
+ supports_block_expectations
7
+
8
+ chain(:with_payload) { |payload| @expected_payload = payload }
9
+ chain(:with_headers) { |headers| @expected_headers = headers }
10
+ chain(:exactly) { |count| @expected_count = count }
11
+
12
+ match do |block|
13
+ @before = Pgbus::Testing.store.events(routing_key: expected_routing_key).dup
14
+ block.call
15
+ @after = Pgbus::Testing.store.events(routing_key: expected_routing_key)
16
+ @new_events = @after - @before
17
+
18
+ return false if @new_events.empty?
19
+
20
+ if @expected_payload
21
+ @new_events = @new_events.select { |e| values_match?(@expected_payload, e.payload) }
22
+ return false if @new_events.empty?
23
+ end
24
+
25
+ if @expected_headers
26
+ @new_events = @new_events.select { |e| values_match?(@expected_headers, e.headers) }
27
+ return false if @new_events.empty?
28
+ end
29
+
30
+ return false if @expected_count && @new_events.size != @expected_count
31
+
32
+ true
33
+ end
34
+
35
+ match_when_negated do |block|
36
+ @before = Pgbus::Testing.store.events(routing_key: expected_routing_key).dup
37
+ block.call
38
+ @after = Pgbus::Testing.store.events(routing_key: expected_routing_key)
39
+ @new_events = @after - @before
40
+
41
+ @new_events = @new_events.select { |e| values_match?(@expected_payload, e.payload) } if @expected_payload
42
+
43
+ @new_events = @new_events.select { |e| values_match?(@expected_headers, e.headers) } if @expected_headers
44
+
45
+ if @expected_count
46
+ @new_events.size != @expected_count
47
+ else
48
+ @new_events.empty?
49
+ end
50
+ end
51
+
52
+ failure_message do
53
+ parts = ["expected block to publish a #{expected_routing_key.inspect} event"]
54
+ parts << "with payload #{@expected_payload.inspect}" if @expected_payload
55
+ parts << "with headers #{@expected_headers.inspect}" if @expected_headers
56
+ parts << "exactly #{@expected_count} time(s)" if @expected_count
57
+
58
+ published = @new_events&.size || 0
59
+ parts << "but #{published} matching event(s) were published"
60
+
61
+ if published.positive? && @expected_payload
62
+ actual_payloads = @after.map(&:payload)
63
+ parts << "actual payloads: #{actual_payloads.inspect}"
64
+ end
65
+
66
+ parts.join(", ")
67
+ end
68
+
69
+ failure_message_when_negated do
70
+ "expected block not to publish a #{expected_routing_key.inspect} event, but #{@new_events.size} were published"
71
+ end
72
+ end
73
+
74
+ RSpec.configure do |config|
75
+ config.include Pgbus::Testing::Assertions
76
+ end