From 5b3482020895e33f52ec0dc93ba3adc7913bb29d Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 30 Jun 2026 13:41:17 +0100 Subject: [PATCH 1/6] test: Windows test flake --- test/e2e/test-apps/other/browser-profiling-manual/test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/test-apps/other/browser-profiling-manual/test.ts b/test/e2e/test-apps/other/browser-profiling-manual/test.ts index d7ae5ea53..0b3215c78 100644 --- a/test/e2e/test-apps/other/browser-profiling-manual/test.ts +++ b/test/e2e/test-apps/other/browser-profiling-manual/test.ts @@ -9,6 +9,7 @@ import { electronTestRunner(__dirname, async (ctx) => { await ctx + .ignoreExpectationOrder() // Expect the transaction (without attached profile since we're using UI profiling) .expect({ envelope: transactionEnvelope({ From 4e7b5876b2d19b8a0c3d483dcd9565556becc3c5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 30 Jun 2026 14:35:32 +0100 Subject: [PATCH 2/6] More test flakes --- test/e2e/utils.ts | 4 ++-- test/unit/minidump-loader.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/utils.ts b/test/e2e/utils.ts index beb7aa746..c25d2bc49 100644 --- a/test/e2e/utils.ts +++ b/test/e2e/utils.ts @@ -339,10 +339,10 @@ export function transactionEnvelope(event: TransactionEvent, ...otherEnvelopeIte export function sessionEnvelope(session: SerializedSession): Envelope { return [ - { + expect.objectContaining({ sent_at: ISO_DATE_MATCHER, sdk: { name: 'sentry.javascript.electron', version: SDK_VERSION }, - }, + }), [ [ { type: 'session' }, diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts index b43d737c6..02b9e8b46 100644 --- a/test/unit/minidump-loader.test.ts +++ b/test/unit/minidump-loader.test.ts @@ -130,7 +130,7 @@ describe('createMinidumpLoader', () => { setTimeout(() => { clearInterval(timer); closeSync(file); - }, 4_200); + }, 3_200); const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); From c0074e29f648c5ac6086c586eb4e755f6c1e2089 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 30 Jun 2026 14:58:25 +0100 Subject: [PATCH 3/6] Use fake timers --- test/unit/minidump-loader.test.ts | 278 ++++++++++++++++-------------- 1 file changed, 144 insertions(+), 134 deletions(-) diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts index 02b9e8b46..8d7e108cc 100644 --- a/test/unit/minidump-loader.test.ts +++ b/test/unit/minidump-loader.test.ts @@ -1,9 +1,9 @@ import '../../scripts/electron-shim.mjs'; import { uuid4 } from '@sentry/core'; -import { closeSync, existsSync, openSync, utimesSync, writeFileSync, writeSync } from 'fs'; +import { existsSync, utimesSync, writeFileSync } from 'fs'; import { join } from 'path'; import * as tmp from 'tmp'; -import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; const { createMinidumpLoader } = await import('../../src/main/integrations/sentry-minidump/minidump-loader'); @@ -11,12 +11,17 @@ function dumpFileName(): string { return `${uuid4()}.dmp`; } +function setMtime(path: string, date: Date): void { + utimesSync(path, date, date); +} + const VALID_LOOKING_MINIDUMP = Buffer.from(`MDMP${'X'.repeat(12_000)}`); const LOOKS_NOTHING_LIKE_A_MINIDUMP = Buffer.from('X'.repeat(12_000)); const MINIDUMP_HEADER_BUT_TOO_SMALL = Buffer.from('MDMPdflahfalfhalkfnaklsfnalfkn'); describe('createMinidumpLoader', () => { let tempDir: tmp.DirResult; + beforeAll(() => { tempDir = tmp.dirSync({ unsafeCleanup: true }); }); @@ -27,136 +32,141 @@ describe('createMinidumpLoader', () => { } }); - test('creates attachment from minidump', () => - new Promise((done) => { - const name = dumpFileName(); - const dumpPath = join(tempDir.name, name); - writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); - - const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); - - void loader(false, async (_, attachment) => { - expect(attachment).to.eql({ - data: VALID_LOOKING_MINIDUMP, - filename: name, - attachmentType: 'event.minidump', - }); - - setTimeout(() => { - expect(existsSync(dumpPath)).to.be.false; - done(); - }, 1_000); - }); - })); - - test("doesn't send invalid minidumps", () => - new Promise((done) => { - const missingHeaderDump = join(tempDir.name, dumpFileName()); - writeFileSync(missingHeaderDump, LOOKS_NOTHING_LIKE_A_MINIDUMP); - const tooSmallDump = join(tempDir.name, dumpFileName()); - writeFileSync(tooSmallDump, MINIDUMP_HEADER_BUT_TOO_SMALL); - - const loader = createMinidumpLoader(() => Promise.resolve([missingHeaderDump, tooSmallDump])); - - let passedAttachment = false; - void loader(false, async () => { - passedAttachment = true; - }); - - setTimeout(() => { - expect(passedAttachment).to.be.false; - expect(existsSync(missingHeaderDump)).to.be.false; - expect(existsSync(tooSmallDump)).to.be.false; - done(); - }, 2_000); - })); - - test("doesn't send minidumps that are over 30 days old", () => - new Promise((done) => { - const dumpPath = join(tempDir.name, dumpFileName()); - writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); - const thirtyOneDaysAgo = new Date(new Date().getTime() - 31 * 24 * 3_600 * 1_000); - utimesSync(dumpPath, thirtyOneDaysAgo, thirtyOneDaysAgo); - - const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); - - let passedAttachment = false; - void loader(false, async () => { - passedAttachment = true; - }); - - setTimeout(() => { - expect(passedAttachment).to.be.false; - expect(existsSync(dumpPath)).to.be.false; - done(); - }, 2_000); - })); - - test('deletes minidumps when sdk is disabled', () => - new Promise((done) => { - const dumpPath = join(tempDir.name, dumpFileName()); - writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); - - const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); - - let passedAttachment = false; - void loader(true, async () => { - passedAttachment = true; - }); - - setTimeout(() => { - expect(passedAttachment).to.be.false; - expect(existsSync(dumpPath)).to.be.false; - done(); - }, 2_000); - })); - - test( - 'waits for minidump to stop being modified', - { timeout: 10_000, repeats: 2 }, - () => - new Promise((done) => { - const dumpPath = join(tempDir.name, dumpFileName()); - const file = openSync(dumpPath, 'w'); - writeSync(file, VALID_LOOKING_MINIDUMP); - - let count = 0; - // Write the file every 500ms - const timer = setInterval(() => { - count += 500; - writeSync(file, 'X'); - }, 500); - - setTimeout(() => { - clearInterval(timer); - closeSync(file); - }, 3_200); - - const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); - - void loader(false, async (_) => { - expect(count).to.be.greaterThanOrEqual(3_000); - done(); - }); - }), - ); - - test('sending continues after loading failures', () => - new Promise((done) => { - const missingPath = join(tempDir.name, dumpFileName()); - const name = dumpFileName(); - const dumpPath = join(tempDir.name, name); - writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); - - const loader = createMinidumpLoader(() => Promise.resolve([missingPath, dumpPath])); - - void loader(false, async (_, attachment) => { - expect(attachment.filename).to.eql(name); - - setTimeout(() => { - expect(existsSync(dumpPath)).to.be.false; - done(); - }, 1_000); - }); - })); + beforeEach(() => { + // Exclude setImmediate so it can be used to yield to the real I/O event-loop phase + // between fake-timer advances (setImmediate fires after poll, where fs.stat completes). + vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test('creates attachment from minidump', async () => { + const name = dumpFileName(); + const dumpPath = join(tempDir.name, name); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + setMtime(dumpPath, new Date(Date.now() - 2_000)); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + let attachment: unknown; + await loader(false, async (_, att) => { + attachment = att; + }); + + expect(attachment).to.eql({ + data: VALID_LOOKING_MINIDUMP, + filename: name, + attachmentType: 'event.minidump', + }); + expect(existsSync(dumpPath)).to.be.false; + }); + + test("doesn't send invalid minidumps", async () => { + const missingHeaderDump = join(tempDir.name, dumpFileName()); + writeFileSync(missingHeaderDump, LOOKS_NOTHING_LIKE_A_MINIDUMP); + setMtime(missingHeaderDump, new Date(Date.now() - 2_000)); + const tooSmallDump = join(tempDir.name, dumpFileName()); + writeFileSync(tooSmallDump, MINIDUMP_HEADER_BUT_TOO_SMALL); + setMtime(tooSmallDump, new Date(Date.now() - 2_000)); + + const loader = createMinidumpLoader(() => Promise.resolve([missingHeaderDump, tooSmallDump])); + + let passedAttachment = false; + await loader(false, async () => { + passedAttachment = true; + }); + + expect(passedAttachment).to.be.false; + expect(existsSync(missingHeaderDump)).to.be.false; + expect(existsSync(tooSmallDump)).to.be.false; + }); + + test("doesn't send minidumps that are over 30 days old", async () => { + const dumpPath = join(tempDir.name, dumpFileName()); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + setMtime(dumpPath, new Date(Date.now() - 31 * 24 * 3_600 * 1_000)); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + let passedAttachment = false; + await loader(false, async () => { + passedAttachment = true; + }); + + expect(passedAttachment).to.be.false; + expect(existsSync(dumpPath)).to.be.false; + }); + + test('deletes minidumps when sdk is disabled', async () => { + const dumpPath = join(tempDir.name, dumpFileName()); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + setMtime(dumpPath, new Date(Date.now() - 2_000)); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + let passedAttachment = false; + await loader(true, async () => { + passedAttachment = true; + }); + + expect(passedAttachment).to.be.false; + expect(existsSync(dumpPath)).to.be.false; + }); + + test('waits for minidump to stop being modified', async () => { + const dumpPath = join(tempDir.name, dumpFileName()); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + // Mtime starts at fake-clock "now" so the loader sees it as recently modified + setMtime(dumpPath, new Date()); + + let writeCount = 0; + const timer = setInterval(() => { + writeCount++; + // Keep mtime in sync with the advancing fake clock + setMtime(dumpPath, new Date()); + }, 500); + + setTimeout(() => clearInterval(timer), 3_000); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + let callbackCalled = false; + const loaderPromise = loader(false, async () => { + callbackCalled = true; + }); + + // Advance in 500ms steps, yielding to the real I/O event-loop phase (via setImmediate, + // which fires after poll) between each step so that fs.stat calls in the retry loop + // complete before the next timer tick. A single advanceTimersByTimeAsync(6000) doesn't + // work because its internal yield is setTimeout(0), which fires before the I/O poll phase. + for (let i = 0; i < 12; i++) { + await vi.advanceTimersByTimeAsync(500); + await new Promise((resolve) => setImmediate(resolve)); + } + + expect(callbackCalled).toBe(true); + expect(writeCount).toBeGreaterThanOrEqual(5); + + await loaderPromise; + }); + + test('sending continues after loading failures', async () => { + const missingPath = join(tempDir.name, dumpFileName()); + const name = dumpFileName(); + const dumpPath = join(tempDir.name, name); + writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); + setMtime(dumpPath, new Date(Date.now() - 2_000)); + + const loader = createMinidumpLoader(() => Promise.resolve([missingPath, dumpPath])); + + let receivedName: string | undefined; + await loader(false, async (_, attachment) => { + receivedName = attachment.filename; + }); + + expect(receivedName).to.eql(name); + expect(existsSync(dumpPath)).to.be.false; + }); }); From c1679fe97e0d7e13abdd4519cc070b835c68d01e Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 30 Jun 2026 15:31:45 +0100 Subject: [PATCH 4/6] fix --- test/unit/minidump-loader.test.ts | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts index 8d7e108cc..9ac8a252d 100644 --- a/test/unit/minidump-loader.test.ts +++ b/test/unit/minidump-loader.test.ts @@ -1,6 +1,6 @@ import '../../scripts/electron-shim.mjs'; import { uuid4 } from '@sentry/core'; -import { existsSync, utimesSync, writeFileSync } from 'fs'; +import { existsSync, utimesSync, writeFileSync, promises as fsPromises } from 'fs'; import { join } from 'path'; import * as tmp from 'tmp'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; @@ -33,12 +33,11 @@ describe('createMinidumpLoader', () => { }); beforeEach(() => { - // Exclude setImmediate so it can be used to yield to the real I/O event-loop phase - // between fake-timer advances (setImmediate fires after poll, where fs.stat completes). - vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] }); + vi.useFakeTimers(); }); afterEach(() => { + vi.restoreAllMocks(); vi.useRealTimers(); }); @@ -118,16 +117,22 @@ describe('createMinidumpLoader', () => { test('waits for minidump to stop being modified', async () => { const dumpPath = join(tempDir.name, dumpFileName()); writeFileSync(dumpPath, VALID_LOOKING_MINIDUMP); - // Mtime starts at fake-clock "now" so the loader sees it as recently modified - setMtime(dumpPath, new Date()); + + // Track the mtime the loader will see - starts at "now" (recently modified) + let fakeMtime = Date.now(); + + // Mock fs.promises.stat so the retry loop resolves via microtasks rather than real + // I/O callbacks. Real I/O callbacks fire in the event-loop poll phase, which + // vi.advanceTimersByTimeAsync can't guarantee to drain between fake timer fires. + const statSpy = vi.spyOn(fsPromises, 'stat').mockImplementation(async () => { + return { mtimeMs: fakeMtime } as any; + }); let writeCount = 0; const timer = setInterval(() => { writeCount++; - // Keep mtime in sync with the advancing fake clock - setMtime(dumpPath, new Date()); + fakeMtime = Date.now(); // keep mtime in sync with advancing fake clock }, 500); - setTimeout(() => clearInterval(timer), 3_000); const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); @@ -137,19 +142,14 @@ describe('createMinidumpLoader', () => { callbackCalled = true; }); - // Advance in 500ms steps, yielding to the real I/O event-loop phase (via setImmediate, - // which fires after poll) between each step so that fs.stat calls in the retry loop - // complete before the next timer tick. A single advanceTimersByTimeAsync(6000) doesn't - // work because its internal yield is setTimeout(0), which fires before the I/O poll phase. - for (let i = 0; i < 12; i++) { - await vi.advanceTimersByTimeAsync(500); - await new Promise((resolve) => setImmediate(resolve)); - } + // Advance past writes stopping (3000ms) + NOT_MODIFIED_MS (1000ms) + one retry (500ms) + await vi.advanceTimersByTimeAsync(6_000); + // After the advance the mtime check passes, but fs.readFile/unlink are real I/O — + // await the loader promise before asserting so that I/O settles. + await loaderPromise; expect(callbackCalled).toBe(true); expect(writeCount).toBeGreaterThanOrEqual(5); - - await loaderPromise; }); test('sending continues after loading failures', async () => { From 96a35def89dd3a63ae40805f70d6d097933d84a9 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 30 Jun 2026 15:34:02 +0100 Subject: [PATCH 5/6] Fix --- test/e2e/runner.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/e2e/runner.ts b/test/e2e/runner.ts index b0e9d35ae..c065dd7b8 100644 --- a/test/e2e/runner.ts +++ b/test/e2e/runner.ts @@ -122,19 +122,17 @@ export function electronTestRunner( }); expect(unorderedEvents).toEqual(expect.arrayContaining(expectedEvents)); - } - } catch (e) { - reject?.(e); - } - if (expectations.length === 0) { - if (options.waitAfterExpectedEvents) { - delay(options.waitAfterExpectedEvents).then(() => { + if (options.waitAfterExpectedEvents) { + delay(options.waitAfterExpectedEvents).then(() => { + resolve?.(); + }); + } else { resolve?.(); - }); - } else { - resolve?.(); + } } + } catch (e) { + reject?.(e); } } From 50fb4fb4d4a924403a0ddc1a085f387c5417b6eb Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 30 Jun 2026 16:06:20 +0100 Subject: [PATCH 6/6] fix lint --- test/unit/minidump-loader.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/minidump-loader.test.ts b/test/unit/minidump-loader.test.ts index 9ac8a252d..84a4aa6de 100644 --- a/test/unit/minidump-loader.test.ts +++ b/test/unit/minidump-loader.test.ts @@ -124,7 +124,7 @@ describe('createMinidumpLoader', () => { // Mock fs.promises.stat so the retry loop resolves via microtasks rather than real // I/O callbacks. Real I/O callbacks fire in the event-loop poll phase, which // vi.advanceTimersByTimeAsync can't guarantee to drain between fake timer fires. - const statSpy = vi.spyOn(fsPromises, 'stat').mockImplementation(async () => { + vi.spyOn(fsPromises, 'stat').mockImplementation(async () => { return { mtimeMs: fakeMtime } as any; });