fix(core): tui summary should show cancelled when interrupting dev server (#31042)
## Current Behavior Interrupting a serve task with `Control + C` displays a "Success" message, which isn't quite accurate. <img width="1077" alt="image" src="https://github.com/user-attachments/assets/b7e7086d-2725-4c65-b1f6-9f8a5db5196a" /> ## Expected Behavior Interrupting a serve task displays a "Cancelled" message <img width="1077" alt="image" src="https://github.com/user-attachments/assets/698e6e43-a376-473a-ab4f-7d514026ff02" /> ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
ce64f85069
commit
9fe487c0f3
@ -1,8 +1,10 @@
|
||||
import { getTuiTerminalSummaryLifeCycle } from './tui-summary-life-cycle';
|
||||
import { Task } from '../../config/task-graph';
|
||||
import { stripVTControlCharacters } from 'util';
|
||||
import { EOL } from 'os';
|
||||
|
||||
import { getTuiTerminalSummaryLifeCycle } from './tui-summary-life-cycle';
|
||||
import { Task } from '../../config/task-graph';
|
||||
import { TaskStatus as NativeTaskStatus } from '../../native';
|
||||
|
||||
let originalHrTime;
|
||||
let originalColumns;
|
||||
|
||||
@ -43,6 +45,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
||||
args: {
|
||||
targets: ['test'],
|
||||
},
|
||||
taskGraph: {
|
||||
tasks: { dep },
|
||||
dependencies: {},
|
||||
continuousDependencies: {},
|
||||
roots: [],
|
||||
},
|
||||
initiatingProject: 'test',
|
||||
initiatingTasks: [],
|
||||
overrides: {},
|
||||
@ -95,6 +103,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
||||
args: {
|
||||
targets: ['test'],
|
||||
},
|
||||
taskGraph: {
|
||||
tasks: { dep, target },
|
||||
dependencies: {},
|
||||
continuousDependencies: {},
|
||||
roots: [],
|
||||
},
|
||||
initiatingProject: 'test',
|
||||
initiatingTasks: [],
|
||||
overrides: {},
|
||||
@ -147,6 +161,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
||||
},
|
||||
initiatingProject: 'test',
|
||||
initiatingTasks: [],
|
||||
taskGraph: {
|
||||
tasks: { dep, target },
|
||||
dependencies: {},
|
||||
continuousDependencies: {},
|
||||
roots: [],
|
||||
},
|
||||
overrides: {},
|
||||
projectNames: ['test'],
|
||||
tasks: [target, dep],
|
||||
@ -184,6 +204,60 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should display cancelled for single continuous task', async () => {
|
||||
const target = {
|
||||
id: 'test:dev',
|
||||
continuous: true,
|
||||
target: {
|
||||
target: 'dev',
|
||||
project: 'test',
|
||||
},
|
||||
} as Partial<Task> as Task;
|
||||
|
||||
const { lifeCycle, printSummary } = getTuiTerminalSummaryLifeCycle({
|
||||
args: {
|
||||
targets: ['dev'],
|
||||
},
|
||||
initiatingProject: 'test',
|
||||
initiatingTasks: [],
|
||||
taskGraph: {
|
||||
tasks: { [target.id]: target },
|
||||
dependencies: {
|
||||
[target.id]: [],
|
||||
},
|
||||
continuousDependencies: {},
|
||||
roots: [],
|
||||
},
|
||||
overrides: {},
|
||||
projectNames: ['test'],
|
||||
tasks: [target],
|
||||
resolveRenderIsDonePromise: jest.fn().mockResolvedValue(null),
|
||||
});
|
||||
|
||||
lifeCycle.startTasks([target], null);
|
||||
lifeCycle.setTaskStatus(target.id, NativeTaskStatus.Stopped);
|
||||
lifeCycle.printTaskTerminalOutput(
|
||||
target,
|
||||
'success',
|
||||
'I was a happy dev server'
|
||||
);
|
||||
lifeCycle.endCommand();
|
||||
// Continuous tasks are marked as stopped when the command is stopped
|
||||
|
||||
const lines = getOutputLines(printSummary);
|
||||
|
||||
expect(lines.join('\n')).toMatchInlineSnapshot(`
|
||||
"
|
||||
> nx run test:dev
|
||||
|
||||
I was a happy dev server
|
||||
———————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
NX Cancelled running target dev for project test (37w)
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runMany', () => {
|
||||
@ -209,6 +283,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
||||
},
|
||||
initiatingProject: null,
|
||||
initiatingTasks: [],
|
||||
taskGraph: {
|
||||
tasks: { foo, bar },
|
||||
dependencies: {},
|
||||
continuousDependencies: {},
|
||||
roots: [],
|
||||
},
|
||||
overrides: {},
|
||||
projectNames: ['foo', 'bar'],
|
||||
tasks: [foo, bar],
|
||||
@ -285,6 +365,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
||||
},
|
||||
initiatingProject: null,
|
||||
initiatingTasks: [],
|
||||
taskGraph: {
|
||||
tasks: { foo, bar },
|
||||
dependencies: {},
|
||||
continuousDependencies: {},
|
||||
roots: [],
|
||||
},
|
||||
overrides: {},
|
||||
projectNames: ['foo', 'bar'],
|
||||
tasks: [foo, bar],
|
||||
@ -349,6 +435,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
||||
initiatingProject: null,
|
||||
initiatingTasks: [],
|
||||
overrides: {},
|
||||
taskGraph: {
|
||||
tasks: { foo, bar },
|
||||
dependencies: {},
|
||||
continuousDependencies: {},
|
||||
roots: [],
|
||||
},
|
||||
projectNames: ['foo', 'bar'],
|
||||
tasks: [foo, bar],
|
||||
resolveRenderIsDonePromise: jest.fn().mockResolvedValue(null),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { EOL } from 'node:os';
|
||||
import { TaskStatus as NativeTaskStatus } from '../../native';
|
||||
import { Task } from '../../config/task-graph';
|
||||
import { Task, TaskGraph } from '../../config/task-graph';
|
||||
import { output } from '../../utils/output';
|
||||
import type { LifeCycle } from '../life-cycle';
|
||||
import type { TaskStatus } from '../tasks-runner';
|
||||
@ -9,6 +9,7 @@ import { prettyTime } from './pretty-time';
|
||||
import { viewLogsFooterRows } from './view-logs-utils';
|
||||
import figures = require('figures');
|
||||
import { getTasksHistoryLifeCycle } from './task-history-life-cycle';
|
||||
import { getLeafTasks } from '../task-graph-utils';
|
||||
|
||||
const LEFT_PAD = ` `;
|
||||
const SPACER = ` `;
|
||||
@ -17,6 +18,7 @@ const EXTENDED_LEFT_PAD = ` `;
|
||||
export function getTuiTerminalSummaryLifeCycle({
|
||||
projectNames,
|
||||
tasks,
|
||||
taskGraph,
|
||||
args,
|
||||
overrides,
|
||||
initiatingProject,
|
||||
@ -25,6 +27,7 @@ export function getTuiTerminalSummaryLifeCycle({
|
||||
}: {
|
||||
projectNames: string[];
|
||||
tasks: Task[];
|
||||
taskGraph: TaskGraph;
|
||||
args: { targets?: string[]; configuration?: string; parallel?: number };
|
||||
overrides: Record<string, unknown>;
|
||||
initiatingProject: string;
|
||||
@ -110,17 +113,39 @@ export function getTuiTerminalSummaryLifeCycle({
|
||||
return;
|
||||
}
|
||||
|
||||
const failure = totalSuccessfulTasks + stoppedTasks.size !== totalTasks;
|
||||
const cancelled =
|
||||
// Some tasks were in progress...
|
||||
inProgressTasks.size > 0 ||
|
||||
// or some tasks had not started yet
|
||||
totalTasks !== tasks.length ||
|
||||
// the run had a continous task as a leaf...
|
||||
// this is needed because continous tasks get marked as stopped when the process is interrupted
|
||||
[...getLeafTasks(taskGraph)].filter((t) => taskGraph.tasks[t].continuous)
|
||||
.length > 0;
|
||||
|
||||
if (isRunOne) {
|
||||
printRunOneSummary();
|
||||
printRunOneSummary({
|
||||
failure,
|
||||
cancelled,
|
||||
});
|
||||
} else {
|
||||
printRunManySummary();
|
||||
printRunManySummary({
|
||||
failure,
|
||||
cancelled,
|
||||
});
|
||||
}
|
||||
getTasksHistoryLifeCycle()?.printFlakyTasksMessage();
|
||||
};
|
||||
|
||||
const printRunOneSummary = () => {
|
||||
const printRunOneSummary = ({
|
||||
failure,
|
||||
cancelled,
|
||||
}: {
|
||||
failure: boolean;
|
||||
cancelled: boolean;
|
||||
}) => {
|
||||
let lines: string[] = [];
|
||||
const failure = totalSuccessfulTasks + stoppedTasks.size !== totalTasks;
|
||||
|
||||
// Prints task outputs in the order they were completed
|
||||
// above the summary, since run-one should print all task results.
|
||||
@ -131,7 +156,7 @@ export function getTuiTerminalSummaryLifeCycle({
|
||||
|
||||
lines.push(...output.getVerticalSeparatorLines(failure ? 'red' : 'green'));
|
||||
|
||||
if (!failure) {
|
||||
if (!failure && !cancelled) {
|
||||
const text = `Successfully ran ${formatTargetsAndProjects(
|
||||
[initiatingProject],
|
||||
targets,
|
||||
@ -167,7 +192,7 @@ export function getTuiTerminalSummaryLifeCycle({
|
||||
);
|
||||
}
|
||||
lines = [output.colors.green(lines.join(EOL))];
|
||||
} else if (inProgressTasks.size === 0) {
|
||||
} else if (!cancelled) {
|
||||
let text = `Ran target ${output.bold(
|
||||
targets[0]
|
||||
)} for project ${output.bold(initiatingProject)}`;
|
||||
@ -230,11 +255,16 @@ export function getTuiTerminalSummaryLifeCycle({
|
||||
console.log(lines.join(EOL));
|
||||
};
|
||||
|
||||
const printRunManySummary = () => {
|
||||
const printRunManySummary = ({
|
||||
failure,
|
||||
cancelled,
|
||||
}: {
|
||||
failure: boolean;
|
||||
cancelled: boolean;
|
||||
}) => {
|
||||
console.log('');
|
||||
|
||||
const lines: string[] = [''];
|
||||
const failure = totalSuccessfulTasks + stoppedTasks.size !== totalTasks;
|
||||
|
||||
for (const taskId of taskIdsInOrderOfCompletion) {
|
||||
const { terminalOutput, taskStatus } = tasksToTerminalOutputs[taskId];
|
||||
@ -296,7 +326,7 @@ export function getTuiTerminalSummaryLifeCycle({
|
||||
lines.push(successSummaryRows.join(EOL));
|
||||
} else {
|
||||
const text = `${
|
||||
inProgressTasks.size ? 'Cancelled while running' : 'Ran'
|
||||
cancelled ? 'Cancelled while running' : 'Ran'
|
||||
} ${formatTargetsAndProjects(projectNames, targets, tasks)}`;
|
||||
const taskOverridesRows: string[] = [];
|
||||
if (Object.keys(overrides).length > 0) {
|
||||
|
||||
@ -60,6 +60,7 @@ import { getTuiTerminalSummaryLifeCycle } from './life-cycles/tui-summary-life-c
|
||||
import {
|
||||
assertTaskGraphDoesNotContainInvalidTargets,
|
||||
findCycle,
|
||||
getLeafTasks,
|
||||
makeAcyclic,
|
||||
validateNoAtomizedTasks,
|
||||
} from './task-graph-utils';
|
||||
@ -170,6 +171,7 @@ async function getTerminalOutputLifeCycle(
|
||||
getTuiTerminalSummaryLifeCycle({
|
||||
projectNames,
|
||||
tasks,
|
||||
taskGraph,
|
||||
args: nxArgs,
|
||||
overrides: overridesWithoutHidden,
|
||||
initiatingProject,
|
||||
@ -520,7 +522,10 @@ export async function runCommandForTasks(
|
||||
|
||||
await printNxKey();
|
||||
|
||||
return { taskResults, completed: didCommandComplete(tasks, taskResults) };
|
||||
return {
|
||||
taskResults,
|
||||
completed: didCommandComplete(tasks, taskGraph, taskResults),
|
||||
};
|
||||
} catch (e) {
|
||||
if (restoreTerminal) {
|
||||
restoreTerminal();
|
||||
@ -529,27 +534,34 @@ export async function runCommandForTasks(
|
||||
}
|
||||
}
|
||||
|
||||
function didCommandComplete(tasks: Task[], taskResults: TaskResults): boolean {
|
||||
function didCommandComplete(
|
||||
tasks: Task[],
|
||||
taskGraph: TaskGraph,
|
||||
taskResults: TaskResults
|
||||
): boolean {
|
||||
// If no tasks, then we can consider it complete
|
||||
if (tasks.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let everyTaskIsContinuous = true;
|
||||
let continousLeafTasks = false;
|
||||
const leafTasks = getLeafTasks(taskGraph);
|
||||
for (const task of tasks) {
|
||||
if (!task.continuous) {
|
||||
everyTaskIsContinuous = false;
|
||||
|
||||
// If any discrete task does not have a result then it did not run
|
||||
if (!taskResults[task.id]) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (leafTasks.has(task.id)) {
|
||||
continousLeafTasks = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If every task is continuous, command cannot complete by definition
|
||||
// If a leaf task is continous, we must have cancelled it.
|
||||
// Otherwise, we've looped through all the discrete tasks and they have results
|
||||
return !everyTaskIsContinuous;
|
||||
return !continousLeafTasks;
|
||||
}
|
||||
|
||||
async function ensureWorkspaceIsInSyncAndGetGraphs(
|
||||
|
||||
@ -192,3 +192,37 @@ class NonParallelTaskDependsOnContinuousTasksError extends Error {
|
||||
this.name = 'NonParallelTaskDependsOnContinuousTasksError';
|
||||
}
|
||||
}
|
||||
|
||||
export function getLeafTasks(taskGraph: TaskGraph): Set<string> {
|
||||
const reversed = reverseTaskGraph(taskGraph);
|
||||
const leafTasks = new Set<string>();
|
||||
for (const [taskId, dependencies] of Object.entries(reversed.dependencies)) {
|
||||
if (dependencies.length === 0) {
|
||||
leafTasks.add(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
return leafTasks;
|
||||
}
|
||||
|
||||
function reverseTaskGraph(taskGraph: TaskGraph): TaskGraph {
|
||||
const reversed = {
|
||||
tasks: taskGraph.tasks,
|
||||
dependencies: Object.fromEntries(
|
||||
Object.entries(taskGraph.tasks).map(([taskId]) => [taskId, []])
|
||||
),
|
||||
} as TaskGraph;
|
||||
for (const [taskId, dependencies] of Object.entries(taskGraph.dependencies)) {
|
||||
for (const dependency of dependencies) {
|
||||
reversed.dependencies[dependency].push(taskId);
|
||||
}
|
||||
}
|
||||
for (const [taskId, dependencies] of Object.entries(
|
||||
taskGraph.continuousDependencies
|
||||
)) {
|
||||
for (const dependency of dependencies) {
|
||||
reversed.dependencies[dependency].push(taskId);
|
||||
}
|
||||
}
|
||||
return reversed;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user