fix(testing): jest batch mode improvements (#13744)

This commit is contained in:
Caleb Ukle 2023-01-27 11:23:56 -06:00 committed by GitHub
parent 6422d3da9d
commit 0f15c140fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 146 additions and 24 deletions

View File

@ -1,5 +1,5 @@
/* eslint-disable */ /* eslint-disable */
const nxPreset = require('@nrwl/jest/preset'); import nxPreset from '@nrwl/jest/preset';
module.exports = { module.exports = {
...nxPreset, ...nxPreset,

View File

@ -15,7 +15,7 @@ import {
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { getSummary } from './summary'; import { getSummary } from './summary';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import type { BatchResults } from 'nx/src/tasks-runner/batch/batch-messages';
process.env.NODE_ENV ??= 'test'; process.env.NODE_ENV ??= 'test';
export async function jestExecutor( export async function jestExecutor(
@ -155,7 +155,7 @@ export async function batchJest(
inputs: Record<string, JestExecutorOptions>, inputs: Record<string, JestExecutorOptions>,
overrides: JestExecutorOptions, overrides: JestExecutorOptions,
context: ExecutorContext context: ExecutorContext
): Promise<Record<string, { success: boolean; terminalOutput: string }>> { ): Promise<BatchResults> {
let configPaths: string[] = []; let configPaths: string[] = [];
let selectedProjects: string[] = []; let selectedProjects: string[] = [];
let projectsWithNoName: [string, string][] = []; let projectsWithNoName: [string, string][] = [];
@ -202,24 +202,29 @@ export async function batchJest(
}, },
[workspaceRoot] [workspaceRoot]
); );
const { configs } = await readConfigs({ $0: undefined, _: [] }, configPaths); const { configs } = await readConfigs({ $0: undefined, _: [] }, configPaths);
const jestTaskExecutionResults: Record< const jestTaskExecutionResults: BatchResults = {};
string,
{ success: boolean; terminalOutput: string }
> = {};
for (let i = 0; i < taskGraph.roots.length; i++) { for (let i = 0; i < taskGraph.roots.length; i++) {
let root = taskGraph.roots[i]; let root = taskGraph.roots[i];
const aggregatedResults = makeEmptyAggregatedTestResult(); const aggregatedResults = makeEmptyAggregatedTestResult();
aggregatedResults.startTime = results.startTime; aggregatedResults.startTime = results.startTime;
let endTime: number;
const projectRoot = join(context.root, taskGraph.tasks[root].projectRoot); const projectRoot = join(context.root, taskGraph.tasks[root].projectRoot);
let resultOutput = ''; let resultOutput = '';
for (const testResult of results.testResults) { for (const testResult of results.testResults) {
if (testResult.testFilePath.startsWith(projectRoot)) { if (testResult.testFilePath.startsWith(projectRoot)) {
aggregatedResults.startTime = aggregatedResults.startTime
? Math.min(aggregatedResults.startTime, testResult.perfStats.start)
: testResult.perfStats.start;
endTime = endTime
? Math.max(testResult.perfStats.end, endTime)
: testResult.perfStats.end;
addResult(aggregatedResults, testResult); addResult(aggregatedResults, testResult);
resultOutput += resultOutput +=
'\n\r' + '\n\r' +
jestReporterUtils.getResultHeader( jestReporterUtils.getResultHeader(
@ -229,10 +234,15 @@ export async function batchJest(
); );
} }
} }
aggregatedResults.numTotalTestSuites = aggregatedResults.testResults.length; aggregatedResults.numTotalTestSuites = aggregatedResults.testResults.length;
jestTaskExecutionResults[root] = { jestTaskExecutionResults[root] = {
startTime: aggregatedResults.startTime,
endTime,
success: aggregatedResults.numFailedTests === 0, success: aggregatedResults.numFailedTests === 0,
// TODO(caleb): getSummary assumed endTime is Date.now().
// might need to make own method to correctly set the endtime base on tests instead of _now_
terminalOutput: resultOutput + '\n\r\n\r' + getSummary(aggregatedResults), terminalOutput: resultOutput + '\n\r\n\r' + getSummary(aggregatedResults),
}; };
} }

View File

@ -56,6 +56,16 @@ export interface Task {
*/ */
runtime?: { [input: string]: string }; runtime?: { [input: string]: string };
}; };
/**
*
* Unix timestamp of when a Batch Task starts
**/
startTime?: number;
/**
*
* Unix timestamp of when a Batch Task ends
**/
endTime?: number;
} }
/** /**

View File

@ -14,7 +14,12 @@ export interface BatchTasksMessage {
* Results of running the batch. Mapped from task id to results * Results of running the batch. Mapped from task id to results
*/ */
export interface BatchResults { export interface BatchResults {
[taskId: string]: { success: boolean; terminalOutput?: string }; [taskId: string]: {
success: boolean;
terminalOutput?: string;
startTime?: number;
endTime?: number;
};
} }
export interface BatchCompleteMessage { export interface BatchCompleteMessage {
type: BatchMessageType.Complete; type: BatchMessageType.Complete;

View File

@ -80,7 +80,6 @@ process.on('message', async (message: BatchMessage) => {
type: BatchMessageType.Complete, type: BatchMessageType.Complete,
results, results,
} as BatchCompleteMessage); } as BatchCompleteMessage);
process.exit(0);
} }
} }
}); });

