Skip to content

Commit 194cfa7

Browse files
mournergithub-actions[bot]
authored andcommitted
Speed up and simplify test-usvg
GitOrigin-RevId: aad8d52acc7d963a704136ecbebc54daab87f689
1 parent 7f120b1 commit 194cfa7

2 files changed

Lines changed: 88 additions & 42 deletions

File tree

test/usvg/usvg.test.ts

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ import {describe, test, expect, afterEach, afterAll, onTestFailed, onTestFinishe
55
import {readIconSet} from '../../src/data/usvg/usvg_pb_decoder';
66
import {renderIcon} from '../../src/data/usvg/usvg_pb_renderer';
77
import {allowed, ignores, scales, formatName} from './utils';
8-
import {readArrayBuffer} from '../util/read_array_buffer';
98
// @ts-expect-error - virtual modules are not typed
10-
import {fixtures} from 'virtual:usvg-fixtures';
9+
import {fixtures, iconsets} from 'virtual:usvg-fixtures';
10+
11+
function base64ToUint8Array(base64: string) {
12+
const binary = atob(base64);
13+
const bytes = new Uint8Array(binary.length);
14+
for (let i = 0; i < binary.length; i++) {
15+
bytes[i] = binary.charCodeAt(i);
16+
}
17+
return bytes;
18+
}
1119

12-
async function getIconSet(iconsetPath) {
13-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
14-
const pbf = new PbfReader(await readArrayBuffer(iconsetPath));
15-
const iconSet = readIconSet(pbf);
16-
return iconSet;
20+
function getIconSet(suite: string) {
21+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
22+
const pbf = new PbfReader(base64ToUint8Array(iconsets[suite]));
23+
return readIconSet(pbf);
1724
}
1825

19-
describe('uSVG', async () => {
26+
describe('uSVG', () => {
2027
const diffs = {};
2128
const passed = [];
2229
const failed = [];
@@ -65,8 +72,8 @@ describe('uSVG', async () => {
6572
await server.commands.writeFile('test/usvg/vitest/index.html', html);
6673
});
6774

68-
const testIconSet = async (iconsetPath: string) => {
69-
const iconset = await getIconSet(iconsetPath);
75+
const testIconSet = (suite: string) => {
76+
const iconset = getIconSet(suite);
7077

7178
for (const icon of iconset.icons.sort((a, b) => a.name.localeCompare(b.name))) {
7279
for (const scale of scales) {
@@ -89,46 +96,47 @@ describe('uSVG', async () => {
8996
const actualImageData = renderIcon(icon, {sx: scale, sy: scale, params: {}});
9097
const {width, height, data} = actualImageData;
9198

92-
// align canvas sizes with the image size
93-
diffCanvas.width = actualCanvas.width = expectedCanvas.width = width;
94-
diffCanvas.height = actualCanvas.height = expectedCanvas.height = height;
95-
page.viewport(width * 3, height);
96-
97-
actualContext.putImageData(actualImageData, 0, 0);
98-
99-
// Render expected icon
99+
// Decode the expected PNG fixture to pixels
100+
expectedCanvas.width = width;
101+
expectedCanvas.height = height;
100102
const expectedImage = new Image();
101103
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
102104
expectedImage.src = `data:image/png;base64,${fixtures[name]}`;
103105
await new Promise((resolve) => {
104106
expectedImage.onload = resolve;
105107
});
106-
107-
expectedContext.drawImage(expectedImage, 0, 0, expectedCanvas.width, expectedCanvas.height);
108-
const expectedImageData = expectedContext.getImageData(0, 0, expectedCanvas.width, expectedCanvas.height);
108+
expectedContext.drawImage(expectedImage, 0, 0, width, height);
109+
const expectedImageData = expectedContext.getImageData(0, 0, width, height);
109110

110111
// Compare images
111-
diffCanvas.width = width;
112-
diffCanvas.height = height;
113-
const diffImageData = diffContext.createImageData(diffCanvas.width, diffCanvas.height);
114-
112+
const diffImageData = diffContext.createImageData(width, height);
115113
const options = {
116114
threshold: 0.2,
117115
checkerboard: false
118116
};
119117
const diff = pixelmatch(data, expectedImageData.data, diffImageData.data, width, height, options) / (width * height);
120-
121118
diffs[name] = diff;
122-
diffContext.putImageData(diffImageData, 0, 0);
123-
await page.screenshot({element: document.body, path: `./vitest/${name}.png`});
124119

125-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
126-
expect(diff).toBeLessThanOrEqual(allowed[icon.name]?.[scale] ?? defaultAllowedDiff);
120+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
121+
const allowedDiff = allowed[icon.name]?.[scale] ?? defaultAllowedDiff;
122+
123+
// Only capture a screenshot for failures — it's expensive, and the report only shows failures
124+
if (diff > allowedDiff) {
125+
diffCanvas.width = actualCanvas.width = width;
126+
diffCanvas.height = actualCanvas.height = height;
127+
page.viewport(width * 3, height);
128+
actualContext.putImageData(actualImageData, 0, 0);
129+
diffContext.putImageData(diffImageData, 0, 0);
130+
await page.screenshot({element: document.body, path: `./vitest/${name}.png`});
131+
}
132+
133+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
134+
expect(diff).toBeLessThanOrEqual(allowedDiff);
127135
});
128136
}
129137
}
130138
};
131139

132-
await testIconSet('test/usvg/test-suite/test-suite.iconset');
133-
await testIconSet('test/usvg/mapbox_usvg_pb_test_suite/test-suite.iconset');
140+
testIconSet('test-suite');
141+
testIconSet('mapbox_usvg_pb_test_suite');
134142
});

vitest.config.usvg.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,54 @@
1-
import {basename} from 'path';
2-
import {readFileSync, globSync} from 'fs';
1+
import {basename, extname} from 'path';
2+
import {readFileSync} from 'fs';
3+
import {gunzipSync} from 'zlib';
34
import virtual from '@rollup/plugin-virtual';
45
import {mergeConfig, defineConfig} from 'vitest/config';
56
import baseConfig, {chromiumBrowser} from './vitest.config.base';
67

7-
// base64 encoded PNG fixtures
8-
const fixtures = globSync(['./test/usvg/test-suite/*.png', './test/usvg/mapbox_usvg_pb_test_suite/*.png']).reduce((acc, pngPath) => {
9-
const name = basename(pngPath, '.png');
10-
const base64Data = readFileSync(pngPath, 'base64');
11-
acc[name] = base64Data;
12-
return acc;
13-
}, {});
8+
// Minimal tar reader (ustar): yields regular file entries from an already-gunzipped buffer.
9+
function *readTar(buffer: Buffer): Generator<{name: string, content: Buffer}> {
10+
for (let offset = 0; offset + 512 <= buffer.length;) {
11+
const block = buffer.subarray(offset, offset + 512);
12+
// Two consecutive zero-filled blocks mark the end of the archive.
13+
if (block.every(byte => byte === 0)) break;
14+
15+
const readString = (start: number, length: number) => {
16+
const raw = block.subarray(start, start + length);
17+
const end = raw.indexOf(0);
18+
return raw.toString('utf8', 0, end === -1 ? length : end);
19+
};
20+
21+
const name = readString(0, 100);
22+
const size = parseInt(readString(124, 12).trim() || '0', 8);
23+
const typeFlag = readString(156, 1);
24+
const prefix = readString(345, 155);
25+
const fullName = prefix ? `${prefix}/${name}` : name;
26+
27+
offset += 512;
28+
if (typeFlag === '0' || typeFlag === '') {
29+
yield {name: fullName, content: buffer.subarray(offset, offset + size)};
30+
}
31+
offset += Math.ceil(size / 512) * 512;
32+
}
33+
}
34+
35+
// Extract PNG fixtures (base64) and iconset protobufs (base64) from the test-suite tarballs,
36+
// so tests can read them without unpacking the archives to disk.
37+
const fixtures: Record<string, string> = {};
38+
const iconsets: Record<string, string> = {};
39+
40+
for (const tarPath of ['./test/usvg/test-suite.tar.gz', './test/usvg/mapbox_usvg_pb_test_suite.tar.gz']) {
41+
const suite = basename(tarPath, '.tar.gz');
42+
for (const {name, content} of readTar(gunzipSync(readFileSync(tarPath)))) {
43+
if (basename(name).startsWith('._')) continue; // skip macOS AppleDouble metadata files
44+
const ext = extname(name);
45+
if (ext === '.png') {
46+
fixtures[basename(name, '.png')] = content.toString('base64');
47+
} else if (ext === '.iconset') {
48+
iconsets[suite] = content.toString('base64');
49+
}
50+
}
51+
}
1452

1553
export default mergeConfig(baseConfig, defineConfig({
1654
test: {
@@ -21,7 +59,7 @@ export default mergeConfig(baseConfig, defineConfig({
2159
},
2260
plugins: [
2361
virtual({
24-
'virtual:usvg-fixtures': `export const fixtures = ${JSON.stringify(fixtures)};`
62+
'virtual:usvg-fixtures': `export const fixtures = ${JSON.stringify(fixtures)};\nexport const iconsets = ${JSON.stringify(iconsets)};`
2563
})
2664
]
2765
}));

0 commit comments

Comments
 (0)