From 453017b92523ba693e134b1011b9971f7f1652d4 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Wed, 3 Jul 2024 13:28:10 -0400 Subject: [PATCH] feat(core): add parallelism to target configuration --- docs/generated/devkit/TargetConfiguration.md | 10 + docs/generated/devkit/Task.md | 9 + packages/cypress/src/plugins/plugin.spec.ts | 4 + packages/cypress/src/plugins/plugin.ts | 3 + .../js/src/utils/buildable-libs-utils.spec.ts | 13 + packages/nx/schemas/project-schema.json | 26 ++ packages/nx/src/config/task-graph.ts | 5 + .../nx/src/config/to-project-name.spec.ts | 1 + .../src/config/workspace-json-project-json.ts | 6 + packages/nx/src/hasher/task-hasher.spec.ts | 52 +++ .../utils/project-configuration-utils.spec.ts | 2 + .../utils/project-configuration-utils.ts | 2 + .../nx/src/tasks-runner/create-task-graph.ts | 3 +- .../src/tasks-runner/tasks-schedule.spec.ts | 404 +++++++++++++++++- .../nx/src/tasks-runner/tasks-schedule.ts | 26 +- .../playwright/src/plugins/plugin.spec.ts | 7 + packages/playwright/src/plugins/plugin.ts | 2 + 17 files changed, 570 insertions(+), 5 deletions(-) diff --git a/docs/generated/devkit/TargetConfiguration.md b/docs/generated/devkit/TargetConfiguration.md index ef01902f857fc..ac5ef2b463a36 100644 --- a/docs/generated/devkit/TargetConfiguration.md +++ b/docs/generated/devkit/TargetConfiguration.md @@ -22,6 +22,7 @@ Target's configuration - [metadata](../../devkit/documents/TargetConfiguration#metadata): TargetMetadata - [options](../../devkit/documents/TargetConfiguration#options): T - [outputs](../../devkit/documents/TargetConfiguration#outputs): string[] +- [parallelism](../../devkit/documents/TargetConfiguration#parallelism): boolean ## Properties @@ -109,3 +110,12 @@ Target's options. They are passed in to the executor. List of the target's outputs. The outputs will be cached by the Nx computation caching engine. + +--- + +### parallelism + +• `Optional` **parallelism**: `boolean` + +Whether this target can be run in parallel with other tasks +Default is true diff --git a/docs/generated/devkit/Task.md b/docs/generated/devkit/Task.md index 686bac2a5e6f2..10f6d600f80f1 100644 --- a/docs/generated/devkit/Task.md +++ b/docs/generated/devkit/Task.md @@ -13,6 +13,7 @@ A representation of the invocation of an Executor - [id](../../devkit/documents/Task#id): string - [outputs](../../devkit/documents/Task#outputs): string[] - [overrides](../../devkit/documents/Task#overrides): any +- [parallelism](../../devkit/documents/Task#parallelism): boolean - [projectRoot](../../devkit/documents/Task#projectroot): string - [startTime](../../devkit/documents/Task#starttime): number - [target](../../devkit/documents/Task#target): Object @@ -84,6 +85,14 @@ Overrides for the configured options of the target --- +### parallelism + +• **parallelism**: `boolean` + +Determines if a given task should be parallelizable. + +--- + ### projectRoot • `Optional` **projectRoot**: `string` diff --git a/packages/cypress/src/plugins/plugin.spec.ts b/packages/cypress/src/plugins/plugin.spec.ts index aced265f5ef53..6393344224965 100644 --- a/packages/cypress/src/plugins/plugin.spec.ts +++ b/packages/cypress/src/plugins/plugin.spec.ts @@ -121,6 +121,7 @@ describe('@nx/cypress/plugin', () => { "{projectRoot}/dist/videos", "{projectRoot}/dist/screenshots", ], + "parallelism": false, }, "open-cypress": { "command": "cypress open", @@ -329,6 +330,7 @@ describe('@nx/cypress/plugin', () => { "{projectRoot}/dist/videos", "{projectRoot}/dist/screenshots", ], + "parallelism": false, }, "e2e-ci": { "cache": true, @@ -369,6 +371,7 @@ describe('@nx/cypress/plugin', () => { "{projectRoot}/dist/videos", "{projectRoot}/dist/screenshots", ], + "parallelism": false, }, "e2e-ci--src/test.cy.ts": { "cache": true, @@ -404,6 +407,7 @@ describe('@nx/cypress/plugin', () => { "{projectRoot}/dist/videos", "{projectRoot}/dist/screenshots", ], + "parallelism": false, }, "open-cypress": { "command": "cypress open", diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts index d43489cf500f6..15a094bbaf3b5 100644 --- a/packages/cypress/src/plugins/plugin.ts +++ b/packages/cypress/src/plugins/plugin.ts @@ -211,6 +211,7 @@ async function buildCypressTargets( cache: true, inputs: getInputs(namedInputs), outputs: getOutputs(projectRoot, cypressConfig, 'e2e'), + parallelism: false, metadata: { technologies: ['cypress'], description: 'Runs Cypress Tests', @@ -276,6 +277,7 @@ async function buildCypressTargets( options: { cwd: projectRoot, }, + parallelism: false, metadata: { technologies: ['cypress'], description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`, @@ -300,6 +302,7 @@ async function buildCypressTargets( inputs, outputs, dependsOn, + parallelism: false, metadata: { technologies: ['cypress'], description: 'Runs Cypress Tests in CI', diff --git a/packages/js/src/utils/buildable-libs-utils.spec.ts b/packages/js/src/utils/buildable-libs-utils.spec.ts index bf987d362344e..84f9f331f533a 100644 --- a/packages/js/src/utils/buildable-libs-utils.spec.ts +++ b/packages/js/src/utils/buildable-libs-utils.spec.ts @@ -389,30 +389,35 @@ describe('calculateDependenciesFromTaskGraph', () => { overrides: {}, target: { project: 'lib1', target: 'build' }, outputs: [], + parallelism: true, }, 'lib2:build': { id: 'lib2:build', overrides: {}, target: { project: 'lib2', target: 'build' }, outputs: [], + parallelism: true, }, 'lib2:build-base': { id: 'lib2:build-base', overrides: {}, target: { project: 'lib2', target: 'build-base' }, outputs: [], + parallelism: true, }, 'lib3:build': { id: 'lib3:build', overrides: {}, target: { project: 'lib3', target: 'build' }, outputs: [], + parallelism: true, }, 'lib4:build': { id: 'lib4:build', overrides: {}, target: { project: 'lib4', target: 'build' }, outputs: [], + parallelism: true, }, }, }; @@ -559,48 +564,56 @@ describe('calculateDependenciesFromTaskGraph', () => { overrides: {}, target: { project: 'lib1', target: 'build' }, outputs: [], + parallelism: true, }, 'lib1:build-base': { id: 'lib1:build-base', overrides: {}, target: { project: 'lib1', target: 'build-base' }, outputs: [], + parallelism: true, }, 'lib2:build': { id: 'lib2:build', overrides: {}, target: { project: 'lib2', target: 'build' }, outputs: [], + parallelism: true, }, 'lib2:build-base': { id: 'lib2:build-base', overrides: {}, target: { project: 'lib2', target: 'build-base' }, outputs: [], + parallelism: true, }, 'lib3:build': { id: 'lib3:build', overrides: {}, target: { project: 'lib3', target: 'build' }, outputs: [], + parallelism: true, }, 'lib3:build-base': { id: 'lib3:build-base', overrides: {}, target: { project: 'lib3', target: 'build-base' }, outputs: [], + parallelism: true, }, 'lib4:build': { id: 'lib4:build', overrides: {}, target: { project: 'lib4', target: 'build' }, outputs: [], + parallelism: true, }, 'lib4:build-base': { id: 'lib4:build-base', overrides: {}, target: { project: 'lib4', target: 'build-base' }, outputs: [], + parallelism: true, }, }, }; diff --git a/packages/nx/schemas/project-schema.json b/packages/nx/schemas/project-schema.json index 969ddbf126dd2..cb8fa2b220b5f 100644 --- a/packages/nx/schemas/project-schema.json +++ b/packages/nx/schemas/project-schema.json @@ -4,6 +4,27 @@ "title": "JSON schema for Nx projects", "type": "object", "properties": { + "name": { + "type": "string", + "description": "Project's name. Optional if specified in workspace.json" + }, + "root": { + "type": "string", + "description": "Project's location relative to the root of the workspace" + }, + "sourceRoot": { + "type": "string", + "description": "The location of project's sources relative to the root of the workspace" + }, + "projectType": { + "type": "string", + "description": "Type of project supported", + "enum": ["library", "application"] + }, + "generators": { + "type": "object", + "description": "List of default values used by generators" + }, "namedInputs": { "type": "object", "description": "Named inputs used by inputs defined in targets", @@ -112,6 +133,11 @@ "cache": { "type": "boolean", "description": "Specifies if the given target should be cacheable" + }, + "parallelism": { + "type": "boolean", + "default": true, + "description": "Whether this target can be run in parallel with other tasks" } } } diff --git a/packages/nx/src/config/task-graph.ts b/packages/nx/src/config/task-graph.ts index 573f691ccc220..9829af409e10f 100644 --- a/packages/nx/src/config/task-graph.ts +++ b/packages/nx/src/config/task-graph.ts @@ -76,6 +76,11 @@ export interface Task { * Determines if a given task should be cacheable. */ cache?: boolean; + + /** + * Determines if a given task should be parallelizable. + */ + parallelism: boolean; } /** diff --git a/packages/nx/src/config/to-project-name.spec.ts b/packages/nx/src/config/to-project-name.spec.ts index 208997128a118..0815e9237b3d4 100644 --- a/packages/nx/src/config/to-project-name.spec.ts +++ b/packages/nx/src/config/to-project-name.spec.ts @@ -73,6 +73,7 @@ describe('Workspaces', () => { ], "executor": "@nx/js:release-publish", "options": {}, + "parallelism": true, }, }, } diff --git a/packages/nx/src/config/workspace-json-project-json.ts b/packages/nx/src/config/workspace-json-project-json.ts index 4d7b1e4ef4fd0..eff58d64c4870 100644 --- a/packages/nx/src/config/workspace-json-project-json.ts +++ b/packages/nx/src/config/workspace-json-project-json.ts @@ -230,4 +230,10 @@ export interface TargetConfiguration { * Metadata about the target */ metadata?: TargetMetadata; + + /** + * Whether this target can be run in parallel with other tasks + * Default is true + */ + parallelism?: boolean; } diff --git a/packages/nx/src/hasher/task-hasher.spec.ts b/packages/nx/src/hasher/task-hasher.spec.ts index 29fa359bb6bd1..19b7e5080524b 100644 --- a/packages/nx/src/hasher/task-hasher.spec.ts +++ b/packages/nx/src/hasher/task-hasher.spec.ts @@ -188,6 +188,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['child-build'], @@ -197,12 +198,14 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, 'child-build': { id: 'child-build', target: { project: 'child', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: { @@ -275,6 +278,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['child-build'], @@ -284,12 +288,14 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, 'child-build': { id: 'child-build', target: { project: 'child', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: { @@ -354,6 +360,7 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'test' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -365,6 +372,7 @@ describe('TaskHasher', () => { id: 'parent-test', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, taskGraph, {} @@ -378,6 +386,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, taskGraph, {} @@ -459,12 +468,14 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'test' }, overrides: {}, outputs: [], + parallelism: true, }, 'child-test': { id: 'child-test', target: { project: 'child', target: 'test' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: { @@ -478,6 +489,7 @@ describe('TaskHasher', () => { id: 'parent-test', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, taskGraph, { MY_TEST_HASH_ENV: 'MY_TEST_HASH_ENV_VALUE' } @@ -491,6 +503,7 @@ describe('TaskHasher', () => { id: 'child-test', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, taskGraph, { MY_TEST_HASH_ENV: 'MY_TEST_HASH_ENV_VALUE' } @@ -559,6 +572,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['child-build'], @@ -568,12 +582,14 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, 'child-build': { id: 'child-build', target: { project: 'child', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: { @@ -622,6 +638,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['parent:build'], @@ -631,6 +648,7 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -687,12 +705,14 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, 'child-build': { id: 'child-build', target: { project: 'child', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: { @@ -706,6 +726,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, taskGraph, {} @@ -719,6 +740,7 @@ describe('TaskHasher', () => { id: 'child-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, taskGraph, {} @@ -763,6 +785,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: {}, outputs: [], + parallelism: true, }, { roots: ['parent:build'], @@ -772,6 +795,7 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -834,6 +858,7 @@ describe('TaskHasher', () => { id: 'app-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['app-build'], @@ -843,6 +868,7 @@ describe('TaskHasher', () => { target: { project: 'app', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -902,6 +928,7 @@ describe('TaskHasher', () => { id: 'app-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['app-build'], @@ -911,6 +938,7 @@ describe('TaskHasher', () => { target: { project: 'app', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -961,6 +989,7 @@ describe('TaskHasher', () => { id: 'app-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['app-build'], @@ -970,6 +999,7 @@ describe('TaskHasher', () => { target: { project: 'app', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -1051,12 +1081,14 @@ describe('TaskHasher', () => { target: { project: 'a', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, 'b-build': { id: 'b-build', target: { project: 'b', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -1071,6 +1103,7 @@ describe('TaskHasher', () => { target: { project: 'a', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, taskGraph, {} @@ -1081,6 +1114,7 @@ describe('TaskHasher', () => { target: { project: 'b', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, taskGraph, {} @@ -1092,6 +1126,7 @@ describe('TaskHasher', () => { target: { project: 'b', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, taskGraph, {} @@ -1102,6 +1137,7 @@ describe('TaskHasher', () => { target: { project: 'a', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, taskGraph, {} @@ -1201,6 +1237,7 @@ describe('TaskHasher', () => { id: 'app-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['app-build'], @@ -1210,6 +1247,7 @@ describe('TaskHasher', () => { target: { project: 'app', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -1418,6 +1456,7 @@ describe('TaskHasher', () => { id: 'app-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['app-build'], @@ -1427,6 +1466,7 @@ describe('TaskHasher', () => { target: { project: 'app', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -1502,6 +1542,7 @@ describe('TaskHasher', () => { id: 'app-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['app-build'], @@ -1511,6 +1552,7 @@ describe('TaskHasher', () => { target: { project: 'app', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -1584,6 +1626,7 @@ describe('TaskHasher', () => { id: 'app-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['app-build'], @@ -1593,6 +1636,7 @@ describe('TaskHasher', () => { target: { project: 'app', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: {}, @@ -1710,6 +1754,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['grandchild-build'], @@ -1719,18 +1764,21 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: ['dist/libs/libs/parent'], + parallelism: true, }, 'child-build': { id: 'child-build', target: { project: 'child', target: 'build' }, overrides: {}, outputs: ['dist/libs/libs/child'], + parallelism: true, }, 'grandchild-build': { id: 'grandchild-build', target: { project: 'grandchild', target: 'build' }, overrides: {}, outputs: ['dist/libs/libs/grandchild'], + parallelism: true, }, }, dependencies: { @@ -1850,6 +1898,7 @@ describe('TaskHasher', () => { id: 'parent-build', overrides: { prop: 'prop-value' }, outputs: [], + parallelism: true, }, { roots: ['grandchild-build'], @@ -1859,18 +1908,21 @@ describe('TaskHasher', () => { target: { project: 'parent', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, 'child-build': { id: 'child-build', target: { project: 'child', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, 'grandchild-build': { id: 'grandchild-build', target: { project: 'grandchild', target: 'build' }, overrides: {}, outputs: [], + parallelism: true, }, }, dependencies: { diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts index c24db50ef4189..a722abf6fc521 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts @@ -1502,6 +1502,7 @@ describe('project-configuration-utils', () => { "options": { "command": "echo libs/project", }, + "parallelism": true, } `); }); @@ -1658,6 +1659,7 @@ describe('project-configuration-utils', () => { "options": { "command": "echo a @ libs/a", }, + "parallelism": true, } `); }); diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.ts index e1a38c2c12eaf..9eeae9a695f35 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -1104,5 +1104,7 @@ export function normalizeTarget( ); } + target.parallelism ??= true; + return target; } diff --git a/packages/nx/src/tasks-runner/create-task-graph.ts b/packages/nx/src/tasks-runner/create-task-graph.ts index 9035be7940b7f..b2dc8878b301a 100644 --- a/packages/nx/src/tasks-runner/create-task-graph.ts +++ b/packages/nx/src/tasks-runner/create-task-graph.ts @@ -26,7 +26,7 @@ export class ProcessTasks { configuration: string, overrides: Object, excludeTaskDependencies: boolean - ) { + ): string[] { for (const projectName of projectNames) { for (const target of targets) { const project = this.projectGraph.nodes[projectName]; @@ -351,6 +351,7 @@ export class ProcessTasks { interpolatedOverrides ), cache: project.data.targets[target].cache, + parallelism: project.data.targets[target].options?.parallelism, }; } diff --git a/packages/nx/src/tasks-runner/tasks-schedule.spec.ts b/packages/nx/src/tasks-runner/tasks-schedule.spec.ts index d849fccdd21e8..528d412564672 100644 --- a/packages/nx/src/tasks-runner/tasks-schedule.spec.ts +++ b/packages/nx/src/tasks-runner/tasks-schedule.spec.ts @@ -5,7 +5,7 @@ import { DependencyType, ProjectGraph } from '../config/project-graph'; import * as nxJsonUtils from '../config/nx-json'; import * as executorUtils from '../command-line/run/executor-utils'; -function createMockTask(id: string): Task { +function createMockTask(id: string, parallelism: boolean = true): Task { const [project, target] = id.split(':'); return { id, @@ -15,6 +15,7 @@ function createMockTask(id: string): Task { }, outputs: [], overrides: {}, + parallelism, }; } @@ -411,4 +412,405 @@ describe('TasksSchedule', () => { }); }); }); + + describe('tasks with parallelism false', () => { + describe('dependent tasks', () => { + let taskSchedule: TasksSchedule; + let taskGraph: TaskGraph; + let app1Build: Task; + let app2Build: Task; + let lib1Build: Task; + let lifeCycle: any; + beforeEach(() => { + // app1 depends on lib1 + // app2 does not depend on anything + // lib1 does not depend on anything + // all tasks have parallelism set to false + app1Build = createMockTask('app1:build', false); + app2Build = createMockTask('app2:build', false); + lib1Build = createMockTask('lib1:build', false); + + taskGraph = { + tasks: { + 'app1:build': app1Build, + 'app2:build': app2Build, + 'lib1:build': lib1Build, + }, + dependencies: { + 'app1:build': ['lib1:build'], + 'app2:build': [], + 'lib1:build': [], + }, + roots: ['lib1:build', 'app2:build'], + }; + jest.spyOn(nxJsonUtils, 'readNxJson').mockReturnValue({}); + jest.spyOn(executorUtils, 'getExecutorInformation').mockReturnValue({ + schema: { + version: 2, + properties: {}, + }, + implementationFactory: jest.fn(), + batchImplementationFactory: jest.fn(), + isNgCompat: true, + isNxExecutor: true, + }); + + const projectGraph: ProjectGraph = { + nodes: { + app1: { + data: { + root: 'app1', + targets: { + build: { + executor: 'awesome-executors:build', + }, + }, + }, + name: 'app1', + type: 'app', + }, + app2: { + name: 'app2', + type: 'app', + data: { + root: 'app2', + targets: { + build: { + executor: 'awesome-executors:app2-build', + }, + }, + }, + }, + lib1: { + name: 'lib1', + type: 'lib', + data: { + root: 'lib1', + targets: { + build: { + executor: 'awesome-executors:build', + }, + }, + }, + }, + } as any, + dependencies: { + app1: [ + { + source: 'app1', + target: 'lib1', + type: DependencyType.static, + }, + ], + app2: [ + { + source: 'app2', + target: 'lib1', + type: DependencyType.static, + }, + ], + }, + externalNodes: {}, + version: '5', + }; + + lifeCycle = { + startTask: jest.fn(), + endTask: jest.fn(), + scheduleTask: jest.fn(), + }; + taskSchedule = new TasksSchedule(projectGraph, taskGraph, { + lifeCycle, + }); + }); + + describe('Without Batch Mode', () => { + let original; + beforeEach(() => { + original = process.env['NX_BATCH_MODE']; + process.env['NX_BATCH_MODE'] = 'false'; + }); + + afterEach(() => { + process.env['NX_BATCH_MODE'] = original; + }); + + it('should begin with no scheduled tasks', () => { + expect(taskSchedule.nextBatch()).toBeNull(); + expect(taskSchedule.nextTask()).toBeNull(); + }); + + it('should schedule root tasks first', async () => { + await taskSchedule.scheduleNextTasks(); + expect(taskSchedule.nextTask()).toEqual(lib1Build); + expect(taskSchedule.nextTask()).toEqual(app2Build); + }); + + it('should not schedule any tasks that still have uncompleted dependencies', async () => { + await taskSchedule.scheduleNextTasks(); + taskSchedule.nextTask(); + taskSchedule.nextTask(); + expect(taskSchedule.nextTask()).toBeNull(); + + taskSchedule.complete([app2Build.id]); + + expect(taskSchedule.nextTask()).toBeNull(); + }); + + it('should continue to schedule tasks that have completed dependencies', async () => { + await taskSchedule.scheduleNextTasks(); + taskSchedule.nextTask(); + taskSchedule.nextTask(); + taskSchedule.complete([lib1Build.id]); + + await taskSchedule.scheduleNextTasks(); + expect(taskSchedule.nextTask()).toEqual(app1Build); + }); + + it('should run out of tasks when they are all complete', async () => { + await taskSchedule.scheduleNextTasks(); + taskSchedule.nextTask(); + taskSchedule.nextTask(); + taskSchedule.complete([lib1Build.id, app1Build.id, app2Build.id]); + + expect(taskSchedule.hasTasks()).toEqual(false); + }); + + it('should not schedule batches', async () => { + await taskSchedule.scheduleNextTasks(); + + expect(taskSchedule.nextTask()).not.toBeNull(); + + expect(taskSchedule.nextBatch()).toBeNull(); + }); + }); + + describe('With Batch Mode', () => { + let original; + beforeEach(() => { + original = process.env['NX_BATCH_MODE']; + process.env['NX_BATCH_MODE'] = 'true'; + }); + + afterEach(() => { + process.env['NX_BATCH_MODE'] = original; + }); + + it('should schedule batches of tasks by different executors', async () => { + await taskSchedule.scheduleNextTasks(); + + expect(taskSchedule.nextTask()).toBeNull(); + + expect(taskSchedule.nextBatch()).toEqual({ + executorName: 'awesome-executors:build', + taskGraph: removeTasksFromTaskGraph(taskGraph, ['app2:build']), + }); + expect(taskSchedule.nextBatch()).toEqual({ + executorName: 'awesome-executors:app2-build', + taskGraph: removeTasksFromTaskGraph(taskGraph, [ + 'app1:build', + 'lib1:build', + ]), + }); + }); + + it('should run out of tasks when all batches are done', async () => { + await taskSchedule.scheduleNextTasks(); + taskSchedule.nextBatch(); + taskSchedule.nextBatch(); + taskSchedule.complete(['app1:build', 'lib1:build', 'app2:build']); + expect(taskSchedule.hasTasks()).toEqual(false); + }); + }); + }); + + describe('non-dependent tasks', () => { + let taskSchedule: TasksSchedule; + let taskGraph: TaskGraph; + let app1Test: Task; + let app2Test: Task; + let lib1Test: Task; + let lifeCycle: any; + beforeEach(() => { + // app1, app2, and lib1 do not depend on each other + // lib1 has parallelism set to false + app1Test = createMockTask('app1:test', true); + app2Test = createMockTask('app2:test', true); + lib1Test = createMockTask('lib1:test', false); + + taskGraph = { + tasks: { + 'app1:test': app1Test, + 'app2:test': app2Test, + 'lib1:test': lib1Test, + }, + dependencies: { + 'app1:test': [], + 'app2:test': [], + 'lib1:test': [], + }, + roots: ['app1:test', 'app2:test', 'lib1:test'], + }; + jest.spyOn(nxJsonUtils, 'readNxJson').mockReturnValue({}); + jest.spyOn(executorUtils, 'getExecutorInformation').mockReturnValue({ + schema: { + version: 2, + properties: {}, + }, + implementationFactory: jest.fn(), + batchImplementationFactory: jest.fn(), + isNgCompat: true, + isNxExecutor: true, + }); + + const projectGraph: ProjectGraph = { + nodes: { + app1: { + data: { + root: 'app1', + targets: { + test: { + executor: 'awesome-executors:test', + }, + }, + }, + name: 'app1', + type: 'app', + }, + app2: { + name: 'app2', + type: 'app', + data: { + root: 'app2', + targets: { + test: { + executor: 'awesome-executors:app2-test', + }, + }, + }, + }, + lib1: { + name: 'lib1', + type: 'lib', + data: { + root: 'lib1', + targets: { + test: { + executor: 'awesome-executors:test', + }, + }, + }, + }, + } as any, + dependencies: { + app1: [ + { + source: 'app1', + target: 'lib1', + type: DependencyType.static, + }, + ], + app2: [ + { + source: 'app2', + target: 'lib1', + type: DependencyType.static, + }, + ], + }, + externalNodes: {}, + version: '5', + }; + + lifeCycle = { + startTask: jest.fn(), + endTask: jest.fn(), + scheduleTask: jest.fn(), + }; + taskSchedule = new TasksSchedule(projectGraph, taskGraph, { + lifeCycle, + }); + }); + + describe('Without Batch Mode', () => { + let original; + beforeEach(() => { + original = process.env['NX_BATCH_MODE']; + process.env['NX_BATCH_MODE'] = 'false'; + }); + + afterEach(() => { + process.env['NX_BATCH_MODE'] = original; + }); + + it('should begin with no scheduled tasks', () => { + expect(taskSchedule.nextBatch()).toBeNull(); + expect(taskSchedule.nextTask()).toBeNull(); + }); + + it('should schedule root tasks in topological order', async () => { + await taskSchedule.scheduleNextTasks(); + expect(taskSchedule.nextTask()).toEqual(lib1Test); + expect(taskSchedule.nextTask()).toEqual(app1Test); + expect(taskSchedule.nextTask()).toEqual(app2Test); + }); + + it('should run out of tasks when they are all complete', async () => { + await taskSchedule.scheduleNextTasks(); + taskSchedule.nextTask(); + taskSchedule.nextTask(); + taskSchedule.nextTask(); + taskSchedule.complete([lib1Test.id, app1Test.id, app2Test.id]); + + expect(taskSchedule.hasTasks()).toEqual(false); + }); + + it('should not schedule batches', async () => { + await taskSchedule.scheduleNextTasks(); + + expect(taskSchedule.nextTask()).not.toBeNull(); + + expect(taskSchedule.nextBatch()).toBeNull(); + }); + }); + + describe('With Batch Mode', () => { + let original; + beforeEach(() => { + original = process.env['NX_BATCH_MODE']; + process.env['NX_BATCH_MODE'] = 'true'; + }); + + afterEach(() => { + process.env['NX_BATCH_MODE'] = original; + }); + + it('should schedule batches of tasks by different executors', async () => { + await taskSchedule.scheduleNextTasks(); + + expect(taskSchedule.nextTask()).toBeNull(); + + expect(taskSchedule.nextBatch()).toEqual({ + executorName: 'awesome-executors:test', + taskGraph: removeTasksFromTaskGraph(taskGraph, ['app2:test']), + }); + expect(taskSchedule.nextBatch()).toEqual({ + executorName: 'awesome-executors:app2-test', + taskGraph: removeTasksFromTaskGraph(taskGraph, [ + 'app1:test', + 'lib1:test', + ]), + }); + }); + + it('should run out of tasks when all batches are done', async () => { + await taskSchedule.scheduleNextTasks(); + taskSchedule.nextBatch(); + taskSchedule.nextBatch(); + taskSchedule.complete(['app1:test', 'lib1:test', 'app2:test']); + expect(taskSchedule.hasTasks()).toEqual(false); + }); + }); + }); + }); }); diff --git a/packages/nx/src/tasks-runner/tasks-schedule.ts b/packages/nx/src/tasks-runner/tasks-schedule.ts index 5d63f3a317302..5b665d3fa5b85 100644 --- a/packages/nx/src/tasks-runner/tasks-schedule.ts +++ b/packages/nx/src/tasks-runner/tasks-schedule.ts @@ -200,9 +200,29 @@ export class TasksSchedule { ); } - private canBeScheduled(taskId: string) { - return this.taskGraph.dependencies[taskId].every((id) => - this.completedTasks.has(id) + private canBeScheduled(taskId: string): boolean { + // if not scheduled tasks, can schedule anything that has no running dependencies + const hasDependenciesCompleted = this.taskGraph.dependencies[taskId].every( + (id) => this.completedTasks.has(id) ); + if (this.scheduleTasks.length === 0) { + return hasDependenciesCompleted; + } + + const allScheduledTasksSupportParallelsim = this.scheduledTasks.every( + (id) => { + return this.taskGraph.tasks[id].parallelism === true; + } + ); + if (allScheduledTasksSupportParallelsim) { + // if all scheduled tasks support parallelism, tasks that have no dependencies and parallelism true can be scheduled + return ( + this.taskGraph.tasks[taskId].parallelism === true && + hasDependenciesCompleted + ); + } else { + // if any scheduled tasks do not support parallelism, no other tasks can be scheduled + return false; + } } } diff --git a/packages/playwright/src/plugins/plugin.spec.ts b/packages/playwright/src/plugins/plugin.spec.ts index f6740f0a17297..60c36e4f2c4a0 100644 --- a/packages/playwright/src/plugins/plugin.spec.ts +++ b/packages/playwright/src/plugins/plugin.spec.ts @@ -91,6 +91,7 @@ describe('@nx/playwright/plugin', () => { "outputs": [ "{projectRoot}/test-results", ], + "parallelism": false, }, "e2e-ci": { "cache": true, @@ -123,6 +124,7 @@ describe('@nx/playwright/plugin', () => { "outputs": [ "{projectRoot}/test-results", ], + "parallelism": false, }, }, }, @@ -200,6 +202,7 @@ describe('@nx/playwright/plugin', () => { "{projectRoot}/test-results/html", "{projectRoot}/test-results", ], + "parallelism": false, }, "e2e-ci": { "cache": true, @@ -235,6 +238,7 @@ describe('@nx/playwright/plugin', () => { "{projectRoot}/test-results/html", "{projectRoot}/test-results", ], + "parallelism": false, }, }, }, @@ -323,6 +327,7 @@ describe('@nx/playwright/plugin', () => { "outputs": [ "{projectRoot}/test-results", ], + "parallelism": false, } `); expect(targets['e2e-ci--tests/run-me.spec.ts']).toMatchInlineSnapshot(` @@ -358,6 +363,7 @@ describe('@nx/playwright/plugin', () => { "outputs": [ "{projectRoot}/test-results", ], + "parallelism": false, } `); expect(targets['e2e-ci--tests/run-me-2.spec.ts']).toMatchInlineSnapshot(` @@ -393,6 +399,7 @@ describe('@nx/playwright/plugin', () => { "outputs": [ "{projectRoot}/test-results", ], + "parallelism": false, } `); expect(targets['e2e-ci--tests/skip-me.spec.ts']).not.toBeDefined(); diff --git a/packages/playwright/src/plugins/plugin.ts b/packages/playwright/src/plugins/plugin.ts index 9f64ffc8a6513..dc45d1ccdf2a2 100644 --- a/packages/playwright/src/plugins/plugin.ts +++ b/packages/playwright/src/plugins/plugin.ts @@ -162,6 +162,7 @@ async function buildPlaywrightTargets( options: { cwd: '{projectRoot}', }, + parallelism: false, metadata: { technologies: ['playwright'], description: 'Runs Playwright Tests', @@ -257,6 +258,7 @@ async function buildPlaywrightTargets( inputs: ciBaseTargetConfig.inputs, outputs: ciBaseTargetConfig.outputs, dependsOn, + parallelism: false, metadata: { technologies: ['playwright'], description: 'Runs Playwright Tests in CI',