From a6484f9cdcf9182bf28a631246bbe51bef931dca Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Mon, 24 Jun 2024 14:49:34 -0400 Subject: [PATCH] feat(gradle): gradle atomizer --- graph/ui-project-details/jest.config.ts | 10 + graph/ui-project-details/tsconfig.json | 3 + graph/ui-project-details/tsconfig.spec.json | 20 ++ graph/ui-tooltips/jest.config.ts | 2 +- .../gradle/src/generators/init/init.spec.ts | 46 ++-- packages/gradle/src/plugin/dependencies.ts | 5 +- packages/gradle/src/plugin/nodes.spec.ts | 178 ++++++++++++++-- packages/gradle/src/plugin/nodes.ts | 200 +++++++++++++++--- .../gradle/src/utils/get-gradle-report.ts | 16 +- 9 files changed, 410 insertions(+), 70 deletions(-) create mode 100644 graph/ui-project-details/jest.config.ts create mode 100644 graph/ui-project-details/tsconfig.spec.json diff --git a/graph/ui-project-details/jest.config.ts b/graph/ui-project-details/jest.config.ts new file mode 100644 index 0000000000000..1781dc7d0a237 --- /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 6b4f8f64d222d..e9994c5f10865 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 0000000000000..26ef046ac5e54 --- /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 06a3b2b281272..cf6c1056e6e54 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 c1107825f4d51..56a58526f5829 100644 --- a/packages/gradle/src/generators/init/init.spec.ts +++ b/packages/gradle/src/generators/init/init.spec.ts @@ -19,17 +19,17 @@ 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", + "classesTargetName": "classes", + "testTargetName": "test", + }, + "plugin": "@nx/gradle", + }, + ] + `); }); it('should not overwrite existing plugins', async () => { @@ -42,18 +42,18 @@ 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", + "classesTargetName": "classes", + "testTargetName": "test", + }, + "plugin": "@nx/gradle", + }, + ] + `); }); it('should not add plugin if already in array', async () => { diff --git a/packages/gradle/src/plugin/dependencies.ts b/packages/gradle/src/plugin/dependencies.ts index b07163165eb5b..1513c28d0e8cc 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.has(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 11912c7819373..609e4cb67b0ef 100644 --- a/packages/gradle/src/plugin/nodes.spec.ts +++ b/packages/gradle/src/plugin/nodes.spec.ts @@ -4,8 +4,9 @@ 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: new Set(['build.gradle', 'build.gradle.kts']), populateGradleReport: jest.fn().mockImplementation(() => void 0), getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), }; @@ -23,14 +24,14 @@ 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']])], + ['proj', new Map([['test', 'Verification']])], ]), gradleProjectToProjectName: new Map([['proj', 'proj']]), }; @@ -48,7 +49,7 @@ describe('@nx/gradle/plugin', () => { }; await tempFs.createFiles({ - 'proj/gradle.build': ``, + 'proj/build.gradle': ``, gradlew: '', }); }); @@ -60,7 +61,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,13 +71,13 @@ describe('@nx/gradle/plugin', () => { expect(results).toMatchInlineSnapshot(` [ [ - "proj/gradle.build", + "proj/build.gradle", { "projects": { "proj": { "metadata": { "targetGroups": { - "Test": [ + "Verification": [ "test", ], }, @@ -87,7 +88,7 @@ describe('@nx/gradle/plugin', () => { "name": "proj", "targets": { "test": { - "cache": false, + "cache": true, "command": "./gradlew proj:test", "dependsOn": [ "classes", @@ -114,11 +115,81 @@ 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', 'Verification']])], + ]), + gradleProjectToProjectName: new Map([['proj', 'proj']]), + }; + await tempFs.createFiles({ + 'nested/nested/proj/build.gradle': ``, + }); + + 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": { + "Verification": [ + "test", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "targets": { + "test": { + "cache": true, + "command": "./gradlew proj:test", + "dependsOn": [ + "classes", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "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']])], @@ -126,13 +197,28 @@ describe('@nx/gradle/plugin', () => { gradleProjectToProjectName: new Map([['proj', 'proj']]), }; await tempFs.createFiles({ - 'nested/nested/proj/gradle.build': ``, + '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/gradle.build'], + [ + 'nested/nested/proj/build.gradle', + 'proj/src/test/java/test/rootTest.java', + 'nested/nested/proj/src/test/java/test/test.java', + 'nested/nested/proj/src/test/java/test/test1.java', + ], { buildTargetName: 'build', + ciTargetName: 'test-ci', }, context ); @@ -140,13 +226,16 @@ describe('@nx/gradle/plugin', () => { expect(results).toMatchInlineSnapshot(` [ [ - "nested/nested/proj/gradle.build", + "nested/nested/proj/build.gradle", { "projects": { "nested/nested/proj": { "metadata": { "targetGroups": { "Test": [ + "test-ci--test", + "test-ci--test1", + "test-ci", "test", ], }, @@ -172,6 +261,67 @@ describe('@nx/gradle/plugin', () => { ], }, }, + "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", + "nonAtomizedTarget": "test", + "technologies": [ + "gradle", + ], + }, + }, + "test-ci--test": { + "cache": true, + "command": "./gradlew proj:test --tests test", + "dependsOn": [ + "classes", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle test nested/nested/proj/src/test/java/test/test.java in CI", + "technologies": [ + "gradle", + ], + }, + }, + "test-ci--test1": { + "cache": true, + "command": "./gradlew proj:test --tests test1", + "dependsOn": [ + "classes", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle test nested/nested/proj/src/test/java/test/test1.java in CI", + "technologies": [ + "gradle", + ], + }, + }, }, }, }, diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts index 222d9f5ed8ed5..a42bdd9ac0f5d 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,8 +12,9 @@ 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 { findProjectForPath } from 'nx/src/devkit-internals'; import { getGradleExecFile } from '../utils/exec-gradle'; import { @@ -22,6 +22,8 @@ import { getCurrentGradleReport, GradleReport, gradleConfigGlob, + GRADLE_BUILD_FILES, + gradleConfigAndTestGlob, } from '../utils/get-gradle-report'; import { hashObject } from 'nx/src/hasher/file-hasher'; @@ -38,12 +40,21 @@ interface GradleTask { } export interface GradlePluginOptions { + ciTargetName?: string; testTargetName?: string; classesTargetName?: string; buildTargetName?: string; [taskTargetName: string]: string | undefined; } +function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions { + options ??= {}; + options.testTargetName ??= 'test'; + options.classesTargetName ??= 'classes'; + options.buildTargetName ??= 'build'; + return options; +} + type GradleTargets = Record< string, { @@ -62,8 +73,9 @@ export function writeTargetsToCache(cachePath: string, results: GradleTargets) { } export const createNodesV2: CreateNodesV2 = [ - gradleConfigGlob, - async (configFiles, options, context) => { + gradleConfigAndTestGlob, + async (files, options, context) => { + const { configFiles, projectRoots, testFiles } = splitConfigFiles(files); const optionsHash = hashObject(options); const cachePath = join( workspaceDataDirectory, @@ -73,10 +85,18 @@ export const createNodesV2: CreateNodesV2 = [ await populateGradleReport(context.workspaceRoot); const gradleReport = getCurrentGradleReport(); + const gradleProjectRootToTestFilesMap = getGradleProjectRootToTestFilesMap( + testFiles, + projectRoots + ); try { - return await createNodesFromFiles( - makeCreateNodes(gradleReport, targetsCache), + return createNodesFromFiles( + makeCreateNodesForGradleConfigFile( + gradleReport, + targetsCache, + gradleProjectRootToTestFilesMap + ), configFiles, options, context @@ -87,10 +107,11 @@ export const createNodesV2: CreateNodesV2 = [ }, ]; -export const makeCreateNodes = +export const makeCreateNodesForGradleConfigFile = ( gradleReport: GradleReport, - targetsCache: GradleTargets + targetsCache: GradleTargets = {}, + gradleProjectRootToTestFilesMap: Record = {} ): CreateNodesFunction => async ( gradleFilePath, @@ -98,17 +119,19 @@ 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, - context + context, + gradleProjectRootToTestFilesMap[projectRoot] ); const project = targetsCache[hash]; if (!project) { @@ -133,16 +156,18 @@ export const createNodes: CreateNodes = [ ); await populateGradleReport(context.workspaceRoot); const gradleReport = getCurrentGradleReport(); - const internalCreateNodes = makeCreateNodes(gradleReport, {}); + const internalCreateNodes = + makeCreateNodesForGradleConfigFile(gradleReport); return await internalCreateNodes(configFile, options, context); }, ]; -function createGradleProject( +async function createGradleProject( gradleReport: GradleReport, gradleFilePath: string, options: GradlePluginOptions | undefined, - context: CreateNodesContext + context: CreateNodesContext, + testFiles = [] ) { try { const { @@ -177,12 +202,14 @@ function createGradleProject( string >; - const { targets, targetGroups } = createGradleTargets( + const { targets, targetGroups } = await createGradleTargets( tasks, options, context, outputDirs, - gradleProject + gradleProject, + gradleFilePath, + testFiles ); const project = { name: projectName, @@ -200,16 +227,18 @@ function createGradleProject( } } -function createGradleTargets( +async function createGradleTargets( tasks: GradleTask[], options: GradlePluginOptions | undefined, context: CreateNodesContext, outputDirs: Map, - gradleProject: string -): { + gradleProject: string, + gradleFilePath: string, + testFiles: string[] = [] +): Promise<{ targetGroups: Record; targets: Record; -} { +}> { const inputsMap = createInputsMap(context); const targets: Record = {}; @@ -217,28 +246,43 @@ function createGradleTargets( for (const task of tasks) { const targetName = options?.[`${task.name}TargetName`] ?? task.name; - const outputs = outputDirs.get(task.name); + let outputs = [outputDirs.get(task.name)].filter(Boolean); + if (task.name === 'test') { + outputs = [ + outputDirs.get('testReport'), + outputDirs.get('testResults'), + ].filter(Boolean); + getTestCiTargets( + testFiles, + gradleProject, + targetName, + options.ciTargetName, + inputsMap['test'], + outputs, + task.type, + targets, + targetGroups + ); + } + 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'], }, + ...(outputs && outputs.length ? { outputs } : {}), }; - 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 +301,103 @@ function createInputsMap( : ['default', '^default'], }; } + +function getTestCiTargets( + testFiles: string[], + gradleProject: string, + testTargetName: string, + ciTargetName: string, + inputs: TargetConfiguration['inputs'], + outputs: string[], + targetGroupName: string, + targets: Record, + targetGroups: Record +): void { + if (!testFiles || testFiles.length === 0 || !ciTargetName) { + return; + } + const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}test`; + + if (!targetGroups[targetGroupName]) { + targetGroups[targetGroupName] = []; + } + + 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, + dependsOn: dependsOnMap['test'], + metadata: { + technologies: ['gradle'], + description: `Runs Gradle test ${testFile} in CI`, + }, + ...(outputs && outputs.length > 0 ? { outputs } : {}), + }; + targetGroups[targetGroupName].push(targetName); + dependsOn.push({ + target: targetName, + projects: 'self', + params: 'forward', + }); + }); + + targets[ciTargetName] = { + executor: 'nx:noop', + cache: true, + inputs, + dependsOn: dependsOn, + ...(outputs && outputs.length > 0 ? { outputs } : {}), + metadata: { + technologies: ['gradle'], + description: 'Runs Gradle Tests in CI', + nonAtomizedTarget: testTargetName, + }, + }; + targetGroups[targetGroupName].push(ciTargetName); +} + +function splitConfigFiles(files: readonly string[]): { + configFiles: string[]; + testFiles: string[]; + projectRoots: string[]; +} { + const configFiles = []; + const testFiles = []; + const projectRoots = new Set(); + files.forEach((file) => { + if (GRADLE_BUILD_FILES.has(basename(file))) { + configFiles.push(file); + projectRoots.add(dirname(file)); + } else { + testFiles.push(file); + } + }); + + return { configFiles, testFiles, projectRoots: Array.from(projectRoots) }; +} + +function getGradleProjectRootToTestFilesMap( + testFiles: string[], + projectRoots: string[] +): Record | undefined { + if (testFiles.length === 0 || projectRoots.length === 0) { + return; + } + const roots = new Map(projectRoots.map((root) => [root, root])); + const testFilesToGradleProjectMap: Record = {}; + testFiles.forEach((testFile) => { + const projectRoot = findProjectForPath(testFile, roots); + if (projectRoot) { + if (!testFilesToGradleProjectMap[projectRoot]) { + testFilesToGradleProjectMap[projectRoot] = []; + } + testFilesToGradleProjectMap[projectRoot].push(testFile); + } + }); + return testFilesToGradleProjectMap; +} diff --git a/packages/gradle/src/utils/get-gradle-report.ts b/packages/gradle/src/utils/get-gradle-report.ts index d3c2bb645a9ad..7c5e22db2add6 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,20 @@ export interface GradleReport { let gradleReportCache: GradleReport; let gradleCurrentConfigHash: string; -export const gradleConfigGlob = '**/build.{gradle.kts,gradle}'; +export const GRADLE_BUILD_FILES = new Set(['build.gradle', 'build.gradle.kts']); +export const GRADLE_TEST_FILES = [ + '**/src/test/java/**/*.java', + '**/src/test/kotlin/**/*.kt', +]; + +export const gradleConfigGlob = combineGlobPatterns( + ...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`) +); + +export const gradleConfigAndTestGlob = combineGlobPatterns( + ...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`), + ...GRADLE_TEST_FILES +); export function getCurrentGradleReport() { if (!gradleReportCache) {