tests: observe Sentry through the gem test helper
Redefining Sentry.capture_message as a singleton was monkey-patching,
and under -w it warned twice per use. setup_sentry_test records into
a dummy transport instead; setup re-opens the SDK because Sentry.init
registers at_exit { close }, which this runner fires before minitest
ever runs the tests.

Assisted-by: GLM-5.3 via pi
change owzovkotxrwtpqnxutylwztknnxkpvkx
commit 1dd5c42cde0a225902e629012c8379d22e93d0b1
author Alpha Chen <alpha@kejadlen.dev>
date
parent xxqvxyok
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index 7733a62..6a7e221 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -8,7 +8,8 @@ require "sequel"
 require "pro_tacts/store"
 
 class StoreTest < Minitest::Test
-  include CapturingSentry
+  include Sentry::TestHelper
+  include SentryMessages
 
   AIDEN = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Aiden\r\nUID:aiden\r\nEND:VCARD\r\n"
   # UTC ISO 8601 to the millisecond, which is what SQLite is asked for.
@@ -28,6 +29,16 @@ class StoreTest < Minitest::Test
     end
   end
 
+  # The store's reports land in the transport this pins for the test;
+  # teardown clears it so nothing carries into the next one.
+  def setup
+    setup_sentry
+  end
+
+  def teardown
+    teardown_sentry_test
+  end
+
   ## Cards
 
   def test_a_stored_card_comes_back_byte_for_byte
@@ -551,7 +562,8 @@ class StoreTest < Minitest::Test
     shared = AIDEN.sub("END:VCARD\r\n", "BDAY:1985-04-12\rNOTE:b\r\nEND:VCARD\r\n")
 
     with_store({"aiden" => AIDEN_BORN}) do |store|
-      messages = capturing_sentry { store.put("aiden", shared) }
+      store.put("aiden", shared)
+      messages = sentry_messages
 
       assert_equal shared, store.contact("aiden").vcard
       assert_nil birthday_row(store, "aiden")
@@ -593,7 +605,8 @@ class StoreTest < Minitest::Test
     edited = AIDEN.sub("FN:Aiden", "FN:Aiden Smith")
 
     with_store({"aiden" => shared}) do |store|
-      messages = capturing_sentry { store.put("aiden", edited) }
+      store.put("aiden", edited)
+      messages = sentry_messages
 
       assert_equal edited, store.contact("aiden").vcard
       assert_equal 1, messages.length
@@ -618,12 +631,13 @@ class StoreTest < Minitest::Test
   # dropped, and nobody would know.
   def test_a_rewrite_dropping_an_unrecognized_bday_is_reported
     with_store({"aiden" => AIDEN.sub("END:VCARD\r\n", "BDAY:1985-13\r\nEND:VCARD\r\n")}) do |store|
-      messages = capturing_sentry {
-        store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
-      }
+      # The seed's own arrival report is not this test's subject.
+      clear_sentry_events
+      store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
+      messages = sentry_messages
 
       assert_equal 1, messages.length
-      assert_match /BDAY/, messages.fetch(0)
+      assert_match(/BDAY/, messages.fetch(0))
     end
   end
 
