| /* |
| * 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} = require('./store'); |
| const {prepareEChartsLib, getActionsFullPath, fetchVersions} = require('./util'); |
| const fse = require('fs-extra'); |
| const fs = require('fs'); |
| const open = require('open'); |
| |
| 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; |
| let aborted = false; |
| |
| function stopRunningTests() { |
| if (runningThreads) { |
| runningThreads.forEach(thread => thread.kill()); |
| runningThreads = []; |
| } |
| if (pendingTests) { |
| pendingTests.forEach(testOpt => { |
| if (testOpt.status === 'pending') { |
| testOpt.status = 'unsettled'; |
| } |
| }); |
| 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(',')); |
| |
| threadsCount = threadsCount || 1; |
| stopRunningTests(); |
| |
| 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 = []; |
| }); |
| |
| if (!aborted) { |
| socket.emit('update', {tests: getTestsList(), running: true}); |
| } |
| } |
| let runningCount = 0; |
| function onExit() { |
| runningCount--; |
| if (runningCount === 0) { |
| runningThreads = []; |
| resolve(); |
| } |
| } |
| function onUpdate() { |
| // Merge tests. |
| if (!aborted && !noSave) { |
| socket.emit('update', {tests: getTestsList(), running: true}); |
| } |
| } |
| threadsCount = Math.min(threadsCount, pendingTests.length); |
| // 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 < threadsCount; i++) { |
| runningThreads[i].onExit = onExit; |
| runningThreads[i].onUpdate = onUpdate; |
| runningThreads[i].fork([ |
| '--speed', replaySpeed || 5, |
| '--actual', actualVersion, |
| '--expected', expectedVersion, |
| '--renderer', renderer || '', |
| ...(noHeadless ? ['--no-headless'] : []), |
| ...(noSave ? ['--no-save'] : []) |
| ]); |
| runningCount++; |
| } |
| // If something bad happens and no proccess are started successfully |
| if (runningCount === 0) { |
| resolve(); |
| } |
| }); |
| } |
| |
| function checkPuppeteer() { |
| try { |
| const packageConfig = require('puppeteer/package.json'); |
| console.log(`puppeteer version: ${packageConfig.version}`); |
| return semver.satisfies(packageConfig.version, '>=1.19.0'); |
| } |
| catch (e) { |
| return false; |
| } |
| } |
| |
| async function start() { |
| if (!checkPuppeteer()) { |
| // TODO Check version. |
| console.error(`Can't find puppeteer >= 1.19.0, use 'npm install puppeteer --no-save' to install or update`); |
| return; |
| } |
| |
| let [versions] = await Promise.all([ |
| fetchVersions(), |
| updateTestsList(true) |
| ]); |
| |
| // 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 => { |
| await updateTestsList(); |
| |
| socket.emit('update', { |
| tests: getTestsList(), |
| running: runningThreads.length > 0 |
| }); |
| |
| socket.on('run', async data => { |
| |
| let startTime = Date.now(); |
| aborted = false; |
| |
| await prepareEChartsLib(data.expectedVersion); // Expected version. |
| await prepareEChartsLib(data.actualVersion); // Version to test |
| |
| if (aborted) { // If it is aborted when downloading echarts lib. |
| return; |
| } |
| |
| // TODO Should broadcast to all sockets. |
| try { |
| 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 (!aborted) { |
| console.log('Finished'); |
| io.of('/client').emit('finish', { |
| time: Date.now() - startTime, |
| count: data.tests.length, |
| threads: data.threads |
| }); |
| } |
| else { |
| console.log('Aborted!'); |
| } |
| }); |
| socket.on('stop', () => { |
| stopRunningTests(); |
| io.of('/client').emit('abort'); |
| aborted = true; |
| }); |
| |
| socket.emit('versions', versions); |
| }); |
| |
| io.of('/recorder').on('connect', async socket => { |
| await updateTestsList(); |
| 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 => { |
| 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', { |
| 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(); |