blob: 1e224ac22c1d84b6da2b51d4d3d8f51117c1cc41 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const handler = require('serve-handler');
const http = require('http');
const path = require('path');
// const open = require('open');
const {fork} = require('child_process');
const semver = require('semver');
const {port, origin} = require('./config');
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');
const CLI_FIXED_THREADS_COUNT = 1;
function serve() {
const server = http.createServer((request, response) => {
return handler(request, response, {
cleanUrls: false,
// Root folder of echarts
public: __dirname + '/../../'
});
});
server.listen(port, () => {
// console.log(`Server started. ${origin}`);
});
const io = require('socket.io')(server);
return {
io
};
};
let runningThreads = [];
let pendingTests;
function isRunning() {
return runningThreads.length > 0;
}
function stopRunningTests() {
if (isRunning()) {
runningThreads.forEach(thread => thread.kill());
runningThreads = [];
}
if (pendingTests) {
pendingTests.forEach(testOpt => {
if (testOpt.status === 'pending') {
testOpt.status = 'unsettled';
}
});
saveTestsList();
pendingTests = null;
}
}
class Thread {
constructor() {
this.tests = [];
this.onExit;
this.onUpdate;
}
fork(extraArgs) {
let p = fork(path.join(__dirname, 'cli.js'), [
'--tests', this.tests.map(testOpt => testOpt.name).join(','),
...extraArgs
]);
this.p = p;
// Finished one test
p.on('message', testOpt => {
mergeTestsResults([testOpt]);
saveTestsList();
this.onUpdate();
});
// Finished all
p.on('exit', () => {
this.p = null;
setTimeout(this.onExit);
});
}
kill() {
if (this.p) {
this.p.kill();
}
}
}
function startTests(testsNameList, socket, {
noHeadless,
threadsCount,
replaySpeed,
actualVersion,
expectedVersion,
renderer,
noSave
}) {
console.log('Received: ', testsNameList.join(','));
return new Promise(resolve => {
pendingTests = getTestsList().filter(testOpt => {
return testsNameList.includes(testOpt.name);
});
if (!noSave) {
pendingTests.forEach(testOpt => {
// Reset all tests results
testOpt.status = 'pending';
testOpt.results = [];
});
// Save status immediately
saveTestsList();
socket.emit('update', {
tests: getTestsList(),
running: true
});
}
threadsCount = Math.min(
Math.ceil((threadsCount || 1) / CLI_FIXED_THREADS_COUNT),
pendingTests.length
);
let runningCount = threadsCount;
function onExit() {
runningCount--;
if (runningCount === 0) {
runningThreads = [];
resolve();
}
}
function onUpdate() {
// Merge tests.
if (isRunning() && !noSave) {
socket.emit('update', {
tests: getTestsList(),
running: true
});
}
}
// Assigning tests to threads
runningThreads = new Array(threadsCount).fill(0).map(() => new Thread() );
for (let i = 0; i < pendingTests.length; i++) {
runningThreads[i % threadsCount].tests.push(pendingTests[i]);
}
for (let i = 0; i < runningThreads.length; i++) {
runningThreads[i].onExit = onExit;
runningThreads[i].onUpdate = onUpdate;
runningThreads[i].fork([
'--speed', replaySpeed || 5,
'--actual', actualVersion,
'--expected', expectedVersion,
'--renderer', renderer || '',
'--threads', Math.min(threadsCount, CLI_FIXED_THREADS_COUNT),
'--dir', getResultBaseDir(),
...(noHeadless ? ['--no-headless'] : []),
...(noSave ? ['--no-save'] : [])
]);
}
});
}
function checkPuppeteer() {
try {
const packageConfig = require('puppeteer/package.json');
console.log(`puppeteer version: ${packageConfig.version}`);
return semver.satisfies(packageConfig.version, '>=9.0.0');
}
catch (e) {
return false;
}
}
async function start() {
if (!checkPuppeteer()) {
// TODO Check version.
console.error(`Can't find puppeteer >= 9.0.0, run 'npm install' to update in the 'test/runTest' folder`);
return;
}
let _currentTestHash;
let _currentRunConfig;
// let runtimeCode = await buildRuntimeCode();
// fse.outputFileSync(path.join(__dirname, 'tmp/testRuntime.js'), runtimeCode, 'utf-8');
// Start a static server for puppeteer open the html test cases.
let {io} = serve();
const stableVersions = await fetchVersions(false);
const nightlyVersions = await fetchVersions(true);
stableVersions.unshift('local');
nightlyVersions.unshift('local');
io.of('/client').on('connect', async socket => {
let isAborted = false;
function abortTests() {
if (!isRunning()) {
return;
}
isAborted = true;
stopRunningTests();
io.of('/client').emit('abort');
}
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),
!isRunning() // Set to unsettled if not running
);
socket.emit('update', {
tests: getTestsList(),
running: isRunning()
});
});
socket.on('getAllTestsRuns', async () => {
socket.emit('getAllTestsRuns_return', {
runs: await getAllTestsRuns()
});
});
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('delTestsRun', async (params) => {
delTestsRun(params.id);
console.log('Deleted', params.id);
});
socket.on('run', async data => {
stopRunningTests();
isAborted = false;
let startTime = Date.now();
await prepareEChartsLib(data.expectedVersion); // Expected version.
await prepareEChartsLib(data.actualVersion); // Version to test
// If aborted in the time downloading lib.
if (isAborted) {
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'),
{
noHeadless: data.noHeadless,
threadsCount: data.threads,
replaySpeed: data.replaySpeed,
actualVersion: data.actualVersion,
expectedVersion: data.expectedVersion,
renderer: data.renderer,
noSave: false
}
);
}
catch (e) {
console.error(e);
}
if (!isAborted) {
const deltaTime = Date.now() - startTime;
console.log('Finished in ', Math.round(deltaTime / 1000) + ' second');
io.of('/client').emit('finish', {
time: deltaTime,
count: data.tests.length,
threads: data.threads
});
}
else {
console.log('Aborted!');
}
});
socket.on('stop', abortTests);
});
io.of('/recorder').on('connect', async socket => {
socket.on('saveActions', data => {
if (data.testName) {
fse.outputFile(
getActionsFullPath(data.testName),
JSON.stringify(data.actions),
'utf-8'
);
updateActionsMeta(data.testName, data.actions);
}
// TODO Broadcast the change?
});
socket.on('changeTest', data => {
try {
const actionData = fs.readFileSync(getActionsFullPath(data.testName), 'utf-8');
socket.emit('updateActions', {
testName: data.testName,
actions: JSON.parse(actionData)
});
}
catch(e) {
// Can't find file.
}
});
socket.on('runSingle', async data => {
stopRunningTests();
try {
await startTests([data.testName], socket, {
noHeadless: true,
threadsCount: 1,
replaySpeed: 2,
actualVersion: data.actualVersion,
expectedVersion: data.expectedVersion,
renderer: data.renderer || '',
noSave: true
});
}
catch (e) { console.error(e); }
console.log('Finished');
socket.emit('finish');
});
socket.emit('getTests', {
// TODO updateTestsList.
tests: getTestsList().map(test => {
return {
name: test.name,
actions: test.actions
};
})
});
});
console.log(`Dashboard: ${origin}/test/runTest/client/index.html`);
console.log(`Interaction Recorder: ${origin}/test/runTest/recorder/index.html`);
open(`${origin}/test/runTest/client/index.html`);
}
start();