From 0708f932a51d7e4a0d1022a01f73d0267d42660d Mon Sep 17 00:00:00 2001 From: Nikolay Bryskin Date: Mon, 25 May 2026 23:10:34 +0300 Subject: [PATCH] apprt/gtk: add regression test for audio-bell MediaFile reuse Guards the contract that prevents the bell thread leak: bellMediaFile must return the same cached MediaFile for an unchanged path and only rebuild when the path changes. A revert to per-bell allocation (the leak) would fail this. Runs in the existing test-gtk CI job; needs no display or playback since the path bookkeeping is all that's asserted. Co-Authored-By: Claude Opus 4.7 --- src/apprt/gtk/media.zig | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/apprt/gtk/media.zig b/src/apprt/gtk/media.zig index 62bf0db8f..7883bf372 100644 --- a/src/apprt/gtk/media.zig +++ b/src/apprt/gtk/media.zig @@ -127,3 +127,31 @@ fn mediaFileError( err.f_message orelse "", }); } + +test "bellMediaFile reuses one MediaFile per path" { + // Regression guard for the audio-bell thread leak: each bell must replay a + // single cached MediaFile, not allocate a fresh GStreamer pipeline (which + // leaked gstglcontext/gldisplay-event threads) per ring. We assert the + // reuse contract of bellMediaFile directly; this needs no display and no + // playback (MediaFile is lazy), only that the path comparison drives reuse. + const testing = std.testing; + + // The files need not exist: MediaFile only records the path until played. + const path_a: [:0]const u8 = "/tmp/ghostty-bell-test-a.oga"; + const path_b: [:0]const u8 = "/tmp/ghostty-bell-test-b.oga"; + + var current = bellMediaFile(null, path_a, false) orelse return error.SkipZigTest; + const first = current; + try testing.expect(isForPath(current, path_a)); + + // Same path => identical object (the leak regression is rebuilding here). + current = bellMediaFile(current, path_a, false).?; + try testing.expectEqual(first, current); + + // Changed path => rebuilt object targeting the new path (old one freed). + current = bellMediaFile(current, path_b, false) orelse return error.SkipZigTest; + try testing.expect(isForPath(current, path_b)); + try testing.expect(!isForPath(current, path_a)); + + current.unref(); +}