1
0
mirror of https://github.com/svg/svgo.git synced 2025-04-19 10:22:15 +03:00
svgo/test/regression.js
Bogdan Chadkin d89d36eace
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.
2021-03-25 00:12:13 +03:00

207 lines
6.9 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const http = require('http');
const { chromium } = require('playwright');
const { PNG } = require('pngjs');
const pixelmatch = require('pixelmatch');
const { optimize } = require('../lib/svgo.js');
const chunkInto = (array, chunksCount) => {
// take upper bound to include tail
const chunkSize = Math.ceil(array.length / chunksCount);
const result = [];
for (let i = 0; i < chunksCount; i += 1) {
const offset = i * chunkSize;
result.push(array.slice(offset, offset + chunkSize));
}
return result;
};
const runTests = async ({ list }) => {
let skipped = 0;
let mismatched = 0;
let passed = 0;
console.info('Start browser...');
const processFile = async (page, name) => {
if (
// hard to detect the end of animation
name.startsWith('w3c-svg-11-test-suite/svg/animate-') ||
// breaks because of optimisation despite of script
name === 'w3c-svg-11-test-suite/svg/interact-pointer-04-f.svg' ||
// messed gradients
name === 'w3c-svg-11-test-suite/svg/pservers-grad-18-b.svg' ||
// animated filter
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
name === 'w3c-svg-11-test-suite/svg/styling-pres-04-f.svg' ||
// messed case insensitivity while inlining styles
name === 'w3c-svg-11-test-suite/svg/styling-css-10-f.svg' ||
// rect is converted to path which matches wrong styles
name === 'w3c-svg-11-test-suite/svg/styling-css-08-f.svg' ||
// external image
name === 'w3c-svg-11-test-suite/svg/struct-image-02-b.svg' ||
// complex selectors are messed becase of converting shapes to paths
name === 'w3c-svg-11-test-suite/svg/struct-use-10-f.svg' ||
name === 'w3c-svg-11-test-suite/svg/struct-use-11-f.svg' ||
name === 'w3c-svg-11-test-suite/svg/styling-css-01-b.svg' ||
name === 'w3c-svg-11-test-suite/svg/styling-css-03-b.svg' ||
name === 'w3c-svg-11-test-suite/svg/styling-css-04-f.svg' ||
// strange artifact breaks inconsistently breaks regression tests
name === 'w3c-svg-11-test-suite/svg/filters-conv-05-f.svg'
) {
console.info(`${name} is skipped`);
skipped += 1;
return;
}
const width = 960;
const height = 720;
await page.goto(`http://localhost:5000/original/${name}`);
await page.setViewportSize({ width, height });
const originalBuffer = await page.screenshot({
omitBackground: true,
clip: { x: 0, y: 0, width, height },
});
await page.goto(`http://localhost:5000/optimized/${name}`);
const optimizedBuffer = await page.screenshot({
omitBackground: true,
clip: { x: 0, y: 0, width, height },
});
const originalPng = PNG.sync.read(originalBuffer);
const optimizedPng = PNG.sync.read(optimizedBuffer);
const diff = new PNG({ width, height });
const matched = pixelmatch(
originalPng.data,
optimizedPng.data,
diff.data,
width,
height
);
// ignore small aliasing issues
if (matched <= 4) {
console.info(`${name} is passed`);
passed += 1;
} else {
mismatched += 1;
console.error(`${name} is mismatched`);
if (process.env.NO_DIFF == null) {
const file = path.join(
__dirname,
'regression-diffs',
`${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 context = await browser.newContext({ javaScriptEnabled: false });
const chunks = chunkInto(list, 8);
await Promise.all(
chunks.map(async (chunk) => {
const page = await context.newPage();
for (const name of chunk) {
await processFile(page, name);
}
await page.close();
})
);
await browser.close();
console.info(`Skipped: ${skipped}`);
console.info(`Mismatched: ${mismatched}`);
console.info(`Passed: ${passed}`);
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 () => {
try {
const start = process.hrtime.bigint();
const fixturesDir = path.join(__dirname, 'regression-fixtures');
const list = await readdirRecursive(fixturesDir);
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) => {
if (req.url.startsWith('/original/')) {
const name = req.url.slice('/original/'.length);
if (originalFiles.has(name)) {
res.setHeader('Content-Type', 'image/svg+xml');
res.end(originalFiles.get(name));
return;
}
}
if (req.url.startsWith('/optimized/')) {
const name = req.url.slice('/optimized/'.length);
if (optimizedFiles.has(name)) {
res.setHeader('Content-Type', 'image/svg+xml');
res.end(optimizedFiles.get(name));
return;
}
}
res.statusCode = 404;
res.end();
});
await new Promise((resolve) => {
server.listen(5000, resolve);
});
const passed = await runTests({ list });
server.close();
// compute time
const end = process.hrtime.bigint();
const diff = (end - start) / BigInt(1e6);
if (passed) {
console.info(`Regression tests successfully completed in ${diff}ms`);
} else {
console.error(`Regression tests failed in ${diff}ms`);
process.exit(1);
}
} catch (error) {
console.error(error);
process.exit(1);
}
})();