From e4e5ffe7a34f62298349871260171801ffd1b853 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Mon, 28 Sep 2026 14:28:46 +0200 Subject: [PATCH] test: Isolate forked tests --- sentry-rails/spec/isolated/rails_exit_spec.rb | 42 ++++++++ sentry-rails/spec/sentry/rails_spec.rb | 39 -------- .../spec/isolated/forked_workers_spec.rb | 97 +++++++++++++++++++ sentry-ruby/spec/sentry/sequel_spec.rb | 2 + .../spec/sentry/session_flusher_spec.rb | 15 --- ...ed_examples_for_telemetry_event_buffers.rb | 31 ------ spec/support/fork_helper.rb | 2 +- 7 files changed, 142 insertions(+), 86 deletions(-) create mode 100644 sentry-rails/spec/isolated/rails_exit_spec.rb create mode 100644 sentry-ruby/spec/isolated/forked_workers_spec.rb diff --git a/sentry-rails/spec/isolated/rails_exit_spec.rb b/sentry-rails/spec/isolated/rails_exit_spec.rb new file mode 100644 index 000000000..4e8c3d06b --- /dev/null +++ b/sentry-rails/spec/isolated/rails_exit_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +RSpec.describe Sentry::Rails, type: :request do + context "at exit" do + before do + make_basic_app + Rails.application.load_runner + end + + it "captures exception if exit code is non-zero" do + skip('fork not supported in jruby') if RUBY_PLATFORM == 'java' + captured_message = capture_in_separate_process(exit_code: 1) do |pipe_out| + allow(Sentry::Rails).to receive(:capture_exception) do |event| + pipe_out.puts event + end + + # silence process + $stderr.reopen('/dev/null', 'w') + $stdout.reopen('/dev/null', 'w') + end + captured_message = captured_message.split("\n").last + + expect(captured_message).to eq('exit') + end + + it "does not capture exception if exit code is zero" do + skip('fork not supported in jruby') if RUBY_PLATFORM == 'java' + captured_message = capture_in_separate_process(exit_code: 0) do |pipe_out| + allow(Sentry::Rails).to receive(:capture_exception) do |event| + pipe_out.puts event + end + + # silence process + $stderr.reopen('/dev/null', 'w') + $stdout.reopen('/dev/null', 'w') + end + captured_message = captured_message.split("\n").last + + expect(captured_message).to be_nil + end + end +end diff --git a/sentry-rails/spec/sentry/rails_spec.rb b/sentry-rails/spec/sentry/rails_spec.rb index 6346fd847..b93435cdd 100644 --- a/sentry-rails/spec/sentry/rails_spec.rb +++ b/sentry-rails/spec/sentry/rails_spec.rb @@ -140,45 +140,6 @@ end end - context "at exit" do - before do - make_basic_app - Rails.application.load_runner - end - - it "captures exception if exit code is non-zero" do - skip('fork not supported in jruby') if RUBY_PLATFORM == 'java' - captured_message = capture_in_separate_process(exit_code: 1) do |pipe_out| - allow(Sentry::Rails).to receive(:capture_exception) do |event| - pipe_out.puts event - end - - # silence process - $stderr.reopen('/dev/null', 'w') - $stdout.reopen('/dev/null', 'w') - end - captured_message = captured_message.split("\n").last - - expect(captured_message).to eq('exit') - end - - it "does not capture exception if exit code is zero" do - skip('fork not supported in jruby') if RUBY_PLATFORM == 'java' - captured_message = capture_in_separate_process(exit_code: 0) do |pipe_out| - allow(Sentry::Rails).to receive(:capture_exception) do |event| - pipe_out.puts event - end - - # silence process - $stderr.reopen('/dev/null', 'w') - $stdout.reopen('/dev/null', 'w') - end - captured_message = captured_message.split("\n").last - - expect(captured_message).to be_nil - end - end - RSpec.shared_examples "report_rescued_exceptions" do context "with report_rescued_exceptions = true" do before do diff --git a/sentry-ruby/spec/isolated/forked_workers_spec.rb b/sentry-ruby/spec/isolated/forked_workers_spec.rb new file mode 100644 index 000000000..9af1a9bed --- /dev/null +++ b/sentry-ruby/spec/isolated/forked_workers_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.describe Sentry::SessionFlusher, when: { ruby_engine?: "ruby" } do + subject { Sentry.session_flusher } + + let(:session) do + session = Sentry::Session.new + session.close + session + end + + before do + perform_basic_setup do |config| + config.release = "test-release" + config.environment = "test" + config.background_worker_threads = 0 + end + end + + after do + subject.kill + subject.thread&.join + end + + it "discards aggregates inherited from the parent after a fork" do + subject.add_session(session) + + result = capture_in_separate_process do |writer| + subject.add_session(session) + aggregate = subject.instance_variable_get(:@pending_aggregates).values.first + writer.puts aggregate[:exited] + end + + expect(result.to_i).to eq(1) + expect(subject.instance_variable_get(:@pending_aggregates).values.first[:exited]).to eq(1) + end +end + +RSpec.shared_examples "forked telemetry buffer" do |event_factory:, max_items_config:| + let(:event) { event_factory.call } + + before do + perform_basic_setup do |config| + config.background_worker_threads = 0 + config.public_send(:"#{max_items_config}=", 3) + end + end + + after do + subject.kill + subject.thread&.join + Sentry.background_worker = Class.new { def shutdown; end }.new + end + + it "resets inherited worker state before adding telemetry" do + subject.add_item(event) + subject.wait_until_idle + expect(subject.size).to eq(1) + + result = capture_in_separate_process do |writer| + subject.add_item(event) + subject.flush + item_count = sentry_envelopes.first.items.first.headers[:item_count] + writer.puts [subject.size, sentry_envelopes.size, item_count].join(",") + end + + child_size, child_envelopes, child_item_count = result.split(",").map(&:to_i) + expect(child_size).to eq(0) + expect(child_envelopes).to eq(1) + expect(child_item_count).to eq(1) + expect(subject.size).to eq(1) + expect(sentry_envelopes).to be_empty + + subject.flush + expect(sentry_envelopes.size).to eq(1) + + subject.add_item(event) + subject.flush + expect(sentry_envelopes.size).to eq(2) + end +end + +RSpec.describe Sentry::LogEventBuffer, when: { ruby_engine?: "ruby" } do + subject { described_class.new(Sentry.configuration, Sentry.get_current_client) } + + include_examples "forked telemetry buffer", + event_factory: -> { Sentry::LogEvent.new(level: :info, body: "Test message") }, + max_items_config: :max_log_events +end + +RSpec.describe Sentry::MetricEventBuffer, when: { ruby_engine?: "ruby" } do + subject { described_class.new(Sentry.configuration, Sentry.get_current_client) } + + include_examples "forked telemetry buffer", + event_factory: -> { Sentry::MetricEvent.new(name: "test.metric", type: :counter, value: 1) }, + max_items_config: :max_metric_events +end diff --git a/sentry-ruby/spec/sentry/sequel_spec.rb b/sentry-ruby/spec/sentry/sequel_spec.rb index 0d7cde1c8..1af27f5a5 100644 --- a/sentry-ruby/spec/sentry/sequel_spec.rb +++ b/sentry-ruby/spec/sentry/sequel_spec.rb @@ -28,6 +28,8 @@ after do db.drop_table?(:posts) + ensure + db.disconnect end context "with tracing enabled" do diff --git a/sentry-ruby/spec/sentry/session_flusher_spec.rb b/sentry-ruby/spec/sentry/session_flusher_spec.rb index 6ec9f19c2..aa7091f6c 100644 --- a/sentry-ruby/spec/sentry/session_flusher_spec.rb +++ b/sentry-ruby/spec/sentry/session_flusher_spec.rb @@ -128,21 +128,6 @@ expect(pending_aggregates.values.first).to include({ errored: 0, exited: 1 }) end - context "when a child adds a session after a fork", when: { ruby_engine?: "ruby" } do - it "discards aggregates inherited from the parent" do - subject.add_session(session) - - result = capture_in_separate_process do |writer| - subject.add_session(session) - aggregate = subject.instance_variable_get(:@pending_aggregates).values.first - writer.puts aggregate[:exited] - end - - expect(result.to_i).to eq(1) - expect(subject.instance_variable_get(:@pending_aggregates).values.first[:exited]).to eq(1) - end - end - context "when thread creation fails" do before do allow(Thread).to receive(:new).and_raise(ThreadError) diff --git a/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb b/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb index 566ecf886..cb9595137 100644 --- a/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb +++ b/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb @@ -324,37 +324,6 @@ end end - context "when a child adds telemetry after a fork", when: { ruby_engine?: "ruby" } do - let(:max_items) { 3 } - - it "resets inherited worker state before adding telemetry" do - subject.add_item(event) - subject.wait_until_idle - expect(subject.size).to eq(1) - - result = capture_in_separate_process do |writer| - subject.add_item(event) - subject.flush - item_count = sentry_envelopes.first.items.first.headers[:item_count] - writer.puts [subject.size, sentry_envelopes.size, item_count].join(",") - end - - child_size, child_envelopes, child_item_count = result.split(",").map(&:to_i) - expect(child_size).to eq(0) - expect(child_envelopes).to eq(1) - expect(child_item_count).to eq(1) - expect(subject.size).to eq(1) - expect(sentry_envelopes).to be_empty - - subject.flush - expect(sentry_envelopes.size).to eq(1) - - subject.add_item(event) - subject.flush - expect(sentry_envelopes.size).to eq(2) - end - end - describe "error handling" do let(:max_items) { 3 } diff --git a/spec/support/fork_helper.rb b/spec/support/fork_helper.rb index 1f5566836..7b6f3d388 100644 --- a/spec/support/fork_helper.rb +++ b/spec/support/fork_helper.rb @@ -31,7 +31,7 @@ def read_from_child(reader) loop do remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) raise "child did not respond" if remaining <= 0 - raise "child did not respond" unless IO.select([reader], nil, nil, remaining) + raise "child did not respond: IO.select timed out" unless IO.select([reader], nil, nil, remaining) begin output << reader.read_nonblock(4096)