pgbus 0.7.2 → 0.7.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1a647a2e485cace147ca201861ff4613e9c14f72f8fd8be9060f64d6eccfff5
4
- data.tar.gz: bf3ca37850d549c6e9ed86bedbad1d88f9a50462b86a2d8a09113de7c1227d0f
3
+ metadata.gz: 8d07ecda9d90b73fa2ab8d9a34bf775b305bba9349dd5d49c99b587ea5b614bc
4
+ data.tar.gz: af072bf9e30ead0928db95896380384c49d8a49a59994864693aea80dd249bda
5
5
  SHA512:
6
- metadata.gz: b32dac214071005fa5974478f2cb8bd4d8a270efc3e6bc94141b74c28cebfea0dfa84e41ad9bf1ec2a10c8ab0d09ae08d0df989f0894b8d5cbc6a461698ecf5b
7
- data.tar.gz: ea55e1a29e53e491063c34aaea3a643498d1567968e64833d9a38deef2bcd7a48dfb35e377601d347fa8e05e55a5647a77dbe958d9584fcdc8669a362ff253a8
6
+ metadata.gz: 49eccf16ff1d0dcdec75f111bda474b343f3cf154b14569d19138f1b666457d71482037e390f4faa7136e21ead14b32a8a9408d18256843992e83476424a333c
7
+ data.tar.gz: 0d78309574c68a90b59b276b7648ac17cd8c74f3dbedebbd3d6cabe70a062a4d879cac19da8b2ab6ac78584f671ec7eda1db37fa459ecd761b81f0cd5c81ded0
@@ -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,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+
3
5
  module Pgbus
4
6
  module EventBus
5
7
  module Publisher
@@ -7,6 +9,27 @@ module Pgbus
7
9
 
8
10
  def publish(routing_key, payload, headers: nil, delay: 0)
9
11
  event_data = build_event_data(payload)
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
 
@@ -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
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pgbus"
4
+
5
+ module Pgbus
6
+ # Test helpers for Pgbus EventBus. Opt-in via explicit require — never
7
+ # autoloaded by Zeitwerk so this code never leaks into production.
8
+ #
9
+ # require "pgbus/testing" # core only
10
+ # require "pgbus/testing/rspec" # RSpec matchers + auto-config
11
+ # require "pgbus/testing/minitest" # Minitest assertions
12
+ #
13
+ # Three modes:
14
+ # :fake — capture published events in an in-memory store (default for tests)
15
+ # :inline — capture AND immediately dispatch to matching handlers
16
+ # :disabled — pass through to the real publisher (production behavior)
17
+ module Testing
18
+ MODES = %i[fake inline disabled].freeze
19
+ MODE_KEY = :__pgbus_test_mode
20
+
21
+ # Thread-safe in-memory store for events captured in fake/inline mode.
22
+ class EventStore
23
+ def initialize
24
+ @mutex = Mutex.new
25
+ @events = []
26
+ end
27
+
28
+ def push_event(event)
29
+ @mutex.synchronize { @events << event }
30
+ end
31
+
32
+ def events(routing_key: nil)
33
+ @mutex.synchronize do
34
+ result = @events.dup
35
+ result = result.select { |e| e.routing_key == routing_key } if routing_key
36
+ result
37
+ end
38
+ end
39
+
40
+ def size
41
+ @mutex.synchronize { @events.size }
42
+ end
43
+
44
+ def clear!
45
+ @mutex.synchronize { @events.clear }
46
+ end
47
+
48
+ # Dispatch all stored events to their matching handlers, then clear.
49
+ # Events are removed one at a time after successful dispatch so that
50
+ # a handler exception leaves unprocessed events in the store.
51
+ def drain!
52
+ loop do
53
+ event = @mutex.synchronize { @events.first }
54
+ break unless event
55
+
56
+ Pgbus::EventBus::Registry.instance.handlers_for(event.routing_key).each do |subscriber|
57
+ subscriber.handler_class.new.handle(event)
58
+ end
59
+
60
+ @mutex.synchronize { @events.shift }
61
+ end
62
+ end
63
+ end
64
+
65
+ # Eagerly initialize the store so concurrent access never races on creation.
66
+ @store = EventStore.new
67
+
68
+ class << self
69
+ def mode!(mode, &block)
70
+ raise ArgumentError, "Unknown mode: #{mode}. Valid modes: #{MODES.join(", ")}" unless MODES.include?(mode)
71
+
72
+ sync_streams_test_mode!(mode)
73
+
74
+ unless block
75
+ Thread.main[MODE_KEY] = mode
76
+ return
77
+ end
78
+
79
+ old = Thread.current[MODE_KEY]
80
+ Thread.current[MODE_KEY] = mode
81
+ yield
82
+ ensure
83
+ if block
84
+ Thread.current[MODE_KEY] = old
85
+ sync_streams_test_mode!(old || :disabled)
86
+ end
87
+ end
88
+
89
+ def mode
90
+ Thread.current[MODE_KEY] || Thread.main[MODE_KEY] || :disabled
91
+ end
92
+
93
+ def fake!(&) = mode!(:fake, &)
94
+ def inline!(&) = mode!(:inline, &)
95
+ def disabled!(&) = mode!(:disabled, &)
96
+
97
+ def fake? = mode == :fake
98
+ def inline? = mode == :inline
99
+ def disabled? = mode == :disabled
100
+
101
+ attr_reader :store
102
+
103
+ private
104
+
105
+ # When entering fake/inline mode, enable streams_test_mode to prevent
106
+ # rack.hijack from spawning background threads that acquire DB
107
+ # connections outside the test transaction (see issue #133). When
108
+ # returning to :disabled mode, restore the previous setting.
109
+ def sync_streams_test_mode!(mode)
110
+ return unless defined?(Pgbus.configuration)
111
+
112
+ if mode == :disabled
113
+ Pgbus.configuration.streams_test_mode = false
114
+ Pgbus::Web::Streamer.reset! if defined?(Pgbus::Web::Streamer)
115
+ else
116
+ Pgbus.configuration.streams_test_mode = true
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ require_relative "testing/assertions"
data/lib/pgbus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.7.2"
4
+ VERSION = "0.7.3"
5
5
  end
