|  | /* | 
|  | * 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, | 
|  | 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, | 
|  | '--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() { | 
|  | 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)).slice(0, 100); | 
|  | 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, | 
|  | useCoarsePointer: data.useCoarsePointer, | 
|  | 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 || '', | 
|  | 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(); |