From 5b9b94e937591897a23050bcd677c268df195d9e Mon Sep 17 00:00:00 2001 From: Matt Carvin <90224411+mcarvin8@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:08:42 -0400 Subject: [PATCH] fix: validate tests against package directories --- README.md | 2 + messages/delta.md | 4 ++ src/commands/apex-tests-git-delta/delta.ts | 24 ++++++++--- src/service/extractTestClasses.ts | 45 +++++++++++++------ src/service/getPackageDirectories.ts | 20 +++++++++ src/service/validateClassPaths.ts | 50 ++++++++++++++++++++++ test/commands/delta/unit.test.ts | 37 ++++++++++++---- 7 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 src/service/getPackageDirectories.ts create mode 100644 src/service/validateClassPaths.ts diff --git a/README.md b/README.md index 949e616..e00f328 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ testclasses=$( { public static readonly examples = messages.getMessages('examples'); public static readonly flags = { - 'to': Flags.string({ + to: Flags.string({ char: 't', summary: messages.getMessage('flags.to.summary'), required: true, default: TO_DEFAULT_VALUE, }), - 'from': Flags.string({ + from: Flags.string({ char: 'f', summary: messages.getMessage('flags.from.summary'), required: true, @@ -38,12 +38,19 @@ export default class ApexTestDelta extends SfCommand { exists: true, default: 'regex.txt', }), - 'output': Flags.file({ + output: Flags.file({ summary: messages.getMessage('flags.output.summary'), required: true, exists: false, default: 'runTests.txt', }), + 'sfdx-configuration': Flags.file({ + summary: messages.getMessage('flags.sfdx-configuration.summary'), + char: 'c', + required: true, + exists: true, + default: 'sfdx-project.json', + }), }; public async run(): Promise { @@ -52,11 +59,18 @@ export default class ApexTestDelta extends SfCommand { const fromGitRef = flags['from']; const regExFile = flags['regular-expression']; const output = flags['output']; + const sfdxConfigFile = flags['sfdx-configuration']; - const deltaTests = extractTestClasses(fromGitRef, toGitRef, regExFile); + const result = await extractTestClasses(fromGitRef, toGitRef, regExFile, sfdxConfigFile); + const deltaTests = result.validatedClasses; + const warnings = result.warnings; this.log(deltaTests); fs.writeFileSync(output, deltaTests); - + if (warnings.length > 0) { + warnings.forEach((warning) => { + this.warn(warning); + }); + } return { tests: deltaTests }; } } diff --git a/src/service/extractTestClasses.ts b/src/service/extractTestClasses.ts index bccb97f..5972742 100644 --- a/src/service/extractTestClasses.ts +++ b/src/service/extractTestClasses.ts @@ -1,24 +1,41 @@ -'use strict' +'use strict'; import { retrieveCommitMessages } from './retrieveCommitMessages.js'; +import { validateClassPaths } from './validateClassPaths.js'; -export function extractTestClasses(fromRef: string, toRef: string, regex: string): string { +export async function extractTestClasses( + fromRef: string, + toRef: string, + regex: string, + sfdxConfigFile: string +): Promise<{ validatedClasses: string; warnings: string[] }> { const testClasses: Set = new Set(); const matchedMessages = retrieveCommitMessages(fromRef, toRef, regex); matchedMessages.forEach((message: string) => { - // Split the commit message by commas or spaces - const classes = message.split(/,|\s/); + // Split the commit message by commas or spaces + const classes = message.split(/,|\s/); - classes.forEach((testClass: string) => { - // Remove leading/trailing whitespaces and add non-empty strings to the set - const trimmedClass = testClass.trim(); - if (trimmedClass !== '') { - testClasses.add(trimmedClass); - } - }); + classes.forEach((testClass: string) => { + // Remove leading/trailing whitespaces and add non-empty strings to the set + const trimmedClass = testClass.trim(); + if (trimmedClass !== '') { + testClasses.add(trimmedClass); + } + }); }); - // Sort test classes alphabetically and then return a space-separated string - const sortedClasses = Array.from(testClasses).sort((a, b) => a.localeCompare(b)); - return sortedClasses.join(' '); + const unvalidatedClasses: string[] = Array.from(testClasses); + let validatedClasses: string = ''; + const result = + unvalidatedClasses.length > 0 + ? await validateClassPaths(unvalidatedClasses, sfdxConfigFile) + : { validatedClasses: new Set(), warnings: [] }; + let sortedClasses: string[] = []; + if (result.validatedClasses.size > 0) { + sortedClasses = Array.from(result.validatedClasses) as string[]; + sortedClasses = sortedClasses.sort((a, b) => a.localeCompare(b)); + validatedClasses = sortedClasses.join(' '); + } + + return { validatedClasses, warnings: result.warnings }; } diff --git a/src/service/getPackageDirectories.ts b/src/service/getPackageDirectories.ts new file mode 100644 index 0000000..ae07f5c --- /dev/null +++ b/src/service/getPackageDirectories.ts @@ -0,0 +1,20 @@ +'use strict'; +/* eslint-disable no-await-in-loop */ + +import * as fs from 'node:fs'; +import * as promises from 'node:fs/promises'; + +interface SfdxProject { + packageDirectories: Array<{ path: string }>; +} + +export async function getPackageDirectories(dxConfigFile: string): Promise { + if (!fs.existsSync(dxConfigFile)) { + throw Error(`Salesforce DX Config File does not exist in this path: ${dxConfigFile}`); + } + + const sfdxProjectRaw: string = await promises.readFile(dxConfigFile, 'utf-8'); + const sfdxProject: SfdxProject = JSON.parse(sfdxProjectRaw) as SfdxProject; + const packageDirectories = sfdxProject.packageDirectories.map((directory) => directory.path); + return packageDirectories; +} diff --git a/src/service/validateClassPaths.ts b/src/service/validateClassPaths.ts new file mode 100644 index 0000000..6c5b4a5 --- /dev/null +++ b/src/service/validateClassPaths.ts @@ -0,0 +1,50 @@ +'use strict'; +/* eslint-disable no-await-in-loop */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { getPackageDirectories } from './getPackageDirectories.js'; + +export async function validateClassPaths( + unvalidatedClasses: string[], + dxConfigFile: string +): Promise<{ validatedClasses: Set; warnings: string[] }> { + const packageDirectories = await getPackageDirectories(dxConfigFile); + const warnings: string[] = []; + + const validatedClasses: Set = new Set(); + for (const unvalidatedClass of unvalidatedClasses) { + let validated: boolean = false; + for (const directory of packageDirectories) { + const relativeFilePath = await searchRecursively(`${unvalidatedClass}.cls`, directory); + if (relativeFilePath !== undefined) { + validatedClasses.add(unvalidatedClass); + validated = true; + break; + } + } + if (!validated) + warnings.push( + `The class ${unvalidatedClass} was not found in any package directory and will not be added to the delta test classes.` + ); + } + return { validatedClasses, warnings }; +} + +async function searchRecursively(fileName: string, dxDirectory: string): Promise { + const files = await fs.promises.readdir(dxDirectory); + for (const file of files) { + const filePath = path.join(dxDirectory, file); + const stats = await fs.promises.stat(filePath); + if (stats.isDirectory()) { + const result = await searchRecursively(fileName, filePath); + if (result) { + return result; + } + } else if (file === fileName) { + return filePath; + } + } + return undefined; +} diff --git a/test/commands/delta/unit.test.ts b/test/commands/delta/unit.test.ts index 468059f..4a8f1db 100644 --- a/test/commands/delta/unit.test.ts +++ b/test/commands/delta/unit.test.ts @@ -7,13 +7,11 @@ import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; import ApexTestDelta from '../../../src/commands/apex-tests-git-delta/delta.js'; // Utility function to create temporary Git commits -async function createTemporaryCommit(message: string, content: string): Promise { - // Create a temporary file - const tempFilePath = 'temp.txt'; - await fs.promises.writeFile(tempFilePath, content); +async function createTemporaryCommit(message: string, filePath: string, content: string): Promise { + await fs.promises.writeFile(filePath, content); // Stage the file - execSync('git add temp.txt'); + execSync(`git add "${filePath}"`); // Commit with the provided message execSync(`git commit -m "${message}"`); @@ -32,13 +30,24 @@ describe('return the delta tests between git commits', () => { const regExFile: string = 'regex.txt'; const regExFileContents: string = '[Aa][Pp][Ee][Xx]::(.*?)::[Aa][Pp][Ee][Xx]'; const originalDir = process.cwd(); + const sfdxConfigFile = 'sfdx-project.json'; + const sfdxConfigFileContents = { + packageDirectories: [{ path: 'force-app', default: true }, { path: 'packaged' }], + namespace: '', + sfdcLoginUrl: 'https://login.salesforce.com', + sourceApiVersion: '58.0', + }; + const sfdxConfigJsonString = JSON.stringify(sfdxConfigFileContents, null, 2); const tempDir = fs.mkdtempSync('../git-temp-'); before(async () => { process.chdir(tempDir); + fs.mkdirSync('force-app/main/default/classes', { recursive: true }); + fs.mkdirSync('packaged/classes', { recursive: true }); execSync('git init', { cwd: tempDir }); execSync('git branch -m main'); fs.writeFileSync(regExFile, regExFileContents); + fs.writeFileSync(sfdxConfigFile, sfdxConfigJsonString); let userName = ''; let userEmail = ''; @@ -53,9 +62,21 @@ describe('return the delta tests between git commits', () => { execSync('git config --global user.name "CI Bot"'); execSync('git config --global user.email "90224411+mcarvin8@users.noreply.github.com"'); } - fromSha = await createTemporaryCommit('chore: initial commit with Apex::TestClass00::Apex', 'dummy 1'); - await createTemporaryCommit('chore: initial commit with Apex::SandboxTest::Apex', 'dummy 11'); - toSha = await createTemporaryCommit('chore: adding new tests Apex::TestClass3 TestClass4::Apex', 'dummy 2'); + fromSha = await createTemporaryCommit( + 'chore: initial commit with Apex::TestClass00::Apex', + 'force-app/main/default/classes/SandboxTest.cls', + 'dummy 1' + ); + await createTemporaryCommit( + 'chore: initial commit with Apex::SandboxTest::Apex', + 'force-app/main/default/classes/TestClass3.cls', + 'dummy 11' + ); + toSha = await createTemporaryCommit( + 'chore: adding new tests Apex::TestClass3 TestClass4::Apex', + 'packaged/classes/TestClass4.cls', + 'dummy 2' + ); }); beforeEach(() => {