Add util/taskReporter

It's a lightweight utility for reporting task progress in a CLI.

It's inspired by execDependencyGraph.

Test plan: `yarn test`; unit tests included.
This commit is contained in:
Dandelion Mané 2019-07-15 00:41:09 +01:00
parent 0889a0a5d1
commit daa7409abb
2 changed files with 201 additions and 0 deletions

87
src/util/taskReporter.js Normal file
View File

@ -0,0 +1,87 @@
// @flow
const chalk = require("chalk");
type TaskId = string;
type MsSinceEpoch = number;
type ConsoleLog = (string) => void;
type GetTime = () => MsSinceEpoch;
/**
* This class is a lightweight utility for reporting task progress to the
* command line.
*
* - When a task is started, it's printed to the CLI with a " GO " label.
* - When it's finished, it's printed with a "DONE" label, along with the time
* elapsed.
* - Tasks are tracked and represented by string id.
* - The same task id may be re-used after the first task with that id is
* finished.
*/
export class TaskReporter {
// Maps the task to the time
activeTasks: Map<TaskId, MsSinceEpoch>;
_consoleLog: ConsoleLog;
_getTime: GetTime;
constructor(consoleLog?: ConsoleLog, getTime?: GetTime) {
this._consoleLog = consoleLog || console.log;
this._getTime =
getTime ||
function() {
return +new Date();
};
this.activeTasks = new Map();
}
start(taskId: TaskId) {
if (this.activeTasks.has(taskId)) {
throw new Error(`task ${taskId} already registered`);
}
this.activeTasks.set(taskId, this._getTime());
this._consoleLog(startMessage(taskId));
return this;
}
finish(taskId: TaskId) {
const startTime = this.activeTasks.get(taskId);
if (startTime == null) {
throw new Error(`task ${taskId} not registered`);
}
const elapsedTime = this._getTime() - startTime;
this._consoleLog(finishMessage(taskId, elapsedTime));
this.activeTasks.delete(taskId);
return this;
}
}
export function formatTimeElapsed(elapsed: MsSinceEpoch): string {
if (elapsed < 0) {
throw new Error("nonegative time expected");
}
if (elapsed < 1000) {
return `${elapsed}ms`;
}
const seconds = Math.round(elapsed / 1000);
const minutes = Math.floor(seconds / 60);
if (minutes == 0) return `${seconds}s`;
const hours = Math.floor(minutes / 60);
if (hours == 0) return `${minutes}m ${seconds % 60}s`;
const days = Math.floor(hours / 24);
if (days == 0) return `${hours}h ${minutes % 60}m`;
return `${days}d ${hours % 24}h`;
}
export function startMessage(taskId: string): string {
const label = chalk.bgBlue.bold.white(" GO ");
const message = `${label} ${taskId}`;
return message;
}
export function finishMessage(taskId: string, elapsedTimeMs: number): string {
const elapsed = formatTimeElapsed(elapsedTimeMs);
const label = chalk.bgGreen.bold.white(" DONE ");
const message = `${label} ${taskId}: ${elapsed}`;
return message;
}

View File

@ -0,0 +1,114 @@
// @flow
import {
TaskReporter,
formatTimeElapsed,
startMessage,
finishMessage,
} from "./taskReporter";
describe("util/taskReporter", () => {
describe("formatTimeElapsed", () => {
function tc(expected, ms) {
it(`works for ${expected}`, () => {
expect(formatTimeElapsed(ms)).toEqual(expected);
});
}
tc("0ms", 0);
tc("50ms", 50);
tc("999ms", 999);
const secs = 1000;
tc("1s", 1 * secs + 400);
tc("2s", 1 * secs + 600);
tc("59s", 59 * secs);
const mins = secs * 60;
tc("1m 3s", mins + 3 * secs);
tc("59m 59s", 59 * mins + 59 * secs);
const hours = mins * 60;
tc("1h 0m", hours);
tc("1h 3m", hours + mins * 3);
tc("23h 59m", 23 * hours + 59 * mins);
const days = 24 * hours;
tc("1d 0h", days);
tc("555d 23h", 555 * days + 23 * hours);
});
describe("TaskReporter", () => {
class TestCase {
_time: number;
messages: string[];
taskReporter: TaskReporter;
constructor() {
this._time = 0;
this.messages = [];
const logMock = (x) => {
this.messages.push(x);
};
const timeMock = () => this._time;
this.taskReporter = new TaskReporter(logMock, timeMock);
}
start(task: string) {
this.taskReporter.start(task);
return this;
}
finish(task: string) {
this.taskReporter.finish(task);
return this;
}
time(t: number) {
this._time = t;
return this;
}
}
it("errors when finishing an unregistered task", () => {
const fail = () => new TestCase().finish("foo");
expect(fail).toThrowError("task foo not registered");
});
it("errors when starting a task twice", () => {
const fail = () => new TestCase().start("foo").start("foo");
expect(fail).toThrowError("task foo already registered");
});
it("errors when finishing a task twice", () => {
const fail = () =>
new TestCase()
.start("foo")
.finish("foo")
.finish("foo");
expect(fail).toThrowError("task foo not registered");
});
it("works for a task that immediately finishes", () => {
const {messages} = new TestCase().start("foo").finish("foo");
expect(messages).toEqual([startMessage("foo"), finishMessage("foo", 0)]);
});
it("works when two tasks are started, then one finishes", () => {
const {messages} = new TestCase()
.start("foo")
.start("bar")
.time(200)
.finish("foo");
expect(messages).toEqual([
startMessage("foo"),
startMessage("bar"),
finishMessage("foo", 200),
]);
});
it("works when a task is started, finished, and re-started", () => {
const {messages} = new TestCase()
.start("foo")
.finish("foo")
.start("foo")
.time(200)
.finish("foo");
expect(messages).toEqual([
startMessage("foo"),
finishMessage("foo", 0),
startMessage("foo"),
finishMessage("foo", 200),
]);
});
});
});