diff --git a/graph/ui-project-details/jest.config.ts b/graph/ui-project-details/jest.config.ts new file mode 100644 index 00000000000000..1781dc7d0a237e --- /dev/null +++ b/graph/ui-project-details/jest.config.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +export default { + displayName: 'graph-ui-project-details', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/graph/graph-ui-project-details', +}; diff --git a/graph/ui-project-details/tsconfig.json b/graph/ui-project-details/tsconfig.json index 6b4f8f64d222d6..e9994c5f10865f 100644 --- a/graph/ui-project-details/tsconfig.json +++ b/graph/ui-project-details/tsconfig.json @@ -13,6 +13,9 @@ { "path": "./tsconfig.lib.json" }, + { + "path": "./tsconfig.spec.json" + }, { "path": "./tsconfig.storybook.json" } diff --git a/graph/ui-project-details/tsconfig.spec.json b/graph/ui-project-details/tsconfig.spec.json new file mode 100644 index 00000000000000..26ef046ac5e544 --- /dev/null +++ b/graph/ui-project-details/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/graph/ui-tooltips/jest.config.ts b/graph/ui-tooltips/jest.config.ts index 06a3b2b2812722..cf6c1056e6e542 100644 --- a/graph/ui-tooltips/jest.config.ts +++ b/graph/ui-tooltips/jest.config.ts @@ -6,5 +6,5 @@ export default { '^.+\\.[tj]sx?$': 'babel-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - coverageDirectory: '../../coverage/graph/ui-graph', + coverageDirectory: '../../coverage/graph/graph-ui-tooltips', }; diff --git a/packages/gradle/src/generators/init/init.spec.ts b/packages/gradle/src/generators/init/init.spec.ts index c1107825f4d512..ff09c2144d4630 100644 --- a/packages/gradle/src/generators/init/init.spec.ts +++ b/packages/gradle/src/generators/init/init.spec.ts @@ -19,17 +19,18 @@ describe('@nx/gradle:init', () => { }); const nxJson = readNxJson(tree); expect(nxJson.plugins).toMatchInlineSnapshot(` - [ - { - "options": { - "buildTargetName": "build", - "classesTargetName": "classes", - "testTargetName": "test", - }, - "plugin": "@nx/gradle", - }, - ] - `); + [ + { + "options": { + "buildTargetName": "build", + "ciTargetName": "test-ci", + "classesTargetName": "classes", + "testTargetName": "test", + }, + "plugin": "@nx/gradle", + }, + ] + `); }); it('should not overwrite existing plugins', async () => { @@ -42,18 +43,19 @@ describe('@nx/gradle:init', () => { }); const nxJson = readNxJson(tree); expect(nxJson.plugins).toMatchInlineSnapshot(` - [ - "foo", - { - "options": { - "buildTargetName": "build", - "classesTargetName": "classes", - "testTargetName": "test", - }, - "plugin": "@nx/gradle", - }, - ] - `); + [ + "foo", + { + "options": { + "buildTargetName": "build", + "ciTargetName": "test-ci", + "classesTargetName": "classes", + "testTargetName": "test", + }, + "plugin": "@nx/gradle", + }, + ] + `); }); it('should not add plugin if already in array', async () => { diff --git a/packages/gradle/src/generators/init/init.ts b/packages/gradle/src/generators/init/init.ts index ae24c6831cfc1d..0418c2a2f3ac6c 100644 --- a/packages/gradle/src/generators/init/init.ts +++ b/packages/gradle/src/generators/init/init.ts @@ -58,6 +58,7 @@ function addPlugin(tree: Tree) { plugin: '@nx/gradle', options: { testTargetName: 'test', + ciTargetName: 'test-ci', classesTargetName: 'classes', buildTargetName: 'build', }, diff --git a/packages/gradle/src/plugin/dependencies.ts b/packages/gradle/src/plugin/dependencies.ts index b07163165eb5ba..820f7fed88c610 100644 --- a/packages/gradle/src/plugin/dependencies.ts +++ b/packages/gradle/src/plugin/dependencies.ts @@ -10,6 +10,7 @@ import { readFileSync } from 'node:fs'; import { basename } from 'node:path'; import { + GRADLE_BUILD_FILES, getCurrentGradleReport, newLineSeparator, } from '../utils/get-gradle-report'; @@ -58,14 +59,12 @@ export const createDependencies: CreateDependencies = async ( return Array.from(dependencies); }; -const gradleConfigFileNames = new Set(['build.gradle', 'build.gradle.kts']); - function findGradleFiles(fileMap: FileMap): string[] { const gradleFiles: string[] = []; for (const [_, files] of Object.entries(fileMap.projectFileMap)) { for (const file of files) { - if (gradleConfigFileNames.has(basename(file.file))) { + if (GRADLE_BUILD_FILES.includes(basename(file.file))) { gradleFiles.push(file.file); } } diff --git a/packages/gradle/src/plugin/nodes.spec.ts b/packages/gradle/src/plugin/nodes.spec.ts index 11912c7819373d..a3e66a37219ae4 100644 --- a/packages/gradle/src/plugin/nodes.spec.ts +++ b/packages/gradle/src/plugin/nodes.spec.ts @@ -4,8 +4,13 @@ import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { type GradleReport } from '../utils/get-gradle-report'; let gradleReport: GradleReport; -jest.mock('../utils/get-gradle-report.ts', () => { +jest.mock('../utils/get-gradle-report', () => { return { + GRADLE_BUILD_FILES: ['build.gradle', 'build.gradle.kts'], + GRADLE_TEST_FILES: [ + '**/src/test/java/**/*.java', + '**/src/test/kotlin/**/*.kt', + ], populateGradleReport: jest.fn().mockImplementation(() => void 0), getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), }; @@ -23,11 +28,11 @@ describe('@nx/gradle/plugin', () => { tempFs = new TempFs('test'); gradleReport = { gradleFileToGradleProjectMap: new Map([ - ['proj/gradle.build', 'proj'], + ['proj/build.gradle', 'proj'], ]), buildFileToDepsMap: new Map(), gradleFileToOutputDirsMap: new Map>([ - ['proj/gradle.build', new Map([['build', 'build']])], + ['proj/build.gradle', new Map([['build', 'build']])], ]), gradleProjectToTasksTypeMap: new Map>([ ['proj', new Map([['test', 'Test']])], @@ -48,7 +53,7 @@ describe('@nx/gradle/plugin', () => { }; await tempFs.createFiles({ - 'proj/gradle.build': ``, + 'proj/build.gradle': ``, gradlew: '', }); }); @@ -60,7 +65,7 @@ describe('@nx/gradle/plugin', () => { it('should create nodes based on gradle', async () => { const results = await createNodesFunction( - ['proj/gradle.build'], + ['proj/build.gradle'], { buildTargetName: 'build', }, @@ -70,7 +75,7 @@ describe('@nx/gradle/plugin', () => { expect(results).toMatchInlineSnapshot(` [ [ - "proj/gradle.build", + "proj/build.gradle", { "projects": { "proj": { @@ -87,7 +92,7 @@ describe('@nx/gradle/plugin', () => { "name": "proj", "targets": { "test": { - "cache": false, + "cache": true, "command": "./gradlew proj:test", "dependsOn": [ "classes", @@ -97,6 +102,15 @@ describe('@nx/gradle/plugin', () => { "^production", ], "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "args": [ + "--test-dry-run", + "--rerun", + ], + }, + }, "technologies": [ "gradle", ], @@ -114,11 +128,11 @@ describe('@nx/gradle/plugin', () => { it('should create nodes based on gradle for nested project root', async () => { gradleReport = { gradleFileToGradleProjectMap: new Map([ - ['nested/nested/proj/gradle.build', 'proj'], + ['nested/nested/proj/build.gradle', 'proj'], ]), buildFileToDepsMap: new Map(), gradleFileToOutputDirsMap: new Map>([ - ['nested/nested/proj/gradle.build', new Map([['build', 'build']])], + ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], ]), gradleProjectToTasksTypeMap: new Map>([ ['proj', new Map([['test', 'Test']])], @@ -126,11 +140,11 @@ describe('@nx/gradle/plugin', () => { gradleProjectToProjectName: new Map([['proj', 'proj']]), }; await tempFs.createFiles({ - 'nested/nested/proj/gradle.build': ``, + 'nested/nested/proj/build.gradle': ``, }); const results = await createNodesFunction( - ['nested/nested/proj/gradle.build'], + ['nested/nested/proj/build.gradle'], { buildTargetName: 'build', }, @@ -140,7 +154,7 @@ describe('@nx/gradle/plugin', () => { expect(results).toMatchInlineSnapshot(` [ [ - "nested/nested/proj/gradle.build", + "nested/nested/proj/build.gradle", { "projects": { "nested/nested/proj": { @@ -157,7 +171,7 @@ describe('@nx/gradle/plugin', () => { "name": "proj", "targets": { "test": { - "cache": false, + "cache": true, "command": "./gradlew proj:test", "dependsOn": [ "classes", @@ -167,6 +181,194 @@ describe('@nx/gradle/plugin', () => { "^production", ], "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "args": [ + "--test-dry-run", + "--rerun", + ], + }, + }, + "technologies": [ + "gradle", + ], + }, + }, + }, + }, + }, + }, + ], + ] + `); + }); + + it('should create nodes with atomized tests targets based on gradle for nested project root', async () => { + gradleReport = { + gradleFileToGradleProjectMap: new Map([ + ['nested/nested/proj/build.gradle', 'proj'], + ]), + buildFileToDepsMap: new Map(), + gradleFileToOutputDirsMap: new Map>([ + ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], + ]), + gradleProjectToTasksTypeMap: new Map>([ + ['proj', new Map([['test', 'Test']])], + ]), + gradleProjectToProjectName: new Map([['proj', 'proj']]), + }; + await tempFs.createFiles({ + 'nested/nested/proj/build.gradle': ``, + }); + await tempFs.createFiles({ + 'proj/src/test/java/test/rootTest.java': ``, + }); + await tempFs.createFiles({ + 'nested/nested/proj/src/test/java/test/test.java': ``, + }); + await tempFs.createFiles({ + 'nested/nested/proj/src/test/java/test/test1.java': ``, + }); + + const results = await createNodesFunction( + ['nested/nested/proj/build.gradle'], + { + buildTargetName: 'build', + }, + context + ); + + expect(results).toMatchInlineSnapshot(` + [ + [ + "nested/nested/proj/build.gradle", + { + "projects": { + "nested/nested/proj": { + "metadata": { + "targetGroups": { + "Test": [ + "test", + "test-ci--test", + "test-ci--test1", + "test-ci", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "targets": { + "test": { + "cache": true, + "command": "./gradlew proj:test", + "dependsOn": [ + "classes", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "args": [ + "--test-dry-run", + "--rerun", + ], + }, + }, + "technologies": [ + "gradle", + ], + }, + }, + "test-ci": { + "cache": true, + "dependsOn": [ + { + "params": "forward", + "projects": "self", + "target": "test-ci--test", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--test1", + }, + ], + "executor": "nx:noop", + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle Tests in CI", + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "args": [ + "--test-dry-run", + "--rerun", + ], + }, + }, + "nonAtomizedTarget": "test", + "technologies": [ + "gradle", + ], + }, + }, + "test-ci--test": { + "cache": true, + "command": "./gradlew proj:test --tests test", + "dependsOn": [ + "classes", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle Tests test in CI", + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "args": [ + "--test-dry-run", + "--rerun", + ], + }, + }, + "technologies": [ + "gradle", + ], + }, + }, + "test-ci--test1": { + "cache": true, + "command": "./gradlew proj:test --tests test1", + "dependsOn": [ + "classes", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle Tests test1 in CI", + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "args": [ + "--test-dry-run", + "--rerun", + ], + }, + }, "technologies": [ "gradle", ], diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts index 222d9f5ed8ed50..340dfe49b25c5c 100644 --- a/packages/gradle/src/plugin/nodes.ts +++ b/packages/gradle/src/plugin/nodes.ts @@ -2,7 +2,6 @@ import { CreateNodes, CreateNodesV2, CreateNodesContext, - CreateNodesContextV2, ProjectConfiguration, TargetConfiguration, createNodesFromFiles, @@ -13,7 +12,7 @@ import { } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { existsSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { basename, dirname, join } from 'node:path'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { getGradleExecFile } from '../utils/exec-gradle'; @@ -22,8 +21,10 @@ import { getCurrentGradleReport, GradleReport, gradleConfigGlob, + GRADLE_TEST_FILES, } from '../utils/get-gradle-report'; import { hashObject } from 'nx/src/hasher/file-hasher'; +import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context'; const cacheableTaskType = new Set(['Build', 'Verification']); const dependsOnMap = { @@ -38,12 +39,22 @@ interface GradleTask { } export interface GradlePluginOptions { + ciTargetName?: string; testTargetName?: string; classesTargetName?: string; buildTargetName?: string; [taskTargetName: string]: string | undefined; } +function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions { + options ??= {}; + options.ciTargetName ??= 'test-ci'; + options.testTargetName ??= 'test'; + options.classesTargetName ??= 'classes'; + options.buildTargetName ??= 'build'; + return options; +} + type GradleTargets = Record< string, { @@ -98,13 +109,14 @@ export const makeCreateNodes = context: CreateNodesContext ) => { const projectRoot = dirname(gradleFilePath); + options = normalizeOptions(options); const hash = await calculateHashForCreateNodes( projectRoot, options ?? {}, context ); - targetsCache[hash] ??= createGradleProject( + targetsCache[hash] ??= await createGradleProject( gradleReport, gradleFilePath, options, @@ -138,7 +150,7 @@ export const createNodes: CreateNodes = [ }, ]; -function createGradleProject( +async function createGradleProject( gradleReport: GradleReport, gradleFilePath: string, options: GradlePluginOptions | undefined, @@ -177,12 +189,13 @@ function createGradleProject( string >; - const { targets, targetGroups } = createGradleTargets( + const { targets, targetGroups } = await createGradleTargets( tasks, options, context, outputDirs, - gradleProject + gradleProject, + gradleFilePath ); const project = { name: projectName, @@ -200,16 +213,17 @@ function createGradleProject( } } -function createGradleTargets( +async function createGradleTargets( tasks: GradleTask[], options: GradlePluginOptions | undefined, context: CreateNodesContext, outputDirs: Map, - gradleProject: string -): { + gradleProject: string, + gradleFilePath: string +): Promise<{ targetGroups: Record; targets: Record; -} { +}> { const inputsMap = createInputsMap(context); const targets: Record = {}; @@ -217,28 +231,50 @@ function createGradleTargets( for (const task of tasks) { const targetName = options?.[`${task.name}TargetName`] ?? task.name; - const outputs = outputDirs.get(task.name); + if (task.name === 'test') { + const testFiles = await globWithWorkspaceContext( + dirname(gradleFilePath), + GRADLE_TEST_FILES + ); + getTestTargets( + gradleProject, + targetName, + options.ciTargetName, + targets, + targetGroups, + inputsMap, + outputDirs, + task.type, + testFiles + ); + continue; + } + const output = outputDirs.get(task.name); + const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}${ + task.name + }`; targets[targetName] = { - command: `${getGradleExecFile()} ${ - gradleProject ? gradleProject + ':' : '' - }${task.name}`, + command: `${getGradleExecFile()} ${taskCommandToRun}`, cache: cacheableTaskType.has(task.type), inputs: inputsMap[task.name], dependsOn: dependsOnMap[task.name], metadata: { technologies: ['gradle'], + help: { + command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, + example: { + args: ['--rerun'], + }, + }, }, + ...(output ? { outputs: [output] } : {}), }; - if (outputs) { - targets[targetName].outputs = [outputs]; - } - if (!targetGroups[task.type]) { targetGroups[task.type] = []; } - targetGroups[task.type].push(task.name); + targetGroups[task.type].push(targetName); } return { targetGroups, targets }; } @@ -257,3 +293,97 @@ function createInputsMap( : ['default', '^default'], }; } + +function getTestTargets( + gradleProject: string, + testTargetName: string, + ciTargetName: string, + targets: Record, + targetGroups: Record, + inputsMap: Record, + outputDirs: Map, + taskType: string, + testFiles: string[] +) { + const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}test`; + const outputs = [ + outputDirs.get('testReport'), + outputDirs.get('testResults'), + ].filter(Boolean); + + targets[testTargetName] = { + command: `${getGradleExecFile()} ${taskCommandToRun}`, + cache: true, + inputs: inputsMap['test'], + dependsOn: dependsOnMap['test'], + metadata: { + technologies: ['gradle'], + help: { + command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, + example: { + args: ['--test-dry-run', '--rerun'], + }, + }, + }, + ...(outputs.length > 0 ? { outputs } : {}), + }; + + if (!targetGroups[taskType]) { + targetGroups[taskType] = []; + } + targetGroups[taskType].push(testTargetName); + + if (!testFiles || testFiles.length === 0) { + return; + } + + const dependsOn: TargetConfiguration['dependsOn'] = []; + testFiles?.forEach((testFile) => { + const testName = basename(testFile).split('.')[0]; + const targetName = ciTargetName + '--' + testName; + + targets[targetName] = { + command: `${getGradleExecFile()} ${taskCommandToRun} --tests ${testName}`, + cache: true, + inputs: inputsMap['test'], + dependsOn: dependsOnMap['test'], + metadata: { + technologies: ['gradle'], + description: `Runs Gradle Tests ${testName} in CI`, + help: { + command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, + example: { + args: ['--test-dry-run', '--rerun'], + }, + }, + }, + ...(outputs && outputs.length > 0 ? { outputs } : {}), + }; + targetGroups[taskType].push(targetName); + dependsOn.push({ + target: targetName, + projects: 'self', + params: 'forward', + }); + }); + + targets[ciTargetName] = { + executor: 'nx:noop', + cache: true, + inputs: inputsMap['test'], + dependsOn: dependsOn, + ...(outputs && outputs.length > 0 ? { outputs } : {}), + metadata: { + technologies: ['gradle'], + description: 'Runs Gradle Tests in CI', + nonAtomizedTarget: testTargetName, + help: { + command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, + example: { + args: ['--test-dry-run', '--rerun'], + }, + }, + }, + }; + targetGroups[taskType].push(ciTargetName); +} diff --git a/packages/gradle/src/utils/get-gradle-report.ts b/packages/gradle/src/utils/get-gradle-report.ts index d3c2bb645a9ad5..0885a7f3381bb7 100644 --- a/packages/gradle/src/utils/get-gradle-report.ts +++ b/packages/gradle/src/utils/get-gradle-report.ts @@ -7,6 +7,7 @@ import { normalizePath, workspaceRoot, } from '@nx/devkit'; +import { combineGlobPatterns } from 'nx/src/utils/globs'; import { execGradleAsync } from './exec-gradle'; import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context'; @@ -30,7 +31,15 @@ export interface GradleReport { let gradleReportCache: GradleReport; let gradleCurrentConfigHash: string; -export const gradleConfigGlob = '**/build.{gradle.kts,gradle}'; +export const GRADLE_BUILD_FILES = ['build.gradle', 'build.gradle.kts']; +export const GRADLE_TEST_FILES = [ + '**/src/test/java/**/*.java', + '**/src/test/kotlin/**/*.kt', +]; + +export const gradleConfigGlob = combineGlobPatterns( + ...GRADLE_BUILD_FILES.map((file) => `**/${file}`) +); export function getCurrentGradleReport() { if (!gradleReportCache) {