Skip to content

Commit 2d7f285

Browse files
committed
test: add afterEach cleanup + timer tracking in error_recovery test (anti-flake)
1 parent bc7e804 commit 2d7f285

1 file changed

Lines changed: 62 additions & 31 deletions

File tree

test/integration/error_recovery_and_state_transitions.test.ts

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,41 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
22
import * as lzma from '../../src/lzma.js';
33

44
describe('Error Recovery and State Transitions', () => {
5+
// Track every stream + timer created so we can guarantee cleanup, even if a
6+
// test rejects mid-flight or a native callback is queued after the test ends.
7+
// Without this, late callbacks (xz.emit('onerror', ...) firing after close())
8+
// can crash the vitest worker — historical Windows+Node20 / Ubuntu+Node24 flakes.
9+
let streams: lzma.Xz[] = [];
10+
let timers: NodeJS.Timeout[] = [];
11+
12+
const track = <T extends lzma.Xz>(s: T): T => {
13+
streams.push(s);
14+
return s;
15+
};
16+
17+
const trackTimer = (t: NodeJS.Timeout): NodeJS.Timeout => {
18+
timers.push(t);
19+
return t;
20+
};
21+
22+
beforeEach(() => {
23+
streams = [];
24+
timers = [];
25+
});
26+
27+
afterEach(() => {
28+
for (const t of timers) clearTimeout(t);
29+
for (const s of streams) {
30+
try {
31+
if (typeof s.destroy === 'function') s.destroy();
32+
else if (typeof s.close === 'function' && !s._closed) s.close();
33+
} catch {
34+
// best-effort: ignore double-destroy errors
35+
}
36+
}
37+
});
38+
539
it('should cover filter validation catch block', () => {
640
// Force the try-catch path in filter validation
741
expect(() => {
@@ -14,12 +48,13 @@ describe('Error Recovery and State Transitions', () => {
1448
};
1549

1650
const xz = new lzma.Xz({ filters: malformedFilters });
51+
track(xz);
1752
xz.close();
1853
}).toThrow('Filters need to be in an array!');
1954
});
2055

2156
it('should cover callback when stream is ending', async () => {
22-
const xz = new lzma.Xz();
57+
const xz = track(new lzma.Xz());
2358

2459
return new Promise<void>((resolve) => {
2560
let callbackExecuted = false;
@@ -29,27 +64,27 @@ describe('Error Recovery and State Transitions', () => {
2964
xz.end(); // This puts stream in 'ending' state
3065

3166
// Immediately try to flush while ending - should trigger the appropriate behavior
32-
setTimeout(() => {
33-
xz.flush(() => {
34-
callbackExecuted = true;
35-
expect(callbackExecuted).toBe(true);
36-
xz.close();
37-
resolve();
38-
});
39-
}, 1); // Very small delay to ensure ending state
67+
trackTimer(
68+
setTimeout(() => {
69+
xz.flush(() => {
70+
callbackExecuted = true;
71+
expect(callbackExecuted).toBe(true);
72+
resolve();
73+
});
74+
}, 1) // Very small delay to ensure ending state
75+
);
4076

4177
// Fallback timeout
42-
setTimeout(() => {
43-
if (!callbackExecuted) {
44-
xz.close();
45-
resolve();
46-
}
47-
}, 50);
78+
trackTimer(
79+
setTimeout(() => {
80+
if (!callbackExecuted) resolve();
81+
}, 50)
82+
);
4883
});
4984
});
5085

5186
it('should cover lines 457-458 - async LZMA error handling', async () => {
52-
const xz = new lzma.Xz();
87+
const xz = track(new lzma.Xz());
5388

5489
return new Promise<void>((resolve) => {
5590
let errorCount = 0;
@@ -59,16 +94,13 @@ describe('Error Recovery and State Transitions', () => {
5994
expect(error).toBeInstanceOf(Error);
6095
// Accept any LZMA error code (native + manual emit may both fire)
6196
expect(error.errno).toBeGreaterThan(0);
62-
63-
if (errorCount === 1) {
64-
xz.close();
65-
}
97+
if (errorCount === 1) xz.close();
6698
});
6799

68100
// Absorb any errors after stream is destroyed (e.g. LZMA_PROG_ERROR
69101
// from native callback firing after close)
70102
xz.on('close', () => {
71-
setTimeout(resolve, 50);
103+
trackTimer(setTimeout(resolve, 50));
72104
});
73105

74106
// Emit onerror directly — this triggers the onerror→error conversion (line 357-360)
@@ -86,7 +118,7 @@ describe('Error Recovery and State Transitions', () => {
86118
];
87119

88120
for (const errorCode of testCases) {
89-
const xz = new lzma.Xz();
121+
const xz = track(new lzma.Xz());
90122

91123
await new Promise<void>((resolve) => {
92124
xz.on('error', (error) => {
@@ -102,18 +134,15 @@ describe('Error Recovery and State Transitions', () => {
102134
});
103135

104136
it('should handle stream state transitions correctly', async () => {
105-
const xz = new lzma.Xz();
137+
const xz = track(new lzma.Xz());
106138

107139
return new Promise<void>((resolve) => {
108140
let statesTracked = 0;
109141
const expectedStates = 3;
110142

111143
const checkComplete = () => {
112144
statesTracked++;
113-
if (statesTracked >= expectedStates) {
114-
xz.close();
115-
resolve();
116-
}
145+
if (statesTracked >= expectedStates) resolve();
117146
};
118147

119148
// Test ended state flush
@@ -126,9 +155,11 @@ describe('Error Recovery and State Transitions', () => {
126155
xz.end();
127156

128157
// Test normal flush
129-
setTimeout(() => {
130-
xz.flush(() => checkComplete());
131-
}, 5);
158+
trackTimer(
159+
setTimeout(() => {
160+
xz.flush(() => checkComplete());
161+
}, 5)
162+
);
132163

133164
checkComplete(); // Initial count
134165
});

0 commit comments

Comments
 (0)