View File

@ -1,5 +1,81 @@
import { Task } from 'nx/src/config/task-graph';
import { TaskStatus } from '../tasks-runner';
import { StoreRunInformationLifeCycle } from './store-run-information-life-cycle'; import { StoreRunInformationLifeCycle } from './store-run-information-life-cycle';
describe('StoreRunInformationLifeCycle', () => { describe('StoreRunInformationLifeCycle', () => {
it.only('should handle startTime/endTime in TaskResults', () => {
let runDetails;
const store = new StoreRunInformationLifeCycle(
'nx run-many --target=test',
(res) => (runDetails = res),
() => 'DATE'
);
store.startCommand();
store.startTasks([{ id: 'proj1:test' }, { id: 'proj2:test' }] as any);
store.endTasks([
{
task: {
id: 'proj1:test',
target: { target: 'test', project: 'proj1' },
hash: 'hash1',
startTime: new Date('2020-01-0T10:00:00:000Z').getTime(),
endTime: new Date('2020-01-0T10:00:02:000Z').getTime(),
},
status: 'cache-miss',
code: 0,
},
{
task: {
id: 'proj2:test',
target: { target: 'test', project: 'proj2' },
hash: 'hash2',
startTime: new Date('2020-01-0T10:00:01:000Z').getTime(),
endTime: new Date('2020-01-0T10:00:04:000Z').getTime(),
},
status: 'cache-miss',
code: 0,
},
] as any);
store.endCommand();
expect(runDetails).toMatchInlineSnapshot(`
Object {
"run": Object {
"command": "nx run-many --target=test",
"endTime": "DATE",
"inner": false,
"startTime": "DATE",
},
"tasks": Array [
Object {
"cacheStatus": "cache-miss",
"endTime": "DATE",
"hash": "hash1",
"params": "",
"projectName": "proj1",
"startTime": "DATE",
"status": 0,
"target": "test",
"taskId": "proj1:test",
},
Object {
"cacheStatus": "cache-miss",
"endTime": "DATE",
"hash": "hash2",
"params": "",
"projectName": "proj2",
"startTime": "DATE",
"status": 0,
"target": "test",
"taskId": "proj2:test",
},
],
}
`);
});
it('should create run details', () => { it('should create run details', () => {
let runDetails; let runDetails;
const store = new StoreRunInformationLifeCycle( const store = new StoreRunInformationLifeCycle(
@ -15,7 +91,7 @@ describe('StoreRunInformationLifeCycle', () => {
{ id: 'proj2:test' }, { id: 'proj2:test' },
{ id: 'proj3:test' }, { id: 'proj3:test' },
{ id: 'proj4:test' }, { id: 'proj4:test' },
] as any); ] as Task[]);
store.endTasks([ store.endTasks([
{ {
@ -54,7 +130,7 @@ describe('StoreRunInformationLifeCycle', () => {
status: 'cache-miss', status: 'cache-miss',
code: 1, code: 1,
}, },
] as any); ] as Array<{ task: Task; status: TaskStatus; code: number }>);
store.endCommand(); store.endCommand();

View File

@ -40,8 +40,16 @@ export class StoreRunInformationLifeCycle implements LifeCycle {
taskResults: Array<{ task: Task; status: TaskStatus; code: number }> taskResults: Array<{ task: Task; status: TaskStatus; code: number }>
): void { ): void {
for (let tr of taskResults) { for (let tr of taskResults) {
if (tr.task.endTime && tr.task.startTime) {
this.timings[tr.task.id].start = new Date(
tr.task.startTime
).toISOString();
this.timings[tr.task.id].end = new Date(tr.task.endTime).toISOString();
} else {
this.timings[tr.task.id].end = this.now(); this.timings[tr.task.id].end = this.now();
} }
}
this.taskResults.push(...taskResults); this.taskResults.push(...taskResults);
} }

View File

@ -37,9 +37,12 @@ export class TaskProfilingLifeCycle implements LifeCycle {
metadata: TaskMetadata metadata: TaskMetadata
): void { ): void {
for (let tr of taskResults) { for (let tr of taskResults) {
this.timings[ if (tr.task.endTime && tr.task.startTime) {
`${tr.task.target.project}:${tr.task.target.target}` this.timings[tr.task.id].perfStart = tr.task.startTime;
].perfEnd = performance.now(); this.timings[tr.task.id].perfEnd = tr.task.endTime;
} else {
this.timings[tr.task.id].perfEnd = performance.now();
}
} }
this.recordTaskCompletions(taskResults, metadata); this.recordTaskCompletions(taskResults, metadata);
} }
@ -54,8 +57,7 @@ export class TaskProfilingLifeCycle implements LifeCycle {
{ groupId }: TaskMetadata { groupId }: TaskMetadata
) { ) {
for (const { task, status } of tasks) { for (const { task, status } of tasks) {
const { perfStart, perfEnd } = const { perfStart, perfEnd } = this.timings[task.id];
this.timings[`${task.target.project}:${task.target.target}`];
this.profile.push({ this.profile.push({
name: task.id, name: task.id,
cat: Object.values(task.target).join(','), cat: Object.values(task.target).join(','),

View File

@ -12,7 +12,7 @@ export class TaskTimingsLifeCycle implements LifeCycle {
startTasks(tasks: Task[]): void { startTasks(tasks: Task[]): void {
for (let t of tasks) { for (let t of tasks) {
this.timings[`${t.target.project}:${t.target.target}`] = { this.timings[t.id] = {
start: new Date().getTime(), start: new Date().getTime(),
end: undefined, end: undefined,
}; };
@ -20,11 +20,19 @@ export class TaskTimingsLifeCycle implements LifeCycle {
} }
endTasks( endTasks(
taskResults: Array<{ task: Task; status: TaskStatus; code: number }> taskResults: Array<{
task: Task;
status: TaskStatus;
code: number;
}>
): void { ): void {
for (let tr of taskResults) { for (let tr of taskResults) {
this.timings[`${tr.task.target.project}:${tr.task.target.target}`].end = if (tr.task.endTime && tr.task.startTime) {
new Date().getTime(); this.timings[tr.task.id].start = tr.task.startTime;
this.timings[tr.task.id].end = tr.task.endTime;
} else {
this.timings[tr.task.id].end = new Date().getTime();
}
} }
} }

View File

@ -161,6 +161,9 @@ export async function runCommand(
} }
const tasks = Object.values(taskGraph.tasks); const tasks = Object.values(taskGraph.tasks);
if (process.env.NX_BATCH_MODE === 'true') {
nxArgs.outputStyle = 'stream';
}
if (nxArgs.outputStyle == 'stream') { if (nxArgs.outputStyle == 'stream') {
process.env.NX_STREAM_OUTPUT = 'true'; process.env.NX_STREAM_OUTPUT = 'true';
process.env.NX_PREFIX_OUTPUT = 'true'; process.env.NX_PREFIX_OUTPUT = 'true';

View File

@ -229,6 +229,7 @@ export class TaskOrchestrator {
); );
const batchResultEntries = Object.entries(results); const batchResultEntries = Object.entries(results);
return batchResultEntries.map(([taskId, result]) => ({ return batchResultEntries.map(([taskId, result]) => ({
...result,
task: this.taskGraph.tasks[taskId], task: this.taskGraph.tasks[taskId],
status: (result.success ? 'success' : 'failure') as TaskStatus, status: (result.success ? 'success' : 'failure') as TaskStatus,
terminalOutput: result.terminalOutput, terminalOutput: result.terminalOutput,
@ -359,7 +360,6 @@ export class TaskOrchestrator {
this.cache.put(task, terminalOutput, outputs, code) this.cache.put(task, terminalOutput, outputs, code)
) )
); );
this.options.lifeCycle.endTasks( this.options.lifeCycle.endTasks(
results.map((result) => { results.map((result) => {
const code = const code =
@ -370,6 +370,7 @@ export class TaskOrchestrator {
? 0 ? 0
: 1; : 1;
return { return {
...result,
task: result.task, task: result.task,
status: result.status, status: result.status,
code, code,