@@ -62,6 +62,8 @@ module Pgbus
62
62
  cursor = parse_cursor(env, request)
63
63
  return bad_request("invalid cursor: #{cursor}") if cursor.is_a?(String)
64
64
 
65
+ return test_mode_stub if config.streams_test_mode
66
+
65
67
  return over_capacity if streamer.registry.size >= config.streams_max_connections
66
68
 
67
69
  if config.streams_falcon_streaming_body
@@ -210,6 +212,11 @@ module Pgbus
210
212
  def server_error
211
213
  [500, { "content-type" => "text/plain" }, ["pgbus: internal error"]]
212
214
  end
215
+
216
+ def test_mode_stub
217
+ body = ": pgbus test mode — connection accepted, no polling\n\n"
218
+ [200, sse_headers, [body]]
219
+ end
213
220
  end
214
221
  end
215
222
  end
data/lib/pgbus.rb CHANGED
@@ -39,6 +39,7 @@ module Pgbus
39
39
  )
40
40
  loader.ignore("#{__dir__}/generators")
41
41
  loader.ignore("#{__dir__}/active_job")
42
+ loader.ignore("#{__dir__}/pgbus/testing")
42
43
  # lib/puma/plugin/pgbus_streams.rb is a Puma plugin — it's required
43
44
  # explicitly by the user from config/puma.rb via `plugin :pgbus_streams`.
44
45
  # Without this ignore, Zeitwerk scans lib/puma/ under the pgbus loader
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -308,6 +308,10 @@ files:
308
308
  - lib/pgbus/streams/turbo_broadcastable.rb
309
309
  - lib/pgbus/streams/turbo_stream_override.rb
310
310
  - lib/pgbus/streams/watermark_cache_middleware.rb
311
+ - lib/pgbus/testing.rb
312
+ - lib/pgbus/testing/assertions.rb
313
+ - lib/pgbus/testing/minitest.rb
314
+ - lib/pgbus/testing/rspec.rb
311
315
  - lib/pgbus/uniqueness.rb
312
316
  - lib/pgbus/version.rb
313
317
  - lib/pgbus/web/authentication.rb