mirror of
https://github.com/svg/svgo.git
synced 2025-07-29 20:21:14 +03:00
Split regression extracter and runner (#1451)
A lot of new sources of regression tests may come and it's important to make debug simpler. Now regression-extract.js downloads and write svg files into test/regression-fixtures. regression.js run each svg in this folder. Mismatched svg diff is written into test/regression-diffs.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
diffs
|
test/regression-fixtures
|
||||||
|
test/regression-diffs
|
||||||
coverage
|
coverage
|
||||||
bin/svgo-profiling
|
bin/svgo-profiling
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -1145,12 +1145,6 @@
|
|||||||
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"get-stream": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.6",
|
"version": "7.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
"fix": "eslint --ignore-path .gitignore --fix . && prettier --write \"**/*.js\" --ignore-path .gitignore",
|
"fix": "eslint --ignore-path .gitignore --fix . && prettier --write \"**/*.js\" --ignore-path .gitignore",
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc",
|
||||||
"test-browser": "rollup -c && node ./test/browser.js",
|
"test-browser": "rollup -c && node ./test/browser.js",
|
||||||
"test-regression": "NO_DIFF=1 node ./test/regression.js",
|
"test-regression": "node ./test/regression-extract.js && NO_DIFF=1 node ./test/regression.js",
|
||||||
"prepublishOnly": "rm -rf dist && rollup -c"
|
"prepublishOnly": "rm -rf dist && rollup -c"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
@ -109,7 +109,6 @@
|
|||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
"eslint": "^7.22.0",
|
"eslint": "^7.22.0",
|
||||||
"get-stream": "^6.0.0",
|
|
||||||
"mocha": "^8.3.2",
|
"mocha": "^8.3.2",
|
||||||
"mock-stdin": "^1.0.0",
|
"mock-stdin": "^1.0.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
|
57
test/regression-extract.js
Normal file
57
test/regression-extract.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const util = require('util');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
const stream = require('stream');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const tarStream = require('tar-stream');
|
||||||
|
|
||||||
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
|
const extractTarGz = async (url, baseDir, include) => {
|
||||||
|
const extract = tarStream.extract();
|
||||||
|
extract.on('entry', async (header, stream, next) => {
|
||||||
|
try {
|
||||||
|
if (include == null || include.test(header.name)) {
|
||||||
|
if (header.name.endsWith('.svg')) {
|
||||||
|
const file = path.join(baseDir, header.name);
|
||||||
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
||||||
|
await pipeline(stream, fs.createWriteStream(file));
|
||||||
|
}
|
||||||
|
if (header.name.endsWith('.svgz')) {
|
||||||
|
// .svgz -> .svg
|
||||||
|
const file = path.join(baseDir, header.name.slice(0, -1));
|
||||||
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
||||||
|
await pipeline(
|
||||||
|
stream,
|
||||||
|
zlib.createGunzip(),
|
||||||
|
fs.createWriteStream(file)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
stream.resume();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
const response = await fetch(url);
|
||||||
|
await pipeline(response.body, extract);
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
console.info('Download W3C SVG 1.1 Test Suite and extract svg files');
|
||||||
|
await extractTarGz(
|
||||||
|
'https://www.w3.org/Graphics/SVG/Test/20110816/archives/W3C_SVG_11_TestSuite.tar.gz',
|
||||||
|
path.join(__dirname, 'regression-fixtures', 'w3c-svg-11-test-suite'),
|
||||||
|
/^svg\//
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
@ -2,92 +2,12 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const util = require('util');
|
|
||||||
const zlib = require('zlib');
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const stream = require('stream');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const tarStream = require('tar-stream');
|
|
||||||
const getStream = require('get-stream');
|
|
||||||
const { chromium } = require('playwright');
|
const { chromium } = require('playwright');
|
||||||
const { PNG } = require('pngjs');
|
const { PNG } = require('pngjs');
|
||||||
const pixelmatch = require('pixelmatch');
|
const pixelmatch = require('pixelmatch');
|
||||||
const { optimize } = require('../lib/svgo.js');
|
const { optimize } = require('../lib/svgo.js');
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
|
||||||
|
|
||||||
const readSvgFiles = async () => {
|
|
||||||
const cachedArchiveFile = path.join(
|
|
||||||
process.cwd(),
|
|
||||||
'node_modules/.cache/W3C_SVG_11_TestSuite.tar.gz'
|
|
||||||
);
|
|
||||||
const svgFiles = new Map();
|
|
||||||
let fileStream;
|
|
||||||
try {
|
|
||||||
await fs.promises.access(cachedArchiveFile);
|
|
||||||
fileStream = fs.createReadStream(cachedArchiveFile);
|
|
||||||
} catch {
|
|
||||||
const response = await fetch(
|
|
||||||
'https://www.w3.org/Graphics/SVG/Test/20110816/archives/W3C_SVG_11_TestSuite.tar.gz'
|
|
||||||
);
|
|
||||||
fileStream = response.body;
|
|
||||||
fileStream.pipe(fs.createWriteStream(cachedArchiveFile));
|
|
||||||
}
|
|
||||||
const extract = tarStream.extract();
|
|
||||||
extract.on('entry', async (header, stream, next) => {
|
|
||||||
try {
|
|
||||||
if (header.name.startsWith('svg/')) {
|
|
||||||
if (header.name.endsWith('.svg')) {
|
|
||||||
// strip folder and extension
|
|
||||||
const name = header.name.slice('svg/'.length, -'.svg'.length);
|
|
||||||
const string = await getStream(stream);
|
|
||||||
svgFiles.set(name, string);
|
|
||||||
}
|
|
||||||
if (header.name.endsWith('.svgz')) {
|
|
||||||
// strip folder and extension
|
|
||||||
const name = header.name.slice('svg/'.length, -'.svgz'.length);
|
|
||||||
const string = await getStream(stream.pipe(zlib.createGunzip()));
|
|
||||||
svgFiles.set(name, string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
stream.resume();
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
await pipeline(fileStream, extract);
|
|
||||||
return svgFiles;
|
|
||||||
};
|
|
||||||
|
|
||||||
const optimizeSvgFiles = (svgFiles) => {
|
|
||||||
const optimizedFiles = new Map();
|
|
||||||
let failed = 0;
|
|
||||||
for (const [name, string] of svgFiles) {
|
|
||||||
try {
|
|
||||||
const result = optimize(string, { path: name, floatPrecision: 4 });
|
|
||||||
if (result.error) {
|
|
||||||
console.error(result.error);
|
|
||||||
console.error(`File: ${name}`);
|
|
||||||
failed += 1;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
optimizedFiles.set(name, result.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
console.error(`File: ${name}`);
|
|
||||||
failed += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (failed !== 0) {
|
|
||||||
throw Error(`Failed to optimize ${failed} cases`);
|
|
||||||
}
|
|
||||||
return optimizedFiles;
|
|
||||||
};
|
|
||||||
|
|
||||||
const chunkInto = (array, chunksCount) => {
|
const chunkInto = (array, chunksCount) => {
|
||||||
// take upper bound to include tail
|
// take upper bound to include tail
|
||||||
const chunkSize = Math.ceil(array.length / chunksCount);
|
const chunkSize = Math.ceil(array.length / chunksCount);
|
||||||
@ -99,7 +19,7 @@ const chunkInto = (array, chunksCount) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runTests = async ({ svgFiles }) => {
|
const runTests = async ({ list }) => {
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
let mismatched = 0;
|
let mismatched = 0;
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
@ -107,29 +27,31 @@ const runTests = async ({ svgFiles }) => {
|
|||||||
const processFile = async (page, name) => {
|
const processFile = async (page, name) => {
|
||||||
if (
|
if (
|
||||||
// hard to detect the end of animation
|
// hard to detect the end of animation
|
||||||
name.startsWith('animate-') ||
|
name.startsWith('w3c-svg-11-test-suite/svg/animate-') ||
|
||||||
// breaks because of optimisation despite of script
|
// breaks because of optimisation despite of script
|
||||||
name === 'interact-pointer-04-f' ||
|
name === 'w3c-svg-11-test-suite/svg/interact-pointer-04-f.svg' ||
|
||||||
// messed gradients
|
// messed gradients
|
||||||
name === 'pservers-grad-18-b' ||
|
name === 'w3c-svg-11-test-suite/svg/pservers-grad-18-b.svg' ||
|
||||||
// animated filter
|
// animated filter
|
||||||
name === 'filters-light-04-f' ||
|
name === 'w3c-svg-11-test-suite/svg/filters-light-04-f.svg' ||
|
||||||
|
// animated filter
|
||||||
|
name === 'w3c-svg-11-test-suite/svg/filters-composite-05-f.svg' ||
|
||||||
// removing wrapping <g> breaks :first-child pseudo-class
|
// removing wrapping <g> breaks :first-child pseudo-class
|
||||||
name === 'styling-pres-04-f' ||
|
name === 'w3c-svg-11-test-suite/svg/styling-pres-04-f.svg' ||
|
||||||
// messed case insensitivity while inlining styles
|
// messed case insensitivity while inlining styles
|
||||||
name === 'styling-css-10-f' ||
|
name === 'w3c-svg-11-test-suite/svg/styling-css-10-f.svg' ||
|
||||||
// rect is converted to path which matches wrong styles
|
// rect is converted to path which matches wrong styles
|
||||||
name === 'styling-css-08-f' ||
|
name === 'w3c-svg-11-test-suite/svg/styling-css-08-f.svg' ||
|
||||||
// external image
|
// external image
|
||||||
name === 'struct-image-02-b' ||
|
name === 'w3c-svg-11-test-suite/svg/struct-image-02-b.svg' ||
|
||||||
// complex selectors are messed becase of converting shapes to paths
|
// complex selectors are messed becase of converting shapes to paths
|
||||||
name === 'struct-use-10-f' ||
|
name === 'w3c-svg-11-test-suite/svg/struct-use-10-f.svg' ||
|
||||||
name === 'struct-use-11-f' ||
|
name === 'w3c-svg-11-test-suite/svg/struct-use-11-f.svg' ||
|
||||||
name === 'styling-css-01-b' ||
|
name === 'w3c-svg-11-test-suite/svg/styling-css-01-b.svg' ||
|
||||||
name === 'styling-css-03-b' ||
|
name === 'w3c-svg-11-test-suite/svg/styling-css-03-b.svg' ||
|
||||||
name === 'styling-css-04-f' ||
|
name === 'w3c-svg-11-test-suite/svg/styling-css-04-f.svg' ||
|
||||||
// strange artifact breaks inconsistently breaks regression tests
|
// strange artifact breaks inconsistently breaks regression tests
|
||||||
name === 'filters-conv-05-f'
|
name === 'w3c-svg-11-test-suite/svg/filters-conv-05-f.svg'
|
||||||
) {
|
) {
|
||||||
console.info(`${name} is skipped`);
|
console.info(`${name} is skipped`);
|
||||||
skipped += 1;
|
skipped += 1;
|
||||||
@ -166,17 +88,19 @@ const runTests = async ({ svgFiles }) => {
|
|||||||
mismatched += 1;
|
mismatched += 1;
|
||||||
console.error(`${name} is mismatched`);
|
console.error(`${name} is mismatched`);
|
||||||
if (process.env.NO_DIFF == null) {
|
if (process.env.NO_DIFF == null) {
|
||||||
await fs.promises.mkdir('diffs', { recursive: true });
|
const file = path.join(
|
||||||
await fs.promises.writeFile(
|
__dirname,
|
||||||
`diffs/${name}.diff.png`,
|
'regression-diffs',
|
||||||
PNG.sync.write(diff)
|
`${name}.diff.png`
|
||||||
);
|
);
|
||||||
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
||||||
|
await fs.promises.writeFile(file, PNG.sync.write(diff));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const browser = await chromium.launch();
|
const browser = await chromium.launch();
|
||||||
const context = await browser.newContext({ javaScriptEnabled: false });
|
const context = await browser.newContext({ javaScriptEnabled: false });
|
||||||
const chunks = chunkInto(svgFiles, 8);
|
const chunks = chunkInto(list, 8);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
chunks.map(async (chunk) => {
|
chunks.map(async (chunk) => {
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
@ -193,18 +117,60 @@ const runTests = async ({ svgFiles }) => {
|
|||||||
return mismatched === 0;
|
return mismatched === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readdirRecursive = async (absolute, relative = '') => {
|
||||||
|
let result = [];
|
||||||
|
const list = await fs.promises.readdir(absolute, { withFileTypes: true });
|
||||||
|
for (const item of list) {
|
||||||
|
const itemAbsolute = path.join(absolute, item.name);
|
||||||
|
const itemRelative = path.join(relative, item.name);
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
const itemList = await readdirRecursive(itemAbsolute, itemRelative);
|
||||||
|
result = [...result, ...itemList];
|
||||||
|
} else if (item.name.endsWith('.svg')) {
|
||||||
|
result = [...result, itemRelative];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const start = process.hrtime.bigint();
|
const start = process.hrtime.bigint();
|
||||||
console.info('Download W3C SVG 1.1 Test Suite and extract svg files');
|
const fixturesDir = path.join(__dirname, 'regression-fixtures');
|
||||||
const svgFiles = await readSvgFiles();
|
const list = await readdirRecursive(fixturesDir);
|
||||||
const optimizedFiles = optimizeSvgFiles(svgFiles);
|
const originalFiles = new Map();
|
||||||
|
const optimizedFiles = new Map();
|
||||||
|
// read original and optimize
|
||||||
|
let failed = 0;
|
||||||
|
for (const name of list) {
|
||||||
|
try {
|
||||||
|
const file = path.join(fixturesDir, name);
|
||||||
|
const original = await fs.promises.readFile(file, 'utf-8');
|
||||||
|
const result = optimize(original, { path: name, floatPrecision: 4 });
|
||||||
|
if (result.error) {
|
||||||
|
console.error(result.error);
|
||||||
|
console.error(`File: ${name}`);
|
||||||
|
failed += 1;
|
||||||
|
} else {
|
||||||
|
originalFiles.set(name, original);
|
||||||
|
optimizedFiles.set(name, result.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.error(`File: ${name}`);
|
||||||
|
failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failed !== 0) {
|
||||||
|
throw Error(`Failed to optimize ${failed} cases`);
|
||||||
|
}
|
||||||
|
// setup server
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.url.startsWith('/original/')) {
|
if (req.url.startsWith('/original/')) {
|
||||||
const name = req.url.slice('/original/'.length);
|
const name = req.url.slice('/original/'.length);
|
||||||
if (svgFiles.has(name)) {
|
if (originalFiles.has(name)) {
|
||||||
res.setHeader('Content-Type', 'image/svg+xml');
|
res.setHeader('Content-Type', 'image/svg+xml');
|
||||||
res.end(svgFiles.get(name));
|
res.end(originalFiles.get(name));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,8 +188,9 @@ const runTests = async ({ svgFiles }) => {
|
|||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
server.listen(5000, resolve);
|
server.listen(5000, resolve);
|
||||||
});
|
});
|
||||||
const passed = await runTests({ svgFiles: Array.from(svgFiles.keys()) });
|
const passed = await runTests({ list });
|
||||||
server.close();
|
server.close();
|
||||||
|
// compute time
|
||||||
const end = process.hrtime.bigint();
|
const end = process.hrtime.bigint();
|
||||||
const diff = (end - start) / BigInt(1e6);
|
const diff = (end - start) / BigInt(1e6);
|
||||||
if (passed) {
|
if (passed) {
|
||||||
|
Reference in New Issue
Block a user