/* * 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 chalk = require('chalk'); process.on('uncaughtException', (err) => { if (err.message.includes('Cannot find module')) { console.error(chalk.red(err.message.split('\n')[0])); console.log(); console.error(`Please run \`${chalk.yellow('npm install')}\` in \`${chalk.green('test/runTest')}\` folder first!`); console.log(); } else { console.error('Uncaught err:', err); } process.exit(1); }); const handler = require('serve-handler'); const http = require('http'); const path = require('path'); 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, clearStaledResults } = require('./store'); const {prepareEChartsLib, getActionsFullPath, fetchVersions, cleanBranchDirectory, fetchRecentPRs} = require('./util'); const fse = require('fs-extra'); const fs = require('fs'); const open = require('open'); const genReport = require('./genReport'); const useCNMirror = process.argv.includes('--useCNMirror') || !!process.env.USE_CN_MIRROR; console.info(chalk.green('useCNMirror:'), useCNMirror); const CLI_FIXED_THREADS_COUNT = 1; function serve() { clearStaledResults(); 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, actualSource, actualVersion, expectedSource, expectedVersion, renderer, useCoarsePointer, 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, '--actual-source', actualSource, '--expected-source', expectedSource, '--renderer', renderer || '', '--use-coarse-pointer', useCoarsePointer, '--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() { // Clean PR directories before starting const {cleanPRDirectories} = require('./util'); cleanPRDirectories(); if (!checkPuppeteer()) { console.error(`Can't find puppeteer >= 9.0.0, run 'npm install' to update in the 'test/runTest' folder`); return; } let stableVersions; let nightlyVersions; try { stableVersions = await fetchVersions(false, useCNMirror); nightlyVersions = (await fetchVersions(true, useCNMirror)).slice(0, 100); } catch (e) { console.error('Failed to fetch version list:', e); console.log(`Try again later or try the CN mirror with: ${chalk.yellow('npm run test:visual -- -- --useCNMirror')}`) 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(); 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; } socket.emit('syncRunConfig_return', { runConfig: _currentRunConfig, versions: stableVersions, nightlyVersions: nightlyVersions }); 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) => { console.log('genTestsRunReport', 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(); try { await prepareEChartsLib(data.expectedSource, data.expectedVersion, useCNMirror); await prepareEChartsLib(data.actualSource, data.actualVersion, useCNMirror); } catch (e) { console.error(e); // Send error to client socket.emit('run_error', { message: e.toString() }); return; } // If aborted in the time downloading lib. if (isAborted) { return; } // TODO Should broadcast to all sockets. 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, actualSource: data.actualSource, actualVersion: data.actualVersion, expectedSource: data.expectedSource, expectedVersion: data.expectedVersion, renderer: data.renderer, useCoarsePointer: data.useCoarsePointer, noSave: false } ); 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 || '', useCoarsePointer: data.useCoarsePointer, 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();