|
| 1 | +/** |
| 2 | + * @author Toru Nagashima <https://github.com/mysticatea> |
| 3 | + * @copyright 2017 Toru Nagashima. All rights reserved. |
| 4 | + * See LICENSE file in root directory for full license. |
| 5 | + */ |
| 6 | +"use strict" |
| 7 | + |
| 8 | +/* |
| 9 | + * Run tests in parallel. |
| 10 | + * This can reduce the spent time of tests to 1/3, but this is badly affecting to the timers in tests. |
| 11 | + * I need more investigation. |
| 12 | + */ |
| 13 | + |
| 14 | +//------------------------------------------------------------------------------ |
| 15 | +// Requirements |
| 16 | +//------------------------------------------------------------------------------ |
| 17 | + |
| 18 | +const spawn = require("child_process").spawn |
| 19 | +const path = require("path") |
| 20 | +const os = require("os") |
| 21 | +const fs = require("fs-extra") |
| 22 | +const PQueue = require("p-queue") |
| 23 | + |
| 24 | +//------------------------------------------------------------------------------ |
| 25 | +// Helpers |
| 26 | +//------------------------------------------------------------------------------ |
| 27 | + |
| 28 | +const ROOT_PATH = path.resolve(__dirname, "../") |
| 29 | +const WORKSPACE_PATH = path.resolve(__dirname, "../../test-workspace") |
| 30 | +const MOCHA_PATH = path.resolve(__dirname, "../../node_modules/mocha/bin/_mocha") |
| 31 | + |
| 32 | +/** |
| 33 | + * Convert a given duration in seconds to a string. |
| 34 | + * @param {number} durationInSec A duration to convert. |
| 35 | + * @returns {string} The string of the duration. |
| 36 | + */ |
| 37 | +function durationToText(durationInSec) { |
| 38 | + return `${durationInSec / 60 | 0}m ${durationInSec % 60 | 0}s` |
| 39 | +} |
| 40 | + |
| 41 | +/** |
| 42 | + * Run a given test file. |
| 43 | + * @param {string} filePath The absolute path to a test file. |
| 44 | + * @param {string} workspacePath The absolute path to the workspace directory. |
| 45 | + * @returns {Promise<{duration:number,exitCode:number,failing:number,id:string,passing:number,text:string}>} |
| 46 | + * - `duration` is the spent time in seconds. |
| 47 | + * - `exitCode` is the exit code of the child process. |
| 48 | + * - `failing` is the number of failed tests. |
| 49 | + * - `id` is the name of this tests. |
| 50 | + * - `passing` is the number of succeeded tests. |
| 51 | + * - `text` is the result text of the child process. |
| 52 | + */ |
| 53 | +function runMocha(filePath, workspacePath) { |
| 54 | + return new Promise((resolve, reject) => { |
| 55 | + const startInSec = process.uptime() |
| 56 | + const cp = spawn( |
| 57 | + process.execPath, |
| 58 | + [MOCHA_PATH, filePath, "--reporter", "dot", "--timeout", "120000"], |
| 59 | + { cwd: workspacePath, stdio: ["ignore", "pipe", "inherit"] } |
| 60 | + ) |
| 61 | + |
| 62 | + let resultText = "" |
| 63 | + |
| 64 | + cp.stdout.setEncoding("utf8") |
| 65 | + cp.stdout.on("data", (rawChunk) => { |
| 66 | + const chunk = rawChunk.trim().replace(/^[․.!]+/, (dots) => { |
| 67 | + process.stdout.write(dots) |
| 68 | + return "" |
| 69 | + }) |
| 70 | + if (chunk) { |
| 71 | + resultText += chunk |
| 72 | + resultText += "\n\n" |
| 73 | + } |
| 74 | + }) |
| 75 | + |
| 76 | + cp.on("exit", (exitCode) => { |
| 77 | + let passing = 0 |
| 78 | + let failing = 0 |
| 79 | + const text = resultText |
| 80 | + .replace(/(\d+) passing\s*\(.+?\)/, (_, n) => { |
| 81 | + passing += Number(n) |
| 82 | + return "" |
| 83 | + }) |
| 84 | + .replace(/(\d+) failing\s*/, (_, n) => { |
| 85 | + failing += Number(n) |
| 86 | + return "" |
| 87 | + }) |
| 88 | + .replace(/^\s*\d+\)/gm, "") |
| 89 | + .split("\n") |
| 90 | + .filter(line => !line.includes("empower-core")) |
| 91 | + .join("\n") |
| 92 | + .trim() |
| 93 | + |
| 94 | + resolve({ |
| 95 | + duration: process.uptime() - startInSec, |
| 96 | + exitCode, |
| 97 | + failing, |
| 98 | + id: path.basename(filePath, ".js"), |
| 99 | + passing, |
| 100 | + text, |
| 101 | + }) |
| 102 | + }) |
| 103 | + cp.on("error", reject) |
| 104 | + }) |
| 105 | +} |
| 106 | + |
| 107 | +/** |
| 108 | + * Run a given test file. |
| 109 | + * @param {string} filePath The absolute path to a test file. |
| 110 | + * @returns {Promise<{duration:number,exitCode:number,failing:number,id:string,passing:number,text:string}>} |
| 111 | + * - `duration` is the spent time in seconds. |
| 112 | + * - `exitCode` is the exit code of the child process. |
| 113 | + * - `failing` is the number of failed tests. |
| 114 | + * - `id` is the name of this tests. |
| 115 | + * - `passing` is the number of succeeded tests. |
| 116 | + * - `text` is the result text of the child process. |
| 117 | + */ |
| 118 | +async function runMochaWithWorkspace(filePath) { |
| 119 | + const basename = path.basename(filePath, ".js") |
| 120 | + const workspacePath = path.resolve(__dirname, `../../test-workspace-${basename}`) |
| 121 | + |
| 122 | + await fs.remove(workspacePath) |
| 123 | + await fs.copy(WORKSPACE_PATH, workspacePath, { dereference: true, recursive: true }) |
| 124 | + try { |
| 125 | + return await runMocha(filePath, workspacePath) |
| 126 | + } |
| 127 | + finally { |
| 128 | + try { |
| 129 | + await fs.remove(workspacePath) |
| 130 | + } |
| 131 | + catch (_error) { |
| 132 | + // ignore to keep the original error. |
| 133 | + } |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +//------------------------------------------------------------------------------ |
| 138 | +// Main |
| 139 | +//------------------------------------------------------------------------------ |
| 140 | + |
| 141 | +(async () => { |
| 142 | + const startInSec = process.uptime() |
| 143 | + const queue = new PQueue({ concurrency: os.cpus().length + 1 }) |
| 144 | + const results = await Promise.all( |
| 145 | + (await fs.readdir(ROOT_PATH)) |
| 146 | + .filter(fileName => path.extname(fileName) === ".js") |
| 147 | + .map(fileName => path.join(ROOT_PATH, fileName)) |
| 148 | + .map(filePath => queue.add(() => runMochaWithWorkspace(filePath))) |
| 149 | + ) |
| 150 | + |
| 151 | + process.stdout.write("\n\n") |
| 152 | + |
| 153 | + for (const result of results) { |
| 154 | + if (result.text) { |
| 155 | + process.stdout.write(`\n${result.text}\n\n`) |
| 156 | + } |
| 157 | + if (result.exitCode) { |
| 158 | + process.exitCode = 1 |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + let passing = 0 |
| 163 | + let failing = 0 |
| 164 | + for (const result of results) { |
| 165 | + passing += result.passing |
| 166 | + failing += result.failing |
| 167 | + process.stdout.write(`\n${result.id}: passing ${result.passing} failing ${result.failing} (${durationToText(result.duration)})`) |
| 168 | + } |
| 169 | + process.stdout.write(`\n\nTOTAL: passing ${passing} failing ${failing} (${durationToText(process.uptime() - startInSec)})\n\n`) |
| 170 | +})().catch(error => { |
| 171 | + process.stderr.write(`\n\n${error.stack}\n\n`) |
| 172 | + process.exit(1) //eslint-disable-line no-process-exit |
| 173 | +}) |
0 commit comments