diff --git a/config/travis.js b/config/travis.js index 3db40e8..cede169 100644 --- a/config/travis.js +++ b/config/travis.js @@ -1,24 +1,8 @@ // @flow -const chalk = require("chalk"); -const child_process = require("child_process"); +const execDependencyGraph = require("../src/tools/execDependencyGraph"); -/*:: -type TaskId = string; -type Task = {| - +id: TaskId, - +cmd: $ReadOnlyArray, - +deps: $ReadOnlyArray, -|}; - -type TaskResult = {| - +id: TaskId, - +success: boolean, - +status: number, - +stdout: string, - +stderr: string, -|}; -*/ +main(); function main() { const mode = @@ -26,170 +10,8 @@ function main() { 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(), - }); - }); + execDependencyGraph(makeTasks(mode)).then(({success}) => { + process.exitCode = success ? 0 : 1; }); } diff --git a/src/tools/execDependencyGraph.js b/src/tools/execDependencyGraph.js new file mode 100644 index 0000000..d642439 --- /dev/null +++ b/src/tools/execDependencyGraph.js @@ -0,0 +1,194 @@ +// @flow + +// NOTE: This module must be written in vanilla ECMAScript that can be +// run by Node without a preprocessor. That means that we use +// `module.exports` and `require` instead of ECMAScript module keywords, +// and we use the Flow comment syntax instead of the inline syntax. + +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, +|}; +type OverallResult = {| + +success: boolean, +|}; +*/ + +module.exports = async function execDepgraph( + tasks /*: $ReadOnlyArray */ +) /*: Promise */ { + 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, processTask(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); + return Promise.resolve({success: overallSuccess}); +}; + +function processTask(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(), + }); + }); + }); +}