Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import chalk from "chalk";

import { loadUserConfig } from "./config";
import { type Config } from "./config/types";
import { startRenderer } from "./renderers";
import { getRunOptions } from "./runOptions/runOptions";
import { type RunOptions } from "./runOptions/types";
import { hasTaskFailures, runTasks, startWatcher } from "./task";

void main().catch((error) => {
Expand All @@ -12,6 +15,16 @@ void main().catch((error) => {
process.exit(1);
});

const executeRun = async (config: Config, runOptions: RunOptions) => {
const stopRenderer = startRenderer(config, runOptions);

try {
await runTasks(config);
} finally {
Comment thread
MaximSrour marked this conversation as resolved.
stopRenderer();
}
};

/**
* Main entry point for executing tasks.
*/
Expand All @@ -34,12 +47,14 @@ async function main() {
}

if (!runOptions.isWatchMode) {
await runTasks(config, runOptions);
await executeRun(config, runOptions);

process.exit(hasTaskFailures(config.tasks) ? 1 : 0);
}

await runTasks(config, runOptions);
await executeRun(config, runOptions);

startWatcher(config, runOptions);
startWatcher(config, () => {
return executeRun(config, runOptions);
});
}
2 changes: 1 addition & 1 deletion src/renderers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { render } from "./render";
export { startRenderer } from "./renderScheduler";
34 changes: 32 additions & 2 deletions src/renderers/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,36 @@ describe("render", () => {
expect(logSpy).not.toHaveBeenCalledWith("Details:");
});

it("uses current time for unfinished overall duration when only running tasks have started", () => {
vi.useFakeTimers();
const now = new Date("2026-02-24T00:00:02.500Z");
vi.setSystemTime(now);
Object.defineProperty(process.stdout, "isTTY", {
configurable: true,
value: false,
});

const tasks = [
createTaskDouble({
label: "Type Check",
tool: "TypeScript",
state: "running",
message: "Running...",
startTime: now.getTime() - 900,
endTime: null,
}),
];

render(createConfig(tasks), baseRunOptions);

expect(renderTableMock).toHaveBeenCalledWith(tasks, [
"⏳ Overall",
"",
"0 issues",
"900 ms",
]);
});

it("does not print failure details until suite is finished", () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(noOp);
Object.defineProperty(process.stdout, "isTTY", {
Expand Down Expand Up @@ -633,7 +663,7 @@ describe("render", () => {
]);
});

it("ignores tasks with partial timing information when computing suite duration", () => {
it("uses started tasks when computing suite duration even if end time is missing", () => {
Object.defineProperty(process.stdout, "isTTY", {
configurable: true,
value: false,
Expand Down Expand Up @@ -672,7 +702,7 @@ describe("render", () => {
"🟢 Overall",
"",
"0 issues",
"1.0 s",
"2.0 s",
]);
});

Expand Down
44 changes: 28 additions & 16 deletions src/renderers/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export const formatUnsupportedTools = (unsupportedTools: string[]) => {
export const render = (config: Config, runOptions: RunOptions) => {
const { tasks, unsupportedTools } = config;

const timeNow = Date.now();

if (process.stdout.isTTY) {
console.clear();
}
Expand Down Expand Up @@ -150,28 +152,38 @@ export const render = (config: Config, runOptions: RunOptions) => {
);

const { suiteStartTime, suiteDurationMs } = (() => {
let hasCompleteTask = false;
let startTime = Number.POSITIVE_INFINITY;
let endTime = Number.NEGATIVE_INFINITY;

for (const task of tasks) {
const start = task.getStartTime();
const end = task.getEndTime();

if (start !== null && end !== null) {
hasCompleteTask = true;
startTime = Math.min(startTime, start);
endTime = Math.max(endTime, end);
const { hasStartedTask, startTime, effectiveEndTime } = tasks.reduce(
(acc, task) => {
const start = task.getStartTime();
const end = task.getEndTime();

if (start !== null) {
return {
hasStartedTask: true,
startTime: Math.min(acc.startTime, start),
effectiveEndTime: Math.max(
acc.effectiveEndTime,
end ?? (suiteFinished ? start : timeNow)
),
};
}

return acc;
},
{
hasStartedTask: false,
startTime: Number.POSITIVE_INFINITY,
effectiveEndTime: Number.NEGATIVE_INFINITY,
}
}
);

if (!hasCompleteTask) {
if (!hasStartedTask) {
return { suiteStartTime: null, suiteDurationMs: 0 };
}

return {
suiteStartTime: startTime,
suiteDurationMs: endTime - startTime,
suiteDurationMs: effectiveEndTime - startTime,
};
})();

Expand All @@ -184,7 +196,7 @@ export const render = (config: Config, runOptions: RunOptions) => {
const overallDurationMs = suiteFinished
? suiteDurationMs
: suiteStartTime
? Date.now() - suiteStartTime
? timeNow - suiteStartTime
: 0;

const breakdownParts = [];
Expand Down
95 changes: 95 additions & 0 deletions src/renderers/renderScheduler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import { type Config } from "../config/types";
import { type RunOptions } from "../runOptions/types";

import { render } from "./render";
import { RENDER_INTERVAL_MS, startRenderer } from "./renderScheduler";

vi.mock("./render", () => {
return {
render: vi.fn(),
};
});

const renderMock = vi.mocked(render);

const baseRunOptions: RunOptions = {
isFixMode: false,
isSilentMode: false,
isWatchMode: false,
isNoColor: false,
configPath: undefined,
};

const baseConfig: Config = {
tasks: [],
unsupportedTools: [],
executionMode: "parallel",
};

describe("startRenderer", () => {
afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});

it("starts a render interval in TTY mode and renders on each tick", () => {
vi.useFakeTimers();
Object.defineProperty(process.stdout, "isTTY", {
configurable: true,
value: true,
});

const stop = startRenderer(baseConfig, baseRunOptions);

expect(renderMock).toHaveBeenCalledTimes(1);

vi.advanceTimersByTime(RENDER_INTERVAL_MS - 1);
expect(renderMock).toHaveBeenCalledTimes(1);

vi.advanceTimersByTime(1);
expect(renderMock).toHaveBeenCalledTimes(2);

vi.advanceTimersByTime(RENDER_INTERVAL_MS);
expect(renderMock).toHaveBeenCalledTimes(3);

stop();
});

it("does not start an interval when stdout is not a TTY", () => {
vi.useFakeTimers();
Object.defineProperty(process.stdout, "isTTY", {
configurable: true,
value: false,
});

const stop = startRenderer(baseConfig, baseRunOptions);

vi.advanceTimersByTime(RENDER_INTERVAL_MS * 3);

expect(renderMock).toHaveBeenCalledTimes(1);
expect(stop).toBeTypeOf("function");

stop();
expect(renderMock).toHaveBeenCalledTimes(2);
});

it("stops rendering after cleanup", () => {
vi.useFakeTimers();
Object.defineProperty(process.stdout, "isTTY", {
configurable: true,
value: true,
});

const stop = startRenderer(baseConfig, baseRunOptions);

vi.advanceTimersByTime(RENDER_INTERVAL_MS);
expect(renderMock).toHaveBeenCalledTimes(2);

stop();
vi.advanceTimersByTime(RENDER_INTERVAL_MS * 2);

expect(renderMock).toHaveBeenCalledTimes(3);
});
});
33 changes: 33 additions & 0 deletions src/renderers/renderScheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { type Config } from "../config/types";
import { type RunOptions } from "../runOptions/types";

import { render } from "./render";

export const RENDER_INTERVAL_MS = 1000;

/**
* Starts a concurrent renderer scheduler for TTY output and returns a cleanup
* function that stops the scheduler.
*
* @param {Config} config - Config and task metadata to render.
* @param {RunOptions} runOptions - Options that influenced the run.
* @returns {() => void} Cleanup function for the renderer scheduler.
*/
export const startRenderer = (config: Config, runOptions: RunOptions) => {
render(config, runOptions);

if (!process.stdout.isTTY) {
return () => {
render(config, runOptions);
};
}

const interval = setInterval(() => {
render(config, runOptions);
}, RENDER_INTERVAL_MS);

return () => {
clearInterval(interval);
render(config, runOptions);
};
};
Loading