webgl stress + glyph-integrity tests for atlas merges; repro vscode#322756 single-terminal garble
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Single-terminal WebGL atlas stress: try to reproduce the "garbled glyph"
|
||||
* corruption from microsoft/vscode#322756 in ONE terminal (no shared atlas),
|
||||
* via heavy unique-glyph churn that forces atlas page merges, both with content
|
||||
* pinned on screen (TUI-style) and mid-scroll. Chromium/SwiftShader only.
|
||||
*/
|
||||
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
captureRowSignatures,
|
||||
expectSignatureMatches,
|
||||
floodDistinctGlyphs,
|
||||
getAtlasPageCount,
|
||||
loadWebglStrict,
|
||||
setMaxAtlasPages,
|
||||
waitForRender,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
import { generateUniqueGlyphFlood } from '../../../test/playwright/SyntheticTui';
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.describe('WebGL single-terminal heavy render + merge', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||
test.describe.configure({ timeout: 30000 });
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
|
||||
await loadWebglStrict(ctx);
|
||||
});
|
||||
|
||||
test('heavy merge churn must not crash the GlyphRenderer (xtermjs#322756)', async () => {
|
||||
// EXPECTED RED: with a small page cap, atlas merges shrink the pages array
|
||||
// while GlyphRenderer.render still indexes the old page count -> throws
|
||||
// "Cannot read properties of undefined (reading 'version')" and the terminal
|
||||
// garbles. Capture the crash via pageerror.
|
||||
const errors: string[] = [];
|
||||
const onError = (e: Error): void => { errors.push(e.message); };
|
||||
ctx.page.on('pageerror', onError);
|
||||
try {
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
await floodDistinctGlyphs(ctx, 10);
|
||||
// Force a GPU read (screenshot) mid-churn so a render lands during/after a
|
||||
// merge, then churn more — this reliably exposes the version crash.
|
||||
await captureRowSignatures(ctx, 1, [1, 2, 3]);
|
||||
const pages = await floodDistinctGlyphs(ctx, 10);
|
||||
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
|
||||
await ctx.page.waitForTimeout(300);
|
||||
expect(errors, `renderer crashed during atlas merges: ${errors[0] ?? ''}`).toEqual([]);
|
||||
} finally {
|
||||
ctx.page.off('pageerror', onError);
|
||||
}
|
||||
});
|
||||
|
||||
test('pinned header stays correct while body churns many pages', async () => {
|
||||
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
await writeAndWaitForRender(ctx, 'HEADER_REF_0123456789');
|
||||
const reference = await captureRowSignatures(ctx, 1, cols);
|
||||
|
||||
// Body churns unique glyphs (forces merges) while header line stays on screen.
|
||||
const pages = await floodDistinctGlyphs(ctx, 8);
|
||||
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
|
||||
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const after = await captureRowSignatures(ctx, 1, cols);
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `header glyph col ${cols[i]} corrupted by single-terminal merges`);
|
||||
}
|
||||
});
|
||||
|
||||
test('visible rows match content while scrolling (no merge)', async () => {
|
||||
// Big scrollback of distinct glyphs across multiple pages (no tiny page cap,
|
||||
// so no merge), then scroll up/down repeatedly.
|
||||
for (let c = 0; c < 8; c++) {
|
||||
await writeAndWaitForRender(ctx, generateUniqueGlyphFlood(24 * 40, 80, c * 24 * 40));
|
||||
}
|
||||
expect(await getAtlasPageCount(ctx)).toBeGreaterThan(1);
|
||||
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const top = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
|
||||
await ctx.proxy.scrollToBottom();
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const topAgain = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
|
||||
for (let i = 0; i < top.length; i++) {
|
||||
expectSignatureMatches(topAgain[i], top[i], 14, `row1 col${i + 1} changed after scroll round-trip`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user