diff --git a/config/travis.js b/config/travis.js new file mode 100644 index 0000000..def9ac5 --- /dev/null +++ b/config/travis.js @@ -0,0 +1,244 @@ +// @flow + +const chalk = require("chalk"); +const child_process = require("child_process"); + +/*:: +type TaskId = string; +type Task = {| + +id: TaskId, + +cmd: $ReadOnlyArray, + +deps: $ReadOnlyArray, +|}; + +type TaskResult = {| + +id: TaskId, + +success: boolean, + +status: number, + +stdout: string, + +stderr: string, +|}; +*/ + +function main() { + const mode = + process.env["TRAVIS_EVENT_TYPE"] === "cron" || + process.argv.includes("--full") + ? "FULL" + : "BASIC"; + processAll(makeTasks(mode)); +} +main(); + +async function processAll(tasks /*: $ReadOnlyArray */) { + const tasksById /*: {[TaskId]: Task} */ = {}; + tasks.forEach((task) => { + if (tasksById[task.id] !== undefined) { + throw new Error("Duplicate tasks with ID: " + task.id); + } + tasksById[task.id] = task; + }); + + const completedTasks /*: Map */ = new Map(); + const tasksInProgress /*: Map> */ = new Map(); + const remainingTasks /*: Set */ = new Set(Object.keys(tasksById)); + + function spawnTasksWhoseDependenciesHaveCompleted() { + for (const task of tasks) { + if (!remainingTasks.has(task.id)) { + continue; + } + if (incompleteDependencies(task).length > 0) { + continue; + } + // Ready to spawn! + remainingTasks.delete(task.id); + console.log(chalk.bgBlue.bold.white(" GO ") + " " + task.id); + tasksInProgress.set(task.id, processOne(task)); + } + } + + function incompleteDependencies(task /*: Task */) /*: TaskId[] */ { + return task.deps.filter((dep) => { + const result = completedTasks.get(dep); + return !(result && result.success); + }); + } + + async function awaitAnyTask() { + if (tasksInProgress.size === 0) { + throw new Error("Invariant violation: No tasks to wait for."); + } + const result /*: TaskResult */ = await Promise.race( + Array.from(tasksInProgress.values()) + ); + tasksInProgress.delete(result.id); + completedTasks.set(result.id, result); + displayResult(result.id, result, "OVERVIEW"); + } + + function displayResult( + id /*: TaskId */, + result /*: ?TaskResult */, + mode /*: "OVERVIEW" | "FULL" */ + ) { + const success = result && result.success; + const badge = success + ? chalk.bgGreen.bold.white(" PASS ") + : chalk.bgRed.bold.white(" FAIL "); + console.log(`${badge} ${id}`); + + if (mode === "OVERVIEW" && success) { + return; + } + + let loggedAnything = false; + function log(...args) { + console.log(...args); + loggedAnything = true; + } + if (!result) { + log(`Did not run. Missing dependencies:`); + incompleteDependencies(tasksById[id]).forEach((dep) => { + log(` - ${dep}`); + }); + log(); + return; + } + if (result.status !== 0) { + log("Exit code: " + result.status); + } + if (result.stdout.length > 0) { + log("Contents of stdout:"); + displayOutputStream(result.stdout); + } + if (result.stderr.length > 0) { + log("Contents of stderr:"); + displayOutputStream(result.stderr); + } + if (loggedAnything) { + console.log(); + } + } + + function displayOutputStream(streamContents /*: string */) { + streamContents.split("\n").forEach((line, index, array) => { + if (line === "" && index === array.length - 1) { + return; + } else { + console.log(" " + line); + } + }); + } + + function printSection(name /*: string */) { + console.log("\n" + chalk.bold(name)); + } + + printSection("Starting tasks"); + spawnTasksWhoseDependenciesHaveCompleted(); + while (tasksInProgress.size > 0) { + await awaitAnyTask(); + spawnTasksWhoseDependenciesHaveCompleted(); + } + + if (remainingTasks.size > 0) { + printSection("Unreachable tasks"); + Array.from(remainingTasks.values()).forEach((line) => { + console.log(` - ${line}`); + }); + } + + printSection("Full results"); + for (const task of tasks) { + const result = completedTasks.get(task.id); + displayResult(task.id, result, "FULL"); + } + + printSection("Overview"); + const failedTasks = tasks.map((t) => t.id).filter((id) => { + const result = completedTasks.get(id); + return !result || !result.success; + }); + if (failedTasks.length > 0) { + console.log("Failed tasks:"); + failedTasks.forEach((line) => { + console.log(` - ${line}`); + }); + } + const overallSuccess /*: boolean */ = failedTasks.length === 0; + const overallBadge = overallSuccess + ? chalk.bgGreen.bold.white(" SUCCESS ") + : chalk.bgRed.bold.white(" FAILURE "); + console.log("Final result: " + overallBadge); + process.exitCode = overallSuccess ? 0 : 1; +} + +function processOne(task /*: Task */) /*: Promise */ { + if (task.cmd.length === 0) { + throw new Error("Empty command for task: " + task.id); + } + const file = task.cmd[0]; + const args = task.cmd.slice(1); + return new Promise((resolve, _unused_reject) => { + child_process.execFile(file, args, (error, stdout, stderr) => { + resolve({ + id: task.id, + success: !error, + status: error ? error.code : 0, + stdout: stdout.toString(), + stderr: stderr.toString(), + }); + }); + }); +} + +function makeTasks(mode /*: "BASIC" | "FULL" */) { + const basicTasks = [ + { + id: "check-pretty", + cmd: ["npm", "run", "check-pretty"], + deps: [], + }, + { + id: "lint", + cmd: ["npm", "run", "lint"], + deps: [], + }, + { + id: "flow", + cmd: ["npm", "run", "flow"], + deps: [], + }, + { + id: "ci-test", + cmd: ["npm", "run", "ci-test"], + deps: [], + }, + ]; + const extraTasks = [ + { + id: "backend", + cmd: ["npm", "run", "backend"], + deps: [], + }, + { + id: "fetchGithubRepoTest", + cmd: ["./src/plugins/github/fetchGithubRepoTest.sh", "--no-build"], + deps: ["backend"], + }, + { + id: "loadRepositoryTest", + cmd: ["./src/plugins/git/loadRepositoryTest.sh", "--no-build"], + deps: ["backend"], + }, + ]; + switch (mode) { + case "BASIC": + return basicTasks; + case "FULL": + return [].concat(basicTasks, extraTasks); + default: + /*:: (mode: empty); */ throw new Error(mode); + } +} diff --git a/package.json b/package.json index 701544c..41aa02b 100644 --- a/package.json +++ b/package.json @@ -62,9 +62,10 @@ "build": "node scripts/build.js", "backend": "node scripts/backend.js", "test": "node scripts/test.js --env=jsdom", + "ci-test": "CI=1 npm run test", "flow": "flow", "lint": "eslint src config --max-warnings 0", - "travis": "npm run check-pretty && npm run lint && npm run flow && CI=true npm run test" + "travis": "node ./config/travis.js" }, "license": "MIT", "lint-staged": {