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:
parent
0889a0a5d1
commit
daa7409abb
|
@ -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;
|
||||||
|
}
|
|
@ -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),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue