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 { stripVTControlCharacters } from 'util';
|
||||||
import { EOL } from 'os';
|
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 originalHrTime;
|
||||||
let originalColumns;
|
let originalColumns;
|
||||||
|
|
||||||
@ -43,6 +45,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
|||||||
args: {
|
args: {
|
||||||
targets: ['test'],
|
targets: ['test'],
|
||||||
},
|
},
|
||||||
|
taskGraph: {
|
||||||
|
tasks: { dep },
|
||||||
|
dependencies: {},
|
||||||
|
continuousDependencies: {},
|
||||||
|
roots: [],
|
||||||
|
},
|
||||||
initiatingProject: 'test',
|
initiatingProject: 'test',
|
||||||
initiatingTasks: [],
|
initiatingTasks: [],
|
||||||
overrides: {},
|
overrides: {},
|
||||||
@ -95,6 +103,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
|||||||
args: {
|
args: {
|
||||||
targets: ['test'],
|
targets: ['test'],
|
||||||
},
|
},
|
||||||
|
taskGraph: {
|
||||||
|
tasks: { dep, target },
|
||||||
|
dependencies: {},
|
||||||
|
continuousDependencies: {},
|
||||||
|
roots: [],
|
||||||
|
},
|
||||||
initiatingProject: 'test',
|
initiatingProject: 'test',
|
||||||
initiatingTasks: [],
|
initiatingTasks: [],
|
||||||
overrides: {},
|
overrides: {},
|
||||||
@ -147,6 +161,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
|||||||
},
|
},
|
||||||
initiatingProject: 'test',
|
initiatingProject: 'test',
|
||||||
initiatingTasks: [],
|
initiatingTasks: [],
|
||||||
|
taskGraph: {
|
||||||
|
tasks: { dep, target },
|
||||||
|
dependencies: {},
|
||||||
|
continuousDependencies: {},
|
||||||
|
roots: [],
|
||||||
|
},
|
||||||
overrides: {},
|
overrides: {},
|
||||||
projectNames: ['test'],
|
projectNames: ['test'],
|
||||||
tasks: [target, dep],
|
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', () => {
|
describe('runMany', () => {
|
||||||
@ -209,6 +283,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
|||||||
},
|
},
|
||||||
initiatingProject: null,
|
initiatingProject: null,
|
||||||
initiatingTasks: [],
|
initiatingTasks: [],
|
||||||
|
taskGraph: {
|
||||||
|
tasks: { foo, bar },
|
||||||
|
dependencies: {},
|
||||||
|
continuousDependencies: {},
|
||||||
|
roots: [],
|
||||||
|
},
|
||||||
overrides: {},
|
overrides: {},
|
||||||
projectNames: ['foo', 'bar'],
|
projectNames: ['foo', 'bar'],
|
||||||
tasks: [foo, bar],
|
tasks: [foo, bar],
|
||||||
@ -285,6 +365,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
|||||||
},
|
},
|
||||||
initiatingProject: null,
|
initiatingProject: null,
|
||||||
initiatingTasks: [],
|
initiatingTasks: [],
|
||||||
|
taskGraph: {
|
||||||
|
tasks: { foo, bar },
|
||||||
|
dependencies: {},
|
||||||
|
continuousDependencies: {},
|
||||||
|
roots: [],
|
||||||
|
},
|
||||||
overrides: {},
|
overrides: {},
|
||||||
projectNames: ['foo', 'bar'],
|
projectNames: ['foo', 'bar'],
|
||||||
tasks: [foo, bar],
|
tasks: [foo, bar],
|
||||||
@ -349,6 +435,12 @@ describe('getTuiTerminalSummaryLifeCycle', () => {
|
|||||||
initiatingProject: null,
|
initiatingProject: null,
|
||||||
initiatingTasks: [],
|
initiatingTasks: [],
|
||||||
overrides: {},
|
overrides: {},
|
||||||
|
taskGraph: {
|
||||||
|
tasks: { foo, bar },
|
||||||
|
dependencies: {},
|
||||||
|
continuousDependencies: {},
|
||||||
|
roots: [],
|
||||||
|
},
|
||||||
projectNames: ['foo', 'bar'],
|
projectNames: ['foo', 'bar'],
|
||||||
tasks: [foo, bar],
|
tasks: [foo, bar],
|
||||||
resolveRenderIsDonePromise: jest.fn().mockResolvedValue(null),
|
resolveRenderIsDonePromise: jest.fn().mockResolvedValue(null),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { EOL } from 'node:os';
|
import { EOL } from 'node:os';
|
||||||
import { TaskStatus as NativeTaskStatus } from '../../native';
|
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 { output } from '../../utils/output';
|
||||||
import type { LifeCycle } from '../life-cycle';
|
import type { LifeCycle } from '../life-cycle';
|
||||||
import type { TaskStatus } from '../tasks-runner';
|
import type { TaskStatus } from '../tasks-runner';
|
||||||
@ -9,6 +9,7 @@ import { prettyTime } from './pretty-time';
|
|||||||
import { viewLogsFooterRows } from './view-logs-utils';
|
import { viewLogsFooterRows } from './view-logs-utils';
|
||||||
import figures = require('figures');
|
import figures = require('figures');
|
||||||
import { getTasksHistoryLifeCycle } from './task-history-life-cycle';
|
import { getTasksHistoryLifeCycle } from './task-history-life-cycle';
|
||||||
|
import { getLeafTasks } from '../task-graph-utils';
|
||||||
|
|
||||||
const LEFT_PAD = ` `;
|
const LEFT_PAD = ` `;
|
||||||
const SPACER = ` `;
|
const SPACER = ` `;
|
||||||
@ -17,6 +18,7 @@ const EXTENDED_LEFT_PAD = ` `;
|
|||||||
export function getTuiTerminalSummaryLifeCycle({
|
export function getTuiTerminalSummaryLifeCycle({
|
||||||
projectNames,
|
projectNames,
|
||||||
tasks,
|
tasks,
|
||||||
|
taskGraph,
|
||||||
args,
|
args,
|
||||||
overrides,
|
overrides,
|
||||||
initiatingProject,
|
initiatingProject,
|
||||||
@ -25,6 +27,7 @@ export function getTuiTerminalSummaryLifeCycle({
|
|||||||
}: {
|
}: {
|
||||||
projectNames: string[];
|
projectNames: string[];
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
|
taskGraph: TaskGraph;
|
||||||
args: { targets?: string[]; configuration?: string; parallel?: number };
|
args: { targets?: string[]; configuration?: string; parallel?: number };
|
||||||
overrides: Record<string, unknown>;
|
overrides: Record<string, unknown>;
|
||||||
initiatingProject: string;
|
initiatingProject: string;
|
||||||
@ -110,17 +113,39 @@ export function getTuiTerminalSummaryLifeCycle({
|
|||||||
return;
|
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) {
|
if (isRunOne) {
|
||||||
printRunOneSummary();
|
printRunOneSummary({
|
||||||
|
failure,
|
||||||
|
cancelled,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
printRunManySummary();
|
printRunManySummary({
|
||||||
|
failure,
|
||||||
|
cancelled,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
getTasksHistoryLifeCycle()?.printFlakyTasksMessage();
|
getTasksHistoryLifeCycle()?.printFlakyTasksMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const printRunOneSummary = () => {
|
const printRunOneSummary = ({
|
||||||
|
failure,
|
||||||
|
cancelled,
|
||||||
|
}: {
|
||||||
|
failure: boolean;
|
||||||
|
cancelled: boolean;
|
||||||
|
}) => {
|
||||||
let lines: string[] = [];
|
let lines: string[] = [];
|
||||||
const failure = totalSuccessfulTasks + stoppedTasks.size !== totalTasks;
|
|
||||||
|
|
||||||
// Prints task outputs in the order they were completed
|
// Prints task outputs in the order they were completed
|
||||||
// above the summary, since run-one should print all task results.
|
// 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'));
|
lines.push(...output.getVerticalSeparatorLines(failure ? 'red' : 'green'));
|
||||||
|
|
||||||
if (!failure) {
|
if (!failure && !cancelled) {
|
||||||
const text = `Successfully ran ${formatTargetsAndProjects(
|
const text = `Successfully ran ${formatTargetsAndProjects(
|
||||||
[initiatingProject],
|
[initiatingProject],
|
||||||
targets,
|
targets,
|
||||||
@ -167,7 +192,7 @@ export function getTuiTerminalSummaryLifeCycle({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
lines = [output.colors.green(lines.join(EOL))];
|
lines = [output.colors.green(lines.join(EOL))];
|
||||||
} else if (inProgressTasks.size === 0) {
|
} else if (!cancelled) {
|
||||||
let text = `Ran target ${output.bold(
|
let text = `Ran target ${output.bold(
|
||||||
targets[0]
|
targets[0]
|
||||||
)} for project ${output.bold(initiatingProject)}`;
|
)} for project ${output.bold(initiatingProject)}`;
|
||||||
@ -230,11 +255,16 @@ export function getTuiTerminalSummaryLifeCycle({
|
|||||||
console.log(lines.join(EOL));
|
console.log(lines.join(EOL));
|
||||||
};
|
};
|
||||||
|
|
||||||
const printRunManySummary = () => {
|
const printRunManySummary = ({
|
||||||
|
failure,
|
||||||
|
cancelled,
|
||||||
|
}: {
|
||||||
|
failure: boolean;
|
||||||
|
cancelled: boolean;
|
||||||
|
}) => {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const lines: string[] = [''];
|
const lines: string[] = [''];
|
||||||
const failure = totalSuccessfulTasks + stoppedTasks.size !== totalTasks;
|
|
||||||
|
|
||||||
for (const taskId of taskIdsInOrderOfCompletion) {
|
for (const taskId of taskIdsInOrderOfCompletion) {
|
||||||
const { terminalOutput, taskStatus } = tasksToTerminalOutputs[taskId];
|
const { terminalOutput, taskStatus } = tasksToTerminalOutputs[taskId];
|
||||||
@ -296,7 +326,7 @@ export function getTuiTerminalSummaryLifeCycle({
|
|||||||
lines.push(successSummaryRows.join(EOL));
|
lines.push(successSummaryRows.join(EOL));
|
||||||
} else {
|
} else {
|
||||||
const text = `${
|
const text = `${
|
||||||
inProgressTasks.size ? 'Cancelled while running' : 'Ran'
|
cancelled ? 'Cancelled while running' : 'Ran'
|
||||||
} ${formatTargetsAndProjects(projectNames, targets, tasks)}`;
|
} ${formatTargetsAndProjects(projectNames, targets, tasks)}`;
|
||||||
const taskOverridesRows: string[] = [];
|
const taskOverridesRows: string[] = [];
|
||||||
if (Object.keys(overrides).length > 0) {
|
if (Object.keys(overrides).length > 0) {
|
||||||
|
|||||||
@ -60,6 +60,7 @@ import { getTuiTerminalSummaryLifeCycle } from './life-cycles/tui-summary-life-c
|
|||||||
import {
|
import {
|
||||||
assertTaskGraphDoesNotContainInvalidTargets,
|
assertTaskGraphDoesNotContainInvalidTargets,
|
||||||
findCycle,
|
findCycle,
|
||||||
|
getLeafTasks,
|
||||||
makeAcyclic,
|
makeAcyclic,
|
||||||
validateNoAtomizedTasks,
|
validateNoAtomizedTasks,
|
||||||
} from './task-graph-utils';
|
} from './task-graph-utils';
|
||||||
@ -170,6 +171,7 @@ async function getTerminalOutputLifeCycle(
|
|||||||
getTuiTerminalSummaryLifeCycle({
|
getTuiTerminalSummaryLifeCycle({
|
||||||
projectNames,
|
projectNames,
|
||||||
tasks,
|
tasks,
|
||||||
|
taskGraph,
|
||||||
args: nxArgs,
|
args: nxArgs,
|
||||||
overrides: overridesWithoutHidden,
|
overrides: overridesWithoutHidden,
|
||||||
initiatingProject,
|
initiatingProject,
|
||||||
@ -520,7 +522,10 @@ export async function runCommandForTasks(
|
|||||||
|
|
||||||
await printNxKey();
|
await printNxKey();
|
||||||
|
|
||||||
return { taskResults, completed: didCommandComplete(tasks, taskResults) };
|
return {
|
||||||
|
taskResults,
|
||||||
|
completed: didCommandComplete(tasks, taskGraph, taskResults),
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (restoreTerminal) {
|
if (restoreTerminal) {
|
||||||
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 no tasks, then we can consider it complete
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let everyTaskIsContinuous = true;
|
let continousLeafTasks = false;
|
||||||
|
const leafTasks = getLeafTasks(taskGraph);
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
if (!task.continuous) {
|
if (!task.continuous) {
|
||||||
everyTaskIsContinuous = false;
|
|
||||||
|
|
||||||
// If any discrete task does not have a result then it did not run
|
// If any discrete task does not have a result then it did not run
|
||||||
if (!taskResults[task.id]) {
|
if (!taskResults[task.id]) {
|
||||||
return false;
|
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
|
// Otherwise, we've looped through all the discrete tasks and they have results
|
||||||
return !everyTaskIsContinuous;
|
return !continousLeafTasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureWorkspaceIsInSyncAndGetGraphs(
|
async function ensureWorkspaceIsInSyncAndGetGraphs(
|
||||||
|
|||||||
@ -192,3 +192,37 @@ class NonParallelTaskDependsOnContinuousTasksError extends Error {
|
|||||||
this.name = 'NonParallelTaskDependsOnContinuousTasksError';
|
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