未验证 提交 5826ca55 编写于 作者: Y Yi Shen 提交者: GitHub

Merge pull request #14862 from apache/enhance-visual-regression-test

Enhance visual regression test
......@@ -45,7 +45,7 @@ under the License.
var gexf = dataTool.gexf;
var chart = echarts.init(document.getElementById('main');
var chart = echarts.init(document.getElementById('main'));
$.get('./data/les-miserables.gexf', function (xml) {
var graph = gexf.parse(xml);
......
......@@ -115,20 +115,7 @@ under the License.
padding: 10,
backgroundColor: '#222',
borderColor: '#777',
borderWidth: 1,
formatter: function (obj) {
var value = obj[0].value;
return '<div style="border-bottom: 1px solid rgba(255,255,255,.3); font-size: 18px;padding-bottom: 7px;margin-bottom: 7px">'
+ obj[0].seriesName + ' ' + value[0] + '日期:'
+ value[7]
+ '</div>'
+ schema[1].text + '' + value[1] + '<br>'
+ schema[2].text + '' + value[2] + '<br>'
+ schema[3].text + '' + value[3] + '<br>'
+ schema[4].text + '' + value[4] + '<br>'
+ schema[5].text + '' + value[5] + '<br>'
+ schema[6].text + '' + value[6] + '<br>';
}
borderWidth: 1
},
visualMap: {
show: true,
......
......@@ -157,17 +157,7 @@ under the License.
padding: 10,
backgroundColor: '#222',
borderColor: '#777',
borderWidth: 1,
formatter: function (obj) {
var value = obj[0].value;
return '<div style="border-bottom: 1px solid rgba(255,255,255,.3); font-size: 18px;padding-bottom: 7px;margin-bottom: 7px">'
+ schema[1].name + '' + value[1] + '<br>'
+ schema[2].name + '' + value[2] + '<br>'
+ schema[3].name + '' + value[3] + '<br>'
+ schema[4].name + '' + value[4] + '<br>'
+ schema[5].name + '' + value[5] + '<br>'
+ schema[6].name + '' + value[6] + '<br>';
}
borderWidth: 1
},
title: [
{
......
......@@ -38,7 +38,8 @@ program
.option('--expected <expected>', 'Expected version')
.option('--actual <actual>', 'Actual version')
.option('--renderer <renderer>', 'svg/canvas renderer')
.option('--no-save', 'Don\'t save result');
.option('--no-save', 'Don\'t save result')
.option('--dir <dir>', 'Out dir');
program.parse(process.argv);
......@@ -46,13 +47,14 @@ program.speed = +program.speed || 1;
program.actual = program.actual || 'local';
program.expected = program.expected || '4.2.1';
program.renderer = (program.renderer || 'canvas').toLowerCase();
program.dir = program.dir || (__dirname + '/tmp');
if (!program.tests) {
throw new Error('Tests are required');
}
function getScreenshotDir() {
return 'tmp/__screenshot__';
return `${program.dir}/__screenshot__`;
}
function sortScreenshots(list) {
......@@ -98,9 +100,6 @@ async function convertToWebP(filePath, lossless) {
async function takeScreenshot(page, fullPage, fileUrl, desc, isExpected, minor) {
let screenshotName = testNameFromFile(fileUrl);
if (program.renderer === 'svg') {
screenshotName += '-_svg_render_';
}
if (desc) {
screenshotName += '-' + slugify(desc, { replacement: '-', lower: true });
}
......@@ -108,8 +107,8 @@ async function takeScreenshot(page, fullPage, fileUrl, desc, isExpected, minor)
screenshotName += '-' + minor;
}
let screenshotPrefix = isExpected ? 'expected' : 'actual';
fse.ensureDirSync(path.join(__dirname, getScreenshotDir()));
let screenshotPath = path.join(__dirname, `${getScreenshotDir()}/${screenshotName}-${screenshotPrefix}.png`);
fse.ensureDirSync(getScreenshotDir());
let screenshotPath = path.join(getScreenshotDir(), `${screenshotName}-${screenshotPrefix}.png`);
await page.screenshot({
path: screenshotPath,
fullPage
......@@ -277,7 +276,7 @@ async function runTest(browser, testOpt, runtimeCode, expectedVersion, actualVer
actual.rawScreenshotPath
);
const diffPath = `${path.resolve(__dirname, getScreenshotDir())}/${shot.screenshotName}-diff.png`;
const diffPath = `${getScreenshotDir()}/${shot.screenshotName}-diff.png`;
await writePNG(diffPNG, diffPath);
const diffWebpPath = await convertToWebP(diffPath);
......@@ -328,6 +327,10 @@ async function runTests(pendingTests) {
let runtimeCode = await buildRuntimeCode();
runtimeCode = `window.__TEST_PLAYBACK_SPEED__ = ${program.speed || 1};\n${runtimeCode}`;
process.on('exit', () => {
browser.close();
});
try {
for (let testOpt of pendingTests) {
console.log(`Running test: ${testOpt.name}, renderer: ${program.renderer}`);
......
......@@ -30,17 +30,22 @@
}
.header {
background-color: #293c55;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
background-color: #fff;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 10;
z-index: 20;
height: 55px;
}
.header>* {
display: inline-block;
vertical-align: middle;
}
.header h1 {
color: #fff;
color: #222;
line-height: 50px;
margin: 0;
font-weight: 200;
font-size: 20px;
}
......@@ -54,65 +59,111 @@
margin-right: 20px;
}
.nav-toolbar {
.el-aside {
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 10;
}
.el-main {
background: #f3f4fa;
padding: 0;
}
.nav-toolbar, .test-run-controls {
padding: 10px 10px;
background: #162436;
box-shadow: inset 0 0 5px black;
background: #fff;
position: fixed;
top: 50px;
width: 330px;
z-index: 2;
/* box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); */
border-bottom: 1px solid #eee;
}
.nav-toolbar .controls {
margin-top: 10px;
.test-run-controls {
z-index: 1;
position: sticky;
width: 100%;
padding: 5px 40px;
top: 0px;
background: #896bda;
box-shadow: 0 0 20px rgb(0 0 0 / 20%);
border-bottom: none;
}
.nav-toolbar .controls>* {
.test-run-controls>* {
display: inline-block;
vertical-align: middle;
}
.nav-toolbar .controls .el-checkbox {
margin-right: 2px;
}
.nav-toolbar .el-icon-setting {
color: #f3f3f3;
font-size: 20px;
margin-left: 5px;
cursor: pointer;
}
.nav-toolbar .el-button {
padding-left: 8px;
padding-right: 8px;
}
.run-config-item {
margin: 5px 0;
margin: 0 5px;
color: #fff;
font-size: 12px;
}
.run-config-item>* {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
font-size: 12px;
color: #fff;
}
.run-config-item .el-progress-circle__track {
stroke: rgba(255, 255, 255, 0.3)!important;
}
.run-config-item .el-progress-circle__path {
stroke: rgba(255, 255, 255, 1)!important;
}
.run-config-item .el-input__inner,
.run-config-item .el-button-group {
border: none;
}
.run-config-item .el-button {
border-color: #fff;
}
.run-config-item .label {
margin-right: 2px;
margin-left: 5px;
text-transform: uppercase;
}
.test-list {
overflow-x: hidden;
background: #293c55;
background: #fff;
margin: 0;
padding: 0;
margin-top: 80px;
position: absolute;
top: 48px;
bottom: 0;
right: 0;
left: 0;
overflow-y: scroll;
}
.test-list li {
list-style: none;
padding-left: 10px;
cursor: pointer;
color: #f3f3f3;
color: #222;
}
.test-list li .el-checkbox {
margin-right: 5px;
}
.test-list li a.menu-link {
display: inline-block;
text-decoration: none;
font-size: 14px;
line-height: 40px;
color: #f3f3f3;
color: #222;
margin-left: 3px;
cursor: pointer;
}
......@@ -122,10 +173,13 @@
}
.test-list li.active {
background: #e43c59;
background: #5470C6;
}
.test-list li.active a {
color: #fff;
}
.test-list li:hover {
background: #162436;
border-right: 4px solid #5470C6
}
.test-list li>* {
vertical-align: middle;
......@@ -136,6 +190,27 @@
font-size: 12px!important;
}
.el-progress.is-success .el-progress__text {
color: #67C23A;
-webkit-text-stroke: 1px #67C23A;
}
.el-progress.is-exception .el-progress__text {
color: #F56C6C;
-webkit-text-stroke: 1px #F56C6C;
}
.no-result {
text-align: center;
font-size: 30px;
padding: 100px 0;
color: #ccc;
}
.test-result {
padding: 20px;
margin-top: 20px;
}
.test-result .el-progress__text {
font-size: 14px!important;
}
......@@ -147,6 +222,9 @@
margin: 0;
}
.test-result .title {
margin-left: 20px;
}
.test-result .title>* {
display: inline-block;
vertical-align: middle;
......@@ -161,6 +239,13 @@
text-decoration: underline;
}
.single-test-ops {
padding: 20px 20px 0 10px;
}
.single-test-ops .el-button {
margin-left: 10px;
}
.test-screenshots {
margin-top: 20px;
padding: 0 20px;
......@@ -173,6 +258,8 @@
.test-screenshots .preview {
cursor: pointer;
color: #409eff;
float: right;
font-size: 20px;
}
.test-screenshots .preview:hover {
text-decoration: underline;
......@@ -185,13 +272,12 @@
.test-screenshots h4 {
font-size: 30px;
font-weight: 200;
margin-left: -20px;
color: #162436;
}
.test-errors, .test-logs {
margin-top: 20px;
padding: 0 50px;
padding: 0 20px;
}
.test-logs .log-item {
......@@ -209,6 +295,11 @@ iframe {
overflow: overlay;
}
#tests-runs-dialog .el-dialog {
width: 90%;
max-width: 1200px;
}
::-webkit-scrollbar {
height: 8px;
......
......@@ -18,7 +18,40 @@
*/
const socket = io('/client');
const LOCAL_SAVE_KEY = 'visual-regression-testing-config';
// const LOCAL_SAVE_KEY = 'visual-regression-testing-config';
function getChangedObject(target, source) {
let changedObject = {};
Object.keys(source).forEach(key => {
if (target[key] !== source[key]) {
changedObject[key] = source[key];
}
});
return changedObject;
}
function parseParams(str) {
if (!str) {
return {};
}
const parts = str.split('&');
const params = {};
parts.forEach((part) => {
const kv = part.split('=');
params[kv[0]] = decodeURIComponent(kv[1]);
});
return params;
}
function assembleParams(paramsObj) {
const paramsArr = [];
Object.keys(paramsObj).forEach((key) => {
let val = paramsObj[key];
paramsArr.push(key + '=' + encodeURIComponent(val));
});
return paramsArr.join('&');
}
function processTestsData(tests, oldTestsData) {
tests.forEach((test, idx) => {
......@@ -45,6 +78,8 @@ function processTestsData(tests, oldTestsData) {
test.summary = 'warning';
}
// To simplify the condition in sort
test.actualErrors = test.actualErrors || [];
// Keep select status not change.
if (oldTestsData && oldTestsData[idx]) {
test.selected = oldTestsData[idx].selected;
......@@ -56,52 +91,113 @@ function processTestsData(tests, oldTestsData) {
return tests;
}
const urlRunConfig = {};
const urlParams = parseParams(window.location.search.substr(1))
// Save and restore
try {
const runConfig = JSON.parse(urlParams.runConfig);
Object.assign(urlRunConfig, runConfig);
}
catch (e) {}
const app = new Vue({
el: '#app',
data: {
fullTests: [],
currentTestName: '',
sortBy: 'name',
currentTestName: urlParams.test || '',
searchString: '',
running: false,
allSelected: false,
lastSelectedIndex: -1,
versions: [],
expectedVersionsList: [],
actualVersionsList: [],
loadingVersion: false,
showIframeDialog: false,
previewIframeSrc: '',
previewTitle: '',
runConfig: {
noHeadless: false,
replaySpeed: 5,
// List of all runs.
showRunsDialog: false,
testsRuns: [],
pageInvisible: false,
runConfig: Object.assign({
sortBy: 'name',
// replaySpeed: 5,
isActualNightly: false,
isExpectedNightly: false,
actualVersion: 'local',
expectedVersion: null,
renderer: 'canvas',
threads: 1
}
threads: 4
}, urlRunConfig)
},
mounted() {
// Sync config from server when first time open
// or switching back
socket.emit('syncRunConfig', {
runConfig: this.runConfig,
// Override server config from URL.
forceSet: Object.keys(urlRunConfig).length > 0
});
socket.on('syncRunConfig_return', res => {
this.expectedVersionsList = res.expectedVersionsList;
this.actualVersionsList = res.actualVersionsList;
// Only assign on changed object to avoid unnecessary vue change.
Object.assign(this.runConfig, getChangedObject(this.runConfig, res.runConfig));
updateUrl();
});
setTimeout(() => {
this.scrollToCurrent();
}, 500);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === 'visible') {
this.pageInvisible = false;
socket.emit('syncRunConfig', {});
}
else {
this.pageInvisible = true;
}
});
},
computed: {
finishedPercentage() {
let finishedCount = 0;
this.fullTests.forEach(test => {
if (test.status === 'finished') {
finishedCount++;
}
});
return +(finishedCount / this.fullTests.length * 100).toFixed(0) || 0;
},
tests() {
let sortFunc = this.sortBy === 'name'
let sortFunc = this.runConfig.sortBy === 'name'
? (a, b) => a.name.localeCompare(b.name)
: (a, b) => {
if (a.percentage === b.percentage) {
if (a.actualErrors && b.actualErrors) {
if (a.actualErrors.length === b.actualErrors.length) {
return a.name.localeCompare(b.name);
}
else {
return b.actualErrors.length - a.actualErrors.length;
}
if (a.actualErrors.length === b.actualErrors.length) {
if (a.percentage === b.percentage) {
return a.name.localeCompare(b.name);
}
else {
return a.name.localeCompare(b.name);
return a.percentage - b.percentage;
}
}
return a.percentage - b.percentage;
return b.actualErrors.length - a.actualErrors.length;
};
if (!this.searchString) {
......@@ -116,7 +212,8 @@ const app = new Vue({
},
selectedTests() {
return this.fullTests.filter(test => {
// Only run visible tests.
return this.tests.filter(test => {
return test.selected;
});
},
......@@ -159,12 +256,34 @@ const app = new Vue({
set() {}
}
},
watch: {
'runConfig.sortBy'() {
setTimeout(() => {
this.scrollToCurrent();
}, 100);
}
},
methods: {
goto(url) {
window.location.hash = '#' + url;
scrollToCurrent() {
const el = document.querySelector(`.test-list>li[title="${this.currentTestName}"]`);
if (el) {
el.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
},
changeTest(target, testName) {
if (!target.matches('input[type="checkbox"]') && !target.matches('.el-checkbox__inner')) {
app.currentTestName = testName;
updateUrl();
}
},
toggleSort() {
this.sortBy = this.sortBy === 'name' ? 'percentage' : 'name';
this.runConfig.sortBy = this.runConfig.sortBy === 'name' ? 'percentage' : 'name';
},
handleSelectAllChange(val) {
// Only select filtered tests.
......@@ -189,8 +308,8 @@ const app = new Vue({
this.tests[i].selected = selected;
}
},
runSingleTest(testName) {
runTests([testName]);
runSingleTest(testName, noHeadless) {
runTests([testName], noHeadless);
},
run(runTarget) {
let tests;
......@@ -206,7 +325,7 @@ const app = new Vue({
else {
tests = this.fullTests;
}
runTests(tests.map(test => test.name));
runTests(tests.map(test => test.name), false);
},
stopTests() {
this.running = false;
......@@ -230,20 +349,51 @@ const app = new Vue({
this.previewIframeSrc = `../../${src}`;
this.previewTitle = src;
this.showIframeDialog = true;
},
showAllTestsRuns() {
this.showRunsDialog = true;
socket.emit('getAllTestsRuns');
},
switchTestsRun(runResult) {
this.runConfig.expectedVersion = runResult.expectedVersion;
this.runConfig.actualVersion = runResult.actualVersion;
// TODO
this.runConfig.isExpectedNightly = runResult.expectedVersion.includes('-dev.');
this.runConfig.isActualNightly = runResult.actualVersion.includes('-dev.');
this.runConfig.renderer = runResult.renderer;
this.showRunsDialog = false;
},
genTestsRunReport(runResult) {
socket.emit('genTestsRunReport', runResult);
},
delTestsRun(runResult) {
app.$confirm('Are you sure to delete this run?', 'Warn', {
confirmButtonText: 'Yes',
cancelButtonText: 'No',
center: true
}).then(value => {
const idx = this.testsRuns.indexOf(runResult);
if (idx >= 0) {
this.testsRuns.splice(idx, 1);
}
socket.emit('delTestsRun', {
id: runResult.id
});
}).catch(() => {});
},
open(url, target) {
window.open(url, target);
}
}
});
// Save and restore
try {
Object.assign(app.runConfig, JSON.parse(localStorage.getItem(LOCAL_SAVE_KEY)));
}
catch (e) {}
app.$watch('runConfig', () => {
localStorage.setItem(LOCAL_SAVE_KEY, JSON.stringify(app.runConfig));
}, {deep: true});
function runTests(tests) {
function runTests(tests, noHeadless) {
if (!tests.length) {
app.$notify({
title: 'No test selected.',
......@@ -265,9 +415,9 @@ function runTests(tests) {
actualVersion: app.runConfig.actualVersion,
threads: app.runConfig.threads,
renderer: app.runConfig.renderer,
noHeadless: app.runConfig.noHeadless,
noHeadless,
replaySpeed: app.runConfig.noHeadless
? app.runConfig.replaySpeed
? 1
: 5 // Force run at 5x speed
});
}
......@@ -275,23 +425,24 @@ function runTests(tests) {
socket.on('connect', () => {
console.log('Connected');
app.$el.style.display = 'block';
});
let firstUpdate = true;
socket.on('update', msg => {
let hasFinishedTest = !!msg.tests.find(test => test.status === 'finished');
if (!hasFinishedTest && firstUpdate) {
app.$confirm('It seems you haven\'t run any test yet!<br />Do you want to start now?', 'Tip', {
confirmButtonText: 'Yes',
cancelButtonText: 'No',
dangerouslyUseHTMLString: true,
center: true
}).then(value => {
runTests(msg.tests.map(test => test.name));
}).catch(() => {});
}
app.$el.style.display = 'block';
// let hasFinishedTest = !!msg.tests.find(test => test.status === 'finished');
// if (!hasFinishedTest && firstUpdate) {
// app.$confirm('You haven\'t run any test on these two versions yet!<br />Do you want to start now?', 'Tip', {
// confirmButtonText: 'Yes',
// cancelButtonText: 'No',
// dangerouslyUseHTMLString: true,
// center: true
// }).then(value => {
// runTests(msg.tests.map(test => test.name));
// }).catch(() => {});
// }
// TODO
app.running = !!msg.running;
app.fullTests = processTestsData(msg.tests, app.fullTests);
......@@ -317,18 +468,29 @@ socket.on('abort', res => {
});
app.running = false;
});
socket.on('versions', versions => {
app.versions = versions.filter(version => {
return !version.startsWith('2.');
}).reverse();
if (!app.runConfig.expectedVersion) {
app.runConfig.expectedVersion = app.versions[0];
}
app.versions.unshift('local');
socket.on('getAllTestsRuns_return', res => {
app.testsRuns = res.runs;
});
socket.on('genTestsRunReport_return', res => {
window.open(res.reportUrl, '_blank');
});
function updateTestHash() {
app.currentTestName = window.location.hash.slice(1);
function updateUrl() {
const searchUrl = assembleParams({
test: app.currentTestName,
runConfig: JSON.stringify(app.runConfig)
});
history.pushState({}, '', location.pathname + '?' + searchUrl);
}
updateTestHash();
window.addEventListener('hashchange', updateTestHash);
\ No newline at end of file
// Only update url when version is changed.
app.$watch('runConfig', (newVal, oldVal) => {
if (!app.pageInvisible) {
socket.emit('syncRunConfig', {
runConfig: app.runConfig,
// Override server config from URL.
forceSet: true
});
}
}, { deep: true });
\ No newline at end of file
此差异已折叠。
......@@ -21,17 +21,10 @@
const fs = require('fs');
const path = require('path');
const util = require('util');
// const jimp = require('jimp');
const marked = require('marked');
const tests = JSON.parse(fs.readFileSync(
path.join(__dirname, 'tmp/__cache__.json'), 'utf-8'
));
const { RESULT_FILE_NAME } = require('./store');
const readFileAsync = util.promisify(fs.readFile);
function resolveImagePath(imageUrl) {
if (!imageUrl) {
return '';
......@@ -108,10 +101,15 @@ async function genDetail(test) {
};
}
async function run() {
module.exports = async function(testDir) {
let sections = [];
let failedTest = 0;
const tests = JSON.parse(fs.readFileSync(
path.join(testDir, RESULT_FILE_NAME), 'utf-8'
));
for (let test of tests) {
let detail = await genDetail(test);
......@@ -119,20 +117,12 @@ async function run() {
failedTest++;
let title = `${failedTest}. ${test.name} (Failed ${detail.failed} / ${detail.total})`;
console.log(title);
// let sectionText = `
// ## ${title}
// <details>
// <summary>Click to expand!</summary>
// ${detail.content}
// </details>
// `;
let sectionText = `
<div style="margin-top: 100px;height: 20px;border-top: 1px solid #aaa"></div>
<a id="${test.name}"></a>
## ${title}
<h2>${title}</h2>
${detail.content}
`;
......@@ -145,22 +135,18 @@ ${detail.content}
}
}
let mdText = '# Visual Regression Test Report\n\n';
mdText += `
let htmlText = '<h1> Visual Regression Test Report</h1>\n';
htmlText += `
<p>Total: ${tests.length}</p>
<p>Failed: ${failedTest}</p>
`;
mdText += sections.map(section => {
return `+ [${section.title}](#${section.id}) `;
}).join('\n');
mdText += sections.map(section => section.content).join('\n\n');
fs.writeFileSync(__dirname + '/tmp-report.md', mdText, 'utf-8');
marked(mdText, { smartLists: true }, (err, res) => {
fs.writeFileSync(__dirname + '/tmp-report.html', res, 'utf-8');
});
htmlText += '<ul>\n' + sections.map(section => {
return `<li><a href="${section.id}">${section.title}</a></li>`;
}).join('\n') + '</ul>';
htmlText += sections.map(section => section.content).join('\n\n');
const file = path.join(testDir, 'report.html');
fs.writeFileSync(file, htmlText, 'utf-8');
return file;
}
run();
......@@ -24,11 +24,24 @@ const path = require('path');
const {fork} = require('child_process');
const semver = require('semver');
const {port, origin} = require('./config');
const {getTestsList, updateTestsList, saveTestsList, mergeTestsResults, updateActionsMeta} = require('./store');
const {
getTestsList,
updateTestsList,
saveTestsList,
mergeTestsResults,
updateActionsMeta,
getResultBaseDir,
getRunHash,
getAllTestsRuns,
delTestsRun,
RESULTS_ROOT_DIR,
checkStoreVersion
} = require('./store');
const {prepareEChartsLib, getActionsFullPath, fetchVersions} = require('./util');
const fse = require('fs-extra');
const fs = require('fs');
const open = require('open');
const genReport = require('./genReport');
function serve() {
const server = http.createServer((request, response) => {
......@@ -52,7 +65,7 @@ function serve() {
let runningThreads = [];
let pendingTests;
let aborted = false;
let running = false;
function stopRunningTests() {
if (runningThreads) {
......@@ -65,6 +78,7 @@ function stopRunningTests() {
testOpt.status = 'unsettled';
}
});
saveTestsList();
pendingTests = null;
}
}
......@@ -130,11 +144,17 @@ function startTests(testsNameList, socket, {
testOpt.status = 'pending';
testOpt.results = [];
});
// Save status immediately
saveTestsList();
if (!aborted) {
socket.emit('update', {tests: getTestsList(), running: true});
if (running) {
socket.emit('update', {
tests: getTestsList(),
running: true
});
}
}
let runningCount = 0;
function onExit() {
runningCount--;
......@@ -145,8 +165,11 @@ function startTests(testsNameList, socket, {
}
function onUpdate() {
// Merge tests.
if (!aborted && !noSave) {
socket.emit('update', {tests: getTestsList(), running: true});
if (running && !noSave) {
socket.emit('update', {
tests: getTestsList(),
running: true
});
}
}
threadsCount = Math.min(threadsCount, pendingTests.length);
......@@ -163,6 +186,7 @@ function startTests(testsNameList, socket, {
'--actual', actualVersion,
'--expected', expectedVersion,
'--renderer', renderer || '',
'--dir', getResultBaseDir(),
...(noHeadless ? ['--no-headless'] : []),
...(noSave ? ['--no-save'] : [])
]);
......@@ -186,6 +210,7 @@ function checkPuppeteer() {
}
}
async function start() {
if (!checkPuppeteer()) {
// TODO Check version.
......@@ -193,10 +218,9 @@ async function start() {
return;
}
let [versions] = await Promise.all([
fetchVersions(),
updateTestsList(true)
]);
let _currentTestHash;
let _currentRunConfig;
// let runtimeCode = await buildRuntimeCode();
// fse.outputFileSync(path.join(__dirname, 'tmp/testRuntime.js'), runtimeCode, 'utf-8');
......@@ -204,43 +228,103 @@ async function start() {
// Start a static server for puppeteer open the html test cases.
let {io} = serve();
io.of('/client').on('connect', async socket => {
await updateTestsList();
const stableVersions = await fetchVersions(false);
const nightlyVersions = await fetchVersions(true);
stableVersions.unshift('local');
nightlyVersions.unshift('local');
io.of('/client').on('connect', async socket => {
function abortTests() {
if (!running) {
return;
}
stopRunningTests();
io.of('/client').emit('abort');
aborted = true;
running = false;
}
function emitUpdatedList() {
socket.on('syncRunConfig', async ({
runConfig,
forceSet
}) => {
// First time open.
if ((!_currentRunConfig || forceSet) && runConfig) {
_currentRunConfig = runConfig;
}
if (!_currentRunConfig) {
return;
}
const expectedVersionsList = _currentRunConfig.isExpectedNightly ? nightlyVersions : stableVersions;
const actualVersionsList = _currentRunConfig.isActualNightly ? nightlyVersions : stableVersions;
if (!expectedVersionsList.includes(_currentRunConfig.expectedVersion)) {
// Pick first version not local
_currentRunConfig.expectedVersion = expectedVersionsList[1];
}
if (!actualVersionsList.includes(_currentRunConfig.actualVersion)) {
_currentRunConfig.actualVersion = 'local';
}
socket.emit('syncRunConfig_return', {
runConfig: _currentRunConfig,
expectedVersionsList,
actualVersionsList
});
if (_currentTestHash !== getRunHash(_currentRunConfig)) {
abortTests();
}
await updateTestsList(
_currentTestHash = getRunHash(_currentRunConfig),
!running // Set to unsettled if not running
);
socket.emit('update', {
tests: getTestsList(),
running: runningThreads.length > 0
});
}
});
socket.on('getAllTestsRuns', async () => {
socket.emit('getAllTestsRuns_return', {
runs: await getAllTestsRuns()
});
});
emitUpdatedList();
socket.on('genTestsRunReport', async (params) => {
const absPath = await genReport(
path.join(RESULTS_ROOT_DIR, getRunHash(params))
);
const relativeUrl = path.join('../', path.relative(__dirname, absPath));
socket.emit('genTestsRunReport_return', {
reportUrl: relativeUrl
});
});
socket.on('fetch', () => {
abortTests();
emitUpdatedList();
socket.on('delTestsRun', async (params) => {
delTestsRun(params.id);
console.log('Deleted', params.id);
});
socket.on('run', async data => {
let startTime = Date.now();
aborted = false;
running = true;
await prepareEChartsLib(data.expectedVersion); // Expected version.
await prepareEChartsLib(data.actualVersion); // Version to test
if (aborted) { // If it is aborted when downloading echarts lib.
if (!running) { // If it is aborted when downloading echarts lib.
return;
}
// TODO Should broadcast to all sockets.
try {
if (!checkStoreVersion(data)) {
throw new Error('Unmatched store version and run version.');
}
await startTests(
data.tests,
io.of('/client'),
......@@ -259,13 +343,14 @@ async function start() {
console.error(e);
}
if (!aborted) {
if (running) {
console.log('Finished');
io.of('/client').emit('finish', {
time: Date.now() - startTime,
count: data.tests.length,
threads: data.threads
});
running = false;
}
else {
console.log('Aborted!');
......@@ -273,8 +358,6 @@ async function start() {
});
socket.on('stop', abortTests);
socket.emit('versions', versions);
});
io.of('/recorder').on('connect', async socket => {
......
......@@ -20,14 +20,30 @@
const path = require('path');
const fse = require('fs-extra');
const fs = require('fs');
const glob = require('glob');
const globby = require('globby');
const {testNameFromFile} = require('./util');
const util = require('util');
const {blacklist, SVGBlacklist} = require('./blacklist');
let _tests = [];
let _testsMap = {};
let _runHash = '';
const RESULT_FILE_NAME = '__result__.json';
const RESULTS_ROOT_DIR = path.join(__dirname, 'tmp', 'result');
module.exports.RESULT_FILE_NAME = RESULT_FILE_NAME;
module.exports.RESULTS_ROOT_DIR = RESULTS_ROOT_DIR;
const TEST_HASH_SPLITTER = '__';
function convertBytes(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
if (bytes == 0) {
return 'N/A';
}
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(Math.min(1, i)) + ' ' + sizes[i]
}
class Test {
constructor(fileUrl) {
this.fileUrl = fileUrl;
......@@ -58,10 +74,55 @@ class Test {
}
}
function getCacheFilePath() {
return path.join(__dirname, 'tmp/__cache__.json');;
/**
* hash of each run is mainly for storing the results.
* It depends on two versions and rendering mode.
*/
function getRunHash(params) {
return [
params.expectedVersion,
params.actualVersion,
params.renderer
].join(TEST_HASH_SPLITTER);
}
/**
* Parse versions and rendering mode from run hash.
*/
function parseRunHash(str) {
const parts = str.split(TEST_HASH_SPLITTER);
return {
expectedVersion: parts[0],
actualVersion: parts[1],
renderer: parts[2]
};
}
function getResultBaseDir() {
return path.join(RESULTS_ROOT_DIR, _runHash);
}
module.exports.getResultBaseDir = getResultBaseDir;
module.exports.getRunHash = getRunHash;
/**
* Check run version is same with store version.
*/
module.exports.checkStoreVersion = function (runParams) {
const storeParams = parseRunHash(_runHash);
console.log('Store ', _runHash);
return storeParams.expectedVersion === runParams.expectedVersion
&& storeParams.actualVersion === runParams.actualVersion
&& storeParams.renderer === runParams.renderer;
}
function getResultFilePath() {
return path.join(getResultBaseDir(), RESULT_FILE_NAME);
}
module.exports.getResultFilePath = getResultFilePath;
module.exports.getTestsList = function () {
return _tests;
};
......@@ -70,15 +131,21 @@ module.exports.getTestByFileUrl = function (url) {
return _testsMap[url];
};
module.exports.updateTestsList = async function (setPendingTestToUnsettled) {
let tmpFolder = path.join(__dirname, 'tmp');
fse.ensureDirSync(tmpFolder);
module.exports.updateTestsList = async function (
runHash,
setPendingTestToUnsettled
) {
_runHash = runHash;
_tests = [];
_testsMap = {};
_testsExists = {};
fse.ensureDirSync(getResultBaseDir());
try {
let cachedStr = fs.readFileSync(getCacheFilePath(), 'utf-8');
_tests = JSON.parse(cachedStr);
_tests.forEach(test => {
let cachedStr = fs.readFileSync(getResultFilePath(), 'utf-8');
const tests = JSON.parse(cachedStr);
tests.forEach(test => {
// In somehow tests are stopped and leave the status pending.
// Set the status to unsettled again.
if (setPendingTestToUnsettled) {
......@@ -90,14 +157,15 @@ module.exports.updateTestsList = async function (setPendingTestToUnsettled) {
});
}
catch(e) {
_tests = [];
}
// Find if there is new html file
const files = await util.promisify(glob)('**.html', { cwd: path.resolve(__dirname, '../') });
const files = await globby('*.html', { cwd: path.resolve(__dirname, '../') });
files.forEach(fileUrl => {
if (blacklist.includes(fileUrl)) {
return;
}
_testsExists[fileUrl] = true;
if (_testsMap[fileUrl]) {
return;
}
......@@ -105,10 +173,14 @@ module.exports.updateTestsList = async function (setPendingTestToUnsettled) {
const test = new Test(fileUrl);
test.ignoreSVG = SVGBlacklist.includes(fileUrl);
_tests.push(test);
_testsMap[fileUrl] = test;
});
// Exclude tests that there is no HTML files.
Object.keys(_testsExists).forEach(key => {
_tests.push(_testsMap[key]);
});
const actionsMetaData = {};
const metaPath = path.join(__dirname, 'actions/__meta__.json');
try {
......@@ -119,11 +191,15 @@ module.exports.updateTestsList = async function (setPendingTestToUnsettled) {
_tests.forEach(testOpt => {
testOpt.actions = actionsMetaData[testOpt.name] || 0;
});
// Save once.
module.exports.saveTestsList();
return _tests;
};
module.exports.saveTestsList = function () {
fse.outputFileSync(getCacheFilePath(), JSON.stringify(_tests, null, 2), 'utf-8');
fse.outputFileSync(getResultFilePath(), JSON.stringify(_tests, null, 2), 'utf-8');
};
module.exports.mergeTestsResults = function (testsResults) {
......@@ -147,4 +223,89 @@ module.exports.updateActionsMeta = function (testName, actions) {
fs.writeFileSync(metaPath, JSON.stringify(
metaData, Object.keys(metaData).sort((a, b) => a.localeCompare(b)), 2
), 'utf-8');
};
\ No newline at end of file
};
async function getFolderSize(dir) {
const files = await globby(dir);
let size = 0;
for (let file of files) {
size += fs.statSync(file).size;
}
return size;
// const statAsync = promisify(fs.stat);
// return Promise.all(
// files.map(file => statAsync(file))
// ).then(sizes => {
// return sizes.reduce((total, current) => {
// return total + current.size;
// }, 0)
// });
}
/**
* Get results of all runs
* @return [ { id, expectedVersion, actualVersion, renderer, lastRunTime, total, finished, passed, diskSize } ]
*/
module.exports.getAllTestsRuns = async function () {
const dirs = await globby('*', { cwd: RESULTS_ROOT_DIR, onlyDirectories: true });
const results = [];
function f(number) {
return number < 10 ? '0' + number : number;
}
function formatDate(lastRunTime) {
const date = new Date(lastRunTime);
return `${date.getFullYear()}-${f(date.getMonth() + 1)}-${f(date.getDate())} ${f(date.getHours())}:${f(date.getMinutes())}:${f(date.getSeconds())}`;
}
for (let dir of dirs) {
const params = parseRunHash(dir);
const resultJson = JSON.parse(fs.readFileSync(path.join(
RESULTS_ROOT_DIR,
dir,
RESULT_FILE_NAME
), 'utf-8'));
const total = resultJson.length;
let lastRunTime = 0;
let finishedCount = 0;
let passedCount = 0;
resultJson.forEach(test => {
lastRunTime = Math.max(test.lastRun, lastRunTime);
if (test.status === 'finished') {
finishedCount++;
let passed = true;
test.results.forEach(result => {
// Threshold?
if (result.diffRatio > 0.0001) {
passed = false;
}
});
if (passed) {
passedCount++;
}
}
});
if (finishedCount === 0) {
// Cleanup empty runs
await module.exports.delTestsRun(dir);
continue;
}
params.lastRunTime = lastRunTime > 0 ? formatDate(lastRunTime) : 'N/A';
params.total = total;
params.passed = passedCount;
params.finished = finishedCount;
params.id = dir;
params.diskSize = convertBytes(await getFolderSize(path.join(RESULTS_ROOT_DIR, dir)));
results.push(params);
};
return results;
}
module.exports.delTestsRun = async function (hash) {
fse.removeSync(path.join(RESULTS_ROOT_DIR, hash));
}
\ No newline at end of file
......@@ -67,9 +67,11 @@ module.exports.prepareEChartsLib = function (version) {
let testLibPath = `${versionFolder}/${module.exports.getEChartsTestFileName()}`;
if (!fs.existsSync(testLibPath)) {
const file = fs.createWriteStream(`${versionFolder}/echarts.js`);
const isNightly = version.includes('-dev');
const packageName = isNightly ? 'echarts-nightly' : 'echarts'
console.log(`Downloading echarts@${version} from `, `https://cdn.jsdelivr.net/npm/echarts@${version}/dist/echarts.js`);
https.get(`https://cdn.jsdelivr.net/npm/echarts@${version}/dist/echarts.js`, response => {
console.log(`Downloading ${packageName}@${version} from `, `https://cdn.jsdelivr.net/npm/${packageName}@${version}/dist/echarts.js`);
https.get(`https://cdn.jsdelivr.net/npm/${packageName}@${version}/dist/echarts.js`, response => {
response.pipe(file);
file.on('finish', () => {
......@@ -85,9 +87,13 @@ module.exports.prepareEChartsLib = function (version) {
});
};
module.exports.fetchVersions = function () {
module.exports.fetchVersions = function (isNighlty) {
return new Promise((resolve, reject) => {
https.get(`https://registry.npmjs.org/echarts`, res => {
https.get(
isNighlty
? `https://registry.npmjs.org/echarts-nightly`
: `https://registry.npmjs.org/echarts`
, res => {
if (res.statusCode !== 200) {
res.destroy();
reject('Failed fetch versions from https://registry.npmjs.org/echarts');
......@@ -98,7 +104,7 @@ module.exports.fetchVersions = function () {
res.on('end', function () {
try {
var data = Buffer.concat(buffers);
resolve(Object.keys(JSON.parse(data).versions));
resolve(Object.keys(JSON.parse(data).versions).reverse());
}
catch (e) {
reject(e.toString());
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册