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); } } 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({ 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..84a4aa6de 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, promises as fsPromises } 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); - }, 4_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(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + 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); + + // 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. + vi.spyOn(fsPromises, 'stat').mockImplementation(async () => { + return { mtimeMs: fakeMtime } as any; + }); + + let writeCount = 0; + const timer = setInterval(() => { + writeCount++; + fakeMtime = Date.now(); // keep mtime in sync with advancing fake clock + }, 500); + setTimeout(() => clearInterval(timer), 3_000); + + const loader = createMinidumpLoader(() => Promise.resolve([dumpPath])); + + let callbackCalled = false; + const loaderPromise = loader(false, async () => { + callbackCalled = true; + }); + + // 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); + }); + + 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; + }); });