@@ -633,11 +647,9 @@ class StoreTest < Minitest::Test
   def test_a_rewrite_over_known_bdays_stays_quiet
     ["BDAY:--0412", "BDAY:1985-04"].each do |line|
       with_store({"aiden" => AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n")}) do |store|
-        messages = capturing_sentry {
-          store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
-        }
+        store.put("aiden", AIDEN.sub("FN:Aiden", "FN:Aiden Smith"))
 
-        assert_empty messages, line
+        assert_empty sentry_messages, line
       end
     end
   end
@@ -650,11 +662,10 @@ class StoreTest < Minitest::Test
   def test_an_unrecognized_bday_arriving_is_reported
     ["BDAY:1985-13", "BDAY:--0432", "BDAY:19850412", "BDAY:1985-4"].each do |line|
       with_store({}) do |store|
-        messages = capturing_sentry {
-          store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
-        }
+        clear_sentry_events
+        store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
 
-        assert_equal 1, messages.length, line
+        assert_equal 1, sentry_messages.length, line
       end
     end
   end
@@ -663,11 +674,9 @@ class StoreTest < Minitest::Test
     ["BDAY:1985-04-12", "BDAY:1985-04-12T23:10:00Z", "BDAY;X-APPLE-OMIT-YEAR=1604:1604-04-12",
       "BDAY:--0412", "BDAY:--04-12", "BDAY:1985-04", "BDAY:1985"].each do |line|
       with_store({}) do |store|
-        messages = capturing_sentry {
-          store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
-        }
+        store.put("aiden", AIDEN.sub("END:VCARD\r\n", "#{line}\r\nEND:VCARD\r\n"))
 
-        assert_empty messages, line
+        assert_empty sentry_messages, line
       end
     end
   end
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index 820ed03..16b098a 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -11,7 +11,8 @@ require "pro_tacts/store"
 require "pro_tacts/web"
 
 class WebTest < Minitest::Test
-  include CapturingSentry
+  include Sentry::TestHelper
+  include SentryMessages
   include Rack::Test::Methods
 
   def app
@@ -19,11 +20,17 @@ class WebTest < Minitest::Test
   end
 
   # Every request needs a Tailscale identity; the middleware refuses without
-  # one. Tests for that refusal are in TailscaleAuthTest.
+  # one. Tests for that refusal are in TailscaleAuthTest. The sentry setup
+  # pins the transport the app's reports land in; teardown clears it.
   def setup
+    setup_sentry
     header "Tailscale-User-Login", "test@example.com"
   end
 
+  def teardown
+    teardown_sentry_test
+  end
+
   def test_options_returns_dav_headers
     options "/dav/"
 
@@ -409,9 +416,8 @@ class WebTest < Minitest::Test
     packed = card("new", "New").sub("FN:New\r\n", "FN:New\rNOTE:b\r\n")
 
     with_contacts({}) do
-      messages = capturing_sentry {
-        put_request "new", packed, "CONTENT_TYPE" => VCARD
-      }
+      put_request "new", packed, "CONTENT_TYPE" => VCARD
+      messages = sentry_messages
 
       assert_equal 201, last_response.status
       assert_equal 1, messages.length
@@ -422,9 +428,8 @@ class WebTest < Minitest::Test
   # Nothing else reports: an ordinary card arrives in silence.
   def test_put_of_an_ordinary_card_is_quiet
     with_contacts({}) do
-      messages = capturing_sentry {
-        put_request "new", card("new", "New"), "CONTENT_TYPE" => VCARD
-      }
+      put_request "new", card("new", "New"), "CONTENT_TYPE" => VCARD
+      messages = sentry_messages
 
       assert_equal 201, last_response.status
       assert_empty messages
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 29cb674..7a291a3 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -44,30 +44,42 @@ File.truncate("log/test.log", 0) if File.exist?("log/test.log")
 
 require_relative "fixture_data"
 require "pro_tacts/web"
+require "sentry-ruby"
+require "sentry/test_helper"
+
+# Sentry is initialized here rather than left dormant because the gem's
+# test helper builds on an initialized SDK. The DSN is forced nil rather
+# than read from the environment, so a real SENTRY_DSN in this shell
+# cannot point a test run at a live project. init also registers
+# at_exit { close }, and the suite's runner requires minitest/autorun
+# before any of this loads — so Sentry's close fires before minitest's
+# at_exit runs the tests, and setup (SentryMessages#setup_sentry)
+# re-opens the SDK when it finds it closed. Before minitest/autorun
+# regardless, per the rule the header states, so direct single-file
+# runs — where this file is the entry — keep the close after the tests.
+Sentry.init { |sentry| sentry.dsn = nil }
+
 require "minitest/autorun"
 
 # Standing in for config.ru, which never runs here: build the store and
 # hand it to the app.
 ProTacts::Web.store = FixtureData.install(data_dir)
 
-# Sentry's capture_message, made observable for the length of a block and
-# restored after, so what one test captures cannot leak into the next.
-# Sentry is never initialized under test, so the original is inert
-# anyway — this only makes it observable. Returns what was captured.
-#
-# Here rather than in one test class because the reports are raised from
-# two layers: the web's PUT reports a broken parser assumption, the store
-# reports what its birthday model could not recompose.
-module CapturingSentry
-  def capturing_sentry
-    messages = []
-    original = Sentry.method(:capture_message)
-    Sentry.define_singleton_method(:capture_message) { |message, **| messages << message }
-    begin
-      yield messages
-    ensure
-      Sentry.define_singleton_method(:capture_message, original)
-    end
-    messages
+# The helper's other half: setup_sentry_test re-points the SDK at a
+# dummy DSN and a recording transport, so what the app reports is
+# observable in sentry_events without anything being sent and without
+# redefining Sentry.capture_message. This suite's assertions read
+# report texts, so the recorded Sentry::ErrorEvents also become the
+# message strings, in capture order. Shared by the two test classes
+# whose layers report — the web's PUT a broken parser assumption, the
+# store its birthday model what it cannot recompose.
+module SentryMessages
+  def setup_sentry
+    Sentry.init { |sentry| sentry.dsn = nil } unless Sentry.initialized?
+    setup_sentry_test
+  end
+
+  def sentry_messages
+    sentry_events.map { it.message }
   end
 end