diff --git a/.gitignore b/.gitignore index 65e5fa0..9bdad43 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ docs package.json.bak. # -- CLEAN ALL +*.tsbuildinfo +.eslintcache +.wireit node_modules # -- diff --git a/.sfdevrc.json b/.sfdevrc.json index 26f3c0b..66121e1 100644 --- a/.sfdevrc.json +++ b/.sfdevrc.json @@ -1,5 +1,10 @@ { "test": { "testsPath": "test/**/*.test.ts" + }, + "wireit": { + "test": { + "dependencies": ["test:compile", "test:only", "lint"] + } } } diff --git a/package.json b/package.json index a985c1b..a2c9078 100644 --- a/package.json +++ b/package.json @@ -74,23 +74,126 @@ } }, "scripts": { - "build": "sf-build", + "build": "wireit", "clean": "sf-clean", "clean-all": "sf-clean all", "clean:lib": "shx rm -rf lib && shx rm -rf coverage && shx rm -rf .nyc_output && shx rm -f oclif.manifest.json", - "compile": "sf-compile", - "format": "sf-format", - "lint": "sf-lint", + "compile": "wireit", + "docs": "sf-docs", + "format": "wireit", + "lint": "wireit", + "postinstall": "yarn husky install", "postpack": "shx rm -f oclif.manifest.json", - "posttest": "yarn lint", "prepack": "sf-prepack", - "pretest": "sf-compile-test", - "test": "sf-test", + "test": "wireit", "test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel", + "test:only": "wireit", "version": "oclif readme" }, "publishConfig": { "access": "public" }, + "wireit": { + "build": { + "dependencies": [ + "compile", + "lint" + ] + }, + "compile": { + "command": "tsc -p . --pretty --incremental", + "files": [ + "src/**/*.ts", + "**/tsconfig.json", + "messages/**" + ], + "output": [ + "lib/**", + "*.tsbuildinfo" + ], + "clean": "if-file-deleted" + }, + "format": { + "command": "prettier --write \"+(src|test|schemas)/**/*.+(ts|js|json)|command-snapshot.json\"", + "files": [ + "src/**/*.ts", + "test/**/*.ts", + "schemas/**/*.json", + "command-snapshot.json", + ".prettier*" + ], + "output": [] + }, + "lint": { + "command": "eslint src test --color --cache --cache-location .eslintcache", + "files": [ + "src/**/*.ts", + "test/**/*.ts", + "messages/**", + "**/.eslint*", + "**/tsconfig.json" + ], + "output": [] + }, + "test:compile": { + "command": "tsc -p \"./test\" --pretty", + "files": [ + "test/**/*.ts", + "**/tsconfig.json" + ], + "output": [] + }, + "test": { + "dependencies": [ + "test:compile", + "test:only", + "lint" + ] + }, + "test:only": { + "command": "nyc mocha \"test/**/*.test.ts\"", + "env": { + "FORCE_COLOR": "2" + }, + "files": [ + "test/**/*.ts", + "src/**/*.ts", + "**/tsconfig.json", + ".mocha*", + "!*.nut.ts", + ".nycrc" + ], + "output": [] + }, + "test:command-reference": { + "command": "\"./bin/dev\" commandreference:generate --erroronwarnings", + "files": [ + "src/**/*.ts", + "messages/**", + "package.json" + ], + "output": [ + "tmp/root" + ] + }, + "test:deprecation-policy": { + "command": "\"./bin/dev\" snapshot:compare", + "files": [ + "src/**/*.ts" + ], + "output": [], + "dependencies": [ + "compile" + ] + }, + "test:json-schema": { + "command": "\"./bin/dev\" schema:compare", + "files": [ + "src/**/*.ts", + "schemas" + ], + "output": [] + } + }, "author": "Anthony Heber" } diff --git a/src/commands/warp/apex.ts b/src/commands/warp/apex.ts index f280ac4..4169b2f 100644 --- a/src/commands/warp/apex.ts +++ b/src/commands/warp/apex.ts @@ -18,6 +18,7 @@ export default class Apex extends SfCommand { summary: messages.getMessage('flags.class.summary'), char: 'c', multiple: true, + required: true, }), 'test-class': Flags.string({ summary: messages.getMessage('flags.test-class.summary'), @@ -83,7 +84,7 @@ export default class Apex extends SfCommand { testClassMatchPatterns: flags['test-class-match-pattern'], classes: flags.class.map((className) => ({ className, - testClasses: flags['test-class'], + testClasses: flags['test-class'] ?? [], })), }).executeWarpTests(); } diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 8717dbf..0000000 --- a/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export = {}; diff --git a/src/lib/commands/apex.ts b/src/lib/commands/apex.ts index 0c0e2f2..adc672c 100644 --- a/src/lib/commands/apex.ts +++ b/src/lib/commands/apex.ts @@ -1,7 +1,7 @@ /* eslint-disable complexity */ /* eslint-disable no-console */ -import * as fs from 'fs'; -import * as path from 'path'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; import * as process from 'node:process'; import { Connection } from '@salesforce/core'; import { getApexParser } from 'web-tree-sitter-sfapex'; @@ -47,14 +47,13 @@ interface Mutant { startPosition: number; testResults; status: string; - error: string; - deploymentDurationMs: number; - testExecuteDurationMs: number; + error?: string; + deploymentDurationMs?: number; + testExecuteDurationMs?: number; } -const isVerboseEnough = (val: Verbosity, minimumVerbosity: Verbosity): boolean => { - return VerbosityVal[val] >= VerbosityVal[minimumVerbosity]; -}; +const isVerboseEnough = (val: Verbosity, minimumVerbosity: Verbosity): boolean => + VerbosityVal[val] >= VerbosityVal[minimumVerbosity]; export default class ApexWarper { // takes in configuration @@ -87,18 +86,19 @@ export default class ApexWarper { this.parser = await getApexParser(); for (const classUnderTest of this.config.classes) { const totalExecutePerfName = getPerfStart(); - if (!classUnderTest.testClasses) { - classUnderTest.testClasses = []; - } // if no tests are specified, try and locate tests inside the org + const promises = [] as Array>; if (!classUnderTest.testClasses || classUnderTest.testClasses.length === 0) { - await this.usePatternsToGuessAtTestClasses(classUnderTest); + promises.push(this.usePatternsToGuessAtTestClasses(classUnderTest)); } + // eslint-disable-next-line no-await-in-loop + await Promise.all(promises); if (classUnderTest.testClasses.length === 0) { throw new Error('No test classes identified, unable to continue'); } // run tests first to ensure they are valid and passing in the current config + // eslint-disable-next-line no-await-in-loop let testResults = await executeTests(this.conn, classUnderTest.testClasses, this.config.timeoutMs); if (testResults.MethodsFailed > 0) { throw new Error('Tests not passing before modifying target, unable to start warp'); @@ -107,6 +107,7 @@ export default class ApexWarper { console.log('All tests passing before warping target'); } const className = classUnderTest.className; + // eslint-disable-next-line no-await-in-loop const classes = await getApexClasses(this.conn, [classUnderTest.className]); const parsePerfName = getPerfStart(); for (const c of classes) { @@ -127,20 +128,21 @@ export default class ApexWarper { // TODO: Need to figure out how to block mutants inside of "ignore" sections const captures = this.getCaptures(tree, query).filter( - (c) => !(this.config.suppressedRuleNames || []).includes(c.name) + (c) => !(this.config.suppressedRuleNames ?? []).includes(c.name), ); if (this.atLeastVerbosity(Verbosity.minimal)) { console.log( - `Found ${captures.length} candidates in ${className}, testing with ${classUnderTest.testClasses.join(', ')}` + `Found ${captures.length} candidates in ${className}, testing with ${classUnderTest.testClasses.join(', ')}`, ); } let mutantsKilled = 0; let count = 0; - this.mutants.set(className, []); + const mutantList: Mutant[] = []; + this.mutants.set(className, mutantList); for (const capture of captures) { let finalStatus = 'unknown'; - let finalStatusMessage: string; + let finalStatusMessage: string | undefined; const perfName = getPerfStart(); const oldLines: Lines = {}; @@ -148,12 +150,12 @@ export default class ApexWarper { oldLines[i] = lines[i]; lines[i] = ''; // blank out the line so it is easier to inject replacements later } - let deployDuration: number; - let testPerfDuration: number; + let deployDuration: number | undefined; + let testPerfDuration: number | undefined; try { const textParts = getMutatedParts(capture, oldLines); if (this.atLeastVerbosity(Verbosity.details)) { - this.reportMutant(capture, oldLines, textParts); + reportMutant(capture, oldLines, textParts); } lines[capture.node.startPosition.row] = textParts.join(''); // push the file to the org @@ -161,6 +163,7 @@ export default class ApexWarper { this.orgClassIsMutated = true; const writePerfName = getPerfStart(); if (!this.config.analyzeOnly) { + // eslint-disable-next-line no-await-in-loop await this.writeApexClassesToOrg(classUnderTest.className, lines.join('\n')); } deployDuration = getPerfDurationMs(writePerfName); @@ -170,6 +173,7 @@ export default class ApexWarper { // capture the results against that mutant const testPerfName = getPerfStart(); if (!this.config.analyzeOnly) { + // eslint-disable-next-line no-await-in-loop testResults = await executeTests(this.conn, classUnderTest.testClasses, this.config.timeoutMs); } testPerfDuration = getPerfDurationMs(testPerfName); @@ -211,7 +215,7 @@ export default class ApexWarper { } finalStatusMessage = errorMessage; } - this.mutants.get(className).push({ + mutantList.push({ type: capture.name, startLine: capture.node.startPosition.row, startPosition: capture.node.startPosition.column, @@ -237,11 +241,12 @@ export default class ApexWarper { `\nKilled ${mutantsKilled}/${count} (${( (mutantsKilled / count) * 100 - ).toFixed()}%) in ${getPerfDurationHumanReadable(totalExecutePerfName)}` + ).toFixed()}%) in ${getPerfDurationHumanReadable(totalExecutePerfName)}`, ); } // put the class back the way we found it, what if they break the command?? // probably best to try and capture the break command and fix the org code + // eslint-disable-next-line no-await-in-loop await this.writeApexClassesToOrg(classUnderTest.className, originalClassText); this.orgClassIsMutated = false; } @@ -259,17 +264,6 @@ export default class ApexWarper { return captures; } - private reportMutant(capture: QueryCapture, oldText: Lines, newLineParts: string[]): void { - // probably a smarter way to do this out there... - const [start, middle, end] = getTextParts(oldText, capture.node); - console.log( - `Start Line ${capture.node.startPosition.row} | ${capture.name}\n`, - `- ${start}\x1b[32m${middle}\x1b[0m${end}`, - '\n', - `+ ${newLineParts[0]}\x1b[31m${newLineParts[1]}\x1b[0m${newLineParts[2]}` - ); - } - private subscribeToSignalToMaybeUnwind(className: string, originalClassText: string): void { // Using a single function to handle multiple signals // if the class in the org is currently mutated, it must be restored on term @@ -306,7 +300,7 @@ export default class ApexWarper { if (this.unwindingPromise !== undefined) { await this.unwindingPromise; } - return writeApexClassesToOrg(this.conn, this.classMapByName[className].Id || '', body, this.config.timeoutMs); + return writeApexClassesToOrg(this.conn, this.classMapByName[className].Id ?? '', body, this.config.timeoutMs); } private async usePatternsToGuessAtTestClasses(classUnderTest: { @@ -319,10 +313,27 @@ export default class ApexWarper { } const testClassResults = await getApexClasses(this.conn, testClassCandidates); + const promises: Array> = []; for (const r of testClassResults) { - if (await isTestClass(r.Body)) { - classUnderTest.testClasses.push(r.Name); - } + promises.push( + isTestClass(r.Body).then((res) => { + if (res) { + classUnderTest.testClasses.push(r.Name); + } + }), + ); } + await Promise.all(promises); } } + +function reportMutant(capture: QueryCapture, oldText: Lines, newLineParts: string[]): void { + // probably a smarter way to do this out there... + const [start, middle, end] = getTextParts(oldText, capture.node); + console.log( + `Start Line ${capture.node.startPosition.row} | ${capture.name}\n`, + `- ${start}\x1b[32m${middle}\x1b[0m${end}`, + '\n', + `+ ${newLineParts[0]}\x1b[31m${newLineParts[1]}\x1b[0m${newLineParts[2]}`, + ); +} diff --git a/src/lib/perf.ts b/src/lib/perf.ts index 68b884d..97ac44b 100644 --- a/src/lib/perf.ts +++ b/src/lib/perf.ts @@ -1,4 +1,4 @@ -import { performance } from 'perf_hooks'; +import { performance } from 'node:perf_hooks'; let perfMarkCount = 0; export function getPerfStart(): string { diff --git a/src/lib/polling.ts b/src/lib/polling.ts index 1b56923..259829b 100644 --- a/src/lib/polling.ts +++ b/src/lib/polling.ts @@ -6,7 +6,7 @@ interface PollConfig { cancelAction?(): void; } -const cancelledTimeouts = []; +const cancelledTimeouts: NodeJS.Timeout[] = []; const MAX_POLL_MS = 3000; export function pollForResult(pollConfig: PollConfig): Promise { @@ -15,7 +15,7 @@ export function pollForResult(pollConfig: PollConfig): Promise { timeoutId = setTimeout(() => { reject('Timeout polling action'); cancelledTimeouts.push(timeoutId); - }, pollConfig.timeout || 30000); + }, pollConfig.timeout ?? 30000); void executePollAction(pollConfig, resolve, reject, timeoutId); }).finally(() => clearTimeout(timeoutId)) as Promise; } @@ -26,7 +26,7 @@ function executePollAction( // eslint-disable-next-line @typescript-eslint/no-explicit-any reject: (reason) => any, timeoutId: NodeJS.Timeout, - pollTime?: number + pollTime?: number, ): void { if (cancelledTimeouts.includes(timeoutId)) { if (pollConfig.cancelAction) { @@ -34,9 +34,9 @@ function executePollAction( } return; } - const waitTime = pollTime || pollConfig.initialWaitMs || 1000; + const waitTime = pollTime ?? pollConfig.initialWaitMs ?? 1000; // eslint-disable-next-line @typescript-eslint/no-misused-promises - setTimeout(async function () { + setTimeout(async () => { let output; try { output = (await pollConfig.action.call(null)) as T; diff --git a/src/lib/sf.ts b/src/lib/sf.ts index 8cecaef..b6464c7 100644 --- a/src/lib/sf.ts +++ b/src/lib/sf.ts @@ -26,7 +26,7 @@ export async function getApexClasses(conn: Connection, classes: string[]): Promi const res = await conn.tooling.query( `SELECT Id, Name, Body FROM ApexClass WHERE Name IN (${classes .map((c) => `'${c}'`) - .join(',')}) AND ManageableState = 'unmanaged'` + .join(',')}) AND ManageableState = 'unmanaged'`, ); return res.records; } @@ -34,7 +34,7 @@ export async function getApexClasses(conn: Connection, classes: string[]): Promi export async function executeTests( conn: Connection, testClasses: string[], - timeoutMs: number + timeoutMs: number, ): Promise { const asyncJobId = await conn.tooling.runTestsAsynchronous({ classNames: testClasses.join(',') }); @@ -43,7 +43,7 @@ export async function executeTests( actionName: `TestClasses:${testClasses[0]}`, action: async () => { const request = await conn.tooling.query( - `SELECT Id, Status, TestTime, ClassesCompleted, ClassesEnqueued, EndTime, MethodsEnqueued, MethodsFailed FROM ApexTestRunResult WHERE AsyncApexJobId = '${asyncJobId}'` + `SELECT Id, Status, TestTime, ClassesCompleted, ClassesEnqueued, EndTime, MethodsEnqueued, MethodsFailed FROM ApexTestRunResult WHERE AsyncApexJobId = '${asyncJobId}'`, ); if (!['Queued', 'Processing'].includes(request.records[0].Status)) { return request.records[0]; @@ -56,7 +56,7 @@ export async function writeApexClassesToOrg( conn: Connection, classId: string, body: string, - timeoutMs: number + timeoutMs: number, ): Promise { const mdContainer = await conn.tooling.create('MetadataContainer', { Name: 'WarpIt' + `${new Date().getTime()}`, @@ -74,7 +74,7 @@ export async function writeApexClassesToOrg( timeout: timeoutMs, action: async () => { const request = await conn.tooling.query( - `SELECT Id, State, ErrorMsg, DeployDetails FROM ContainerAsyncRequest WHERE Id = '${requestSaveResult.id}'` + `SELECT Id, State, ErrorMsg, DeployDetails FROM ContainerAsyncRequest WHERE Id = '${requestSaveResult.id}'`, ); if (!(request.records[0].State === 'Queued')) { return request.records[0]; diff --git a/test/test.ts b/test/test.ts new file mode 100644 index 0000000..b860bd2 --- /dev/null +++ b/test/test.ts @@ -0,0 +1 @@ +// ignore me diff --git a/test/test1.ts b/test/test1.ts deleted file mode 100644 index cfd843c..0000000 --- a/test/test1.ts +++ /dev/null @@ -1,5 +0,0 @@ -function test123() { - // eslint-disable-next-line no-console - console.log('Tests Executed'); -} -test123(); diff --git a/test/tsconfig.json b/test/tsconfig.json index 3fee52b..07de9d7 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@salesforce/dev-config/tsconfig-test", "include": ["./**/*.ts"], "compilerOptions": { - "skipLibCheck": true + "skipLibCheck": true, + "strictNullChecks": true } } diff --git a/tsconfig.json b/tsconfig.json index dfc5665..239efbc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "skipLibCheck": true + "skipLibCheck": true, + "strictNullChecks": true }, "include": ["./src/**/*.ts"] }