Skip to content

Commit

Permalink
feat: run apex tests using the library (#2828)
Browse files Browse the repository at this point in the history
@W-7561367@
  • Loading branch information
AnanyaJha authored Jan 6, 2021
1 parent 53329af commit ff73bb1
Show file tree
Hide file tree
Showing 23 changed files with 874 additions and 270 deletions.
2 changes: 2 additions & 0 deletions packages/salesforcedx-utils-vscode/src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { WorkspaceContextUtil } from './workspaceContextUtil';
export const workspaceContext = WorkspaceContextUtil.getInstance();
export { OrgInfo, WorkspaceContextUtil } from './workspaceContextUtil';
1 change: 1 addition & 0 deletions packages/salesforcedx-vscode-apex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
],
"dependencies": {
"@salesforce/apex-tmlanguage": "1.4.0",
"@salesforce/apex-node": "0.1.10",
"@salesforce/core": "2.11.0",
"@salesforce/salesforcedx-sobjects-faux-generator": "50.12.0",
"@salesforce/salesforcedx-utils-vscode": "50.12.0",
Expand Down
69 changes: 42 additions & 27 deletions packages/salesforcedx-vscode-apex/src/codecoverage/colorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { CodeCoverageResult } from '@salesforce/apex-node';
import * as fs from 'fs';
import * as path from 'path';
import { join, sep } from 'path';
import {
Range,
TextDocument,
Expand All @@ -22,7 +23,7 @@ import {
} from './decorations';
import { StatusBarToggle } from './statusBarToggle';

const apexDirPath = path.join(
const apexDirPath = join(
workspace!.workspaceFolders![0].uri.fsPath,
'.sfdx',
'tools',
Expand Down Expand Up @@ -64,34 +65,35 @@ export type CoverageItem = {
lines: { [key: string]: number };
};

function getTestRunId() {
const testRunIdFile = path.join(apexDirPath, 'test-run-id.txt');
function getTestRunId(): string {
const testRunIdFile = join(apexDirPath, 'test-run-id.txt');
if (!fs.existsSync(testRunIdFile)) {
throw new Error(nls.localize('colorizer_no_code_coverage_on_project'));
}
return fs.readFileSync(testRunIdFile, 'utf8');
}

function getCoverageData() {
function getCoverageData(): CoverageItem[] | CodeCoverageResult[] {
const testRunId = getTestRunId();
const testResultFilePath = path.join(
apexDirPath,
`test-result-${testRunId}.json`
);
const testResultFilePath = join(apexDirPath, `test-result-${testRunId}.json`);

if (!fs.existsSync(testResultFilePath)) {
throw new Error(
nls.localize('colorizer_no_code_coverage_on_test_results', testRunId)
);
}
const testResultOutput = fs.readFileSync(testResultFilePath, 'utf8');
const codeCoverage = JSON.parse(testResultOutput) as CoverageTestResult;
if (codeCoverage.coverage === undefined) {
const testResult = JSON.parse(testResultOutput);
if (
testResult.coverage === undefined &&
testResult.codecoverage === undefined
) {
throw new Error(
nls.localize('colorizer_no_code_coverage_on_test_results', testRunId)
);
}
return codeCoverage.coverage ? codeCoverage.coverage.coverage : '';

return testResult.codecoverage || testResult.coverage.coverage;
}

function isApexMetadata(filePath: string): boolean {
Expand All @@ -101,8 +103,7 @@ function isApexMetadata(filePath: string): boolean {
function getApexMemberName(filePath: string): string {
if (isApexMetadata(filePath)) {
const filePathWithOutType = filePath.replace(/.cls|.trigger/g, '');
const separator = process.platform === 'win32' ? '\\' : '/';
const indexOfLastFolder = filePathWithOutType.lastIndexOf(separator);
const indexOfLastFolder = filePathWithOutType.lastIndexOf(sep);
return filePathWithOutType.substring(indexOfLastFolder + 1);
}
return '';
Expand Down Expand Up @@ -151,10 +152,10 @@ export class CodeCoverage {
public colorizer(editor?: TextEditor) {
try {
if (editor && isApexMetadata(editor.document.uri.fsPath)) {
const codeCovArray = getCoverageData() as CoverageItem[];
const codeCovArray = getCoverageData() as Array<{ name: string }>;
const apexMemberName = getApexMemberName(editor.document.uri.fsPath);
const codeCovItem = codeCovArray.find(
covItem =>
covItem.name === getApexMemberName(editor.document.uri.fsPath)
covItem => covItem.name === apexMemberName
);

if (!codeCovItem) {
Expand All @@ -163,18 +164,32 @@ export class CodeCoverage {
);
}

for (const key in codeCovItem.lines) {
if (codeCovItem.lines.hasOwnProperty(key)) {
if (codeCovItem.lines[key] === 1) {
this.coveredLines.push(
getLineRange(editor.document, Number(key))
);
} else {
this.uncoveredLines.push(
getLineRange(editor.document, Number(key))
);
if (
codeCovItem.hasOwnProperty('lines') &&
!codeCovItem.hasOwnProperty('uncoveredLines')
) {
const covItem = codeCovItem as CoverageItem;
for (const key in covItem.lines) {
if (covItem.lines.hasOwnProperty(key)) {
if (covItem.lines[key] === 1) {
this.coveredLines.push(
getLineRange(editor.document, Number(key))
);
} else {
this.uncoveredLines.push(
getLineRange(editor.document, Number(key))
);
}
}
}
} else {
const covResult = codeCovItem as CodeCoverageResult;
this.coveredLines = covResult.coveredLines.map(cov =>
getLineRange(editor.document, Number(cov))
);
this.uncoveredLines = covResult.uncoveredLines.map(uncov =>
getLineRange(editor.document, Number(uncov))
);
}

editor.setDecorations(coveredLinesDecorationType, this.coveredLines);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import {
HumanReporter,
TestItem,
TestLevel,
TestService
} from '@salesforce/apex-node';
import {
Command,
SfdxCommandBuilder,
TestRunner
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import { notificationService } from '@salesforce/salesforcedx-utils-vscode/out/src/commands';
import { workspaceContext } from '@salesforce/salesforcedx-utils-vscode/out/src/context';
import * as vscode from 'vscode';
import { nls } from '../messages';
import { forceApexTestRunCacheService, isEmpty } from '../testRunCache';
Expand All @@ -22,22 +29,81 @@ const sfdxCoreSettings = sfdxCoreExports.sfdxCoreSettings;
const SfdxCommandlet = sfdxCoreExports.SfdxCommandlet;
const SfdxWorkspaceChecker = sfdxCoreExports.SfdxWorkspaceChecker;
const SfdxCommandletExecutor = sfdxCoreExports.SfdxCommandletExecutor;
const notificationService = sfdxCoreExports.notificationService;
const LibraryCommandletExecutor = sfdxCoreExports.LibraryCommandletExecutor;
const channelService = sfdxCoreExports.channelService;

export class ApexLibraryTestRunExecutor extends LibraryCommandletExecutor<{
outputDir: string;
tests: string[];
codeCoverage: boolean;
}> {
private tests: string[];
private codeCoverage: boolean = false;
private outputDir: string;
protected executionName = nls.localize('apex_test_run_text');
protected logName = 'force_apex_execute_library';

public static diagnostics = vscode.languages.createDiagnosticCollection(
'apex-errors'
);

constructor(tests: string[], outputDir: string, codeCoverage: boolean) {
super();
this.tests = tests;
this.outputDir = outputDir;
this.codeCoverage = codeCoverage;
}

private buildTestItem(testNames: string[]): TestItem[] {
const tItems = testNames.map(item => {
if (item.indexOf('.') > 0) {
const splitItemData = item.split('.');
return {
className: splitItemData[0],
testMethods: [splitItemData[1]]
} as TestItem;
}

return { className: item } as TestItem;
});
return tItems;
}

protected async run(): Promise<boolean> {
const connection = await workspaceContext.getConnection();
const testService = new TestService(connection);
const result = await testService.runTestAsynchronous(
{
tests: this.buildTestItem(this.tests),
testLevel: TestLevel.RunSpecifiedTests
},
this.codeCoverage
);
await testService.writeResultFiles(
result,
{ resultFormat: 'json', dirPath: this.outputDir },
this.codeCoverage
);
const humanOutput = new HumanReporter().format(result, this.codeCoverage);
channelService.appendLine(humanOutput);
return true;
}
}

// build force:apex:test:run w/ given test class or test method
export class ForceApexTestRunCodeActionExecutor extends SfdxCommandletExecutor<{}> {
protected test: string;
protected tests: string;
protected shouldGetCodeCoverage: boolean = false;
protected builder: SfdxCommandBuilder = new SfdxCommandBuilder();
private outputToJson: string;

public constructor(
test: string,
tests: string[],
shouldGetCodeCoverage: boolean,
outputToJson: string
) {
super();
this.test = test || '';
this.tests = tests.join(',') || '';
this.shouldGetCodeCoverage = shouldGetCodeCoverage;
this.outputToJson = outputToJson;
}
Expand All @@ -48,7 +114,7 @@ export class ForceApexTestRunCodeActionExecutor extends SfdxCommandletExecutor<{
nls.localize('force_apex_test_run_codeAction_description_text')
)
.withArg('force:apex:test:run')
.withFlag('--tests', this.test)
.withFlag('--tests', this.tests)
.withFlag('--resultformat', 'human')
.withFlag('--outputdir', this.outputToJson)
.withFlag('--loglevel', 'error')
Expand All @@ -62,13 +128,20 @@ export class ForceApexTestRunCodeActionExecutor extends SfdxCommandletExecutor<{
}
}

async function forceApexTestRunCodeAction(test: string) {
const getCodeCoverage = sfdxCoreSettings.getRetrieveTestCodeCoverage();
async function forceApexTestRunCodeAction(tests: string[]) {
const outputToJson = getTempFolder();
const getCodeCoverage = sfdxCoreSettings.getRetrieveTestCodeCoverage();
const testRunExecutor = sfdxCoreSettings.getApexLibrary()
? new ApexLibraryTestRunExecutor(tests, outputToJson, getCodeCoverage)
: new ForceApexTestRunCodeActionExecutor(
tests,
getCodeCoverage,
outputToJson
);
const commandlet = new SfdxCommandlet(
new SfdxWorkspaceChecker(),
new EmptyParametersGatherer(),
new ForceApexTestRunCodeActionExecutor(test, getCodeCoverage, outputToJson)
testRunExecutor
);
await commandlet.run();
}
Expand Down Expand Up @@ -122,7 +195,7 @@ export async function forceApexTestClassRunCodeAction(testClass: string) {
return;
}

await forceApexTestRunCodeAction(testClass);
await forceApexTestRunCodeAction([testClass]);
}

// T E S T M E T H O D
Expand Down Expand Up @@ -163,5 +236,5 @@ export async function forceApexTestMethodRunCodeAction(testMethod: string) {
return;
}

await forceApexTestRunCodeAction(testMethod);
await forceApexTestRunCodeAction([testMethod]);
}
1 change: 1 addition & 0 deletions packages/salesforcedx-vscode-apex/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

export {
ApexLibraryTestRunExecutor,
forceApexTestClassRunCodeAction,
forceApexTestClassRunCodeActionDelegate,
forceApexTestMethodRunCodeAction,
Expand Down
23 changes: 0 additions & 23 deletions packages/salesforcedx-vscode-apex/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import { TestRunner } from '@salesforce/salesforcedx-utils-vscode/out/src/cli';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { LanguageClient } from 'vscode-languageclient/lib/main';
Expand Down Expand Up @@ -262,28 +261,6 @@ async function registerTestView(
return vscode.Disposable.from(...testViewItems);
}

export async function getApexClassFiles(): Promise<vscode.Uri[]> {
const jsonProject = (await vscode.workspace.findFiles(
'**/sfdx-project.json',
'**/node_modules/**'
))[0];
const innerText = fs.readFileSync(jsonProject.path);
const jsonObject = JSON.parse(innerText.toString());
const packageDirectories =
jsonObject.packageDirectories || jsonObject.PackageDirectories;
const allClasses = new Array<vscode.Uri>();
for (const packageDirectory of packageDirectories) {
const pattern = path.join(packageDirectory.path, '**/*.cls');
const apexClassFiles = await vscode.workspace.findFiles(
pattern,
'**/node_modules/**'
);
allClasses.push(...apexClassFiles);
}
return allClasses;
}

// tslint:disable-next-line:no-empty
export function deactivate() {
telemetryService.sendExtensionDeactivationEvent();
}
1 change: 1 addition & 0 deletions packages/salesforcedx-vscode-apex/src/messages/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const messages = {
java_runtime_missing_text:
'Java runtime could not be located. Set one using the salesforcedx-vscode-apex.java.home VS Code setting. For more information, go to [Set Your Java Version](%s).',
force_sobjects_refresh: 'SFDX: Refresh SObject Definitions',
apex_test_run_text: 'Run Apex Tests',
force_apex_test_run_codeAction_description_text: 'Run Apex test(s)',
force_apex_test_run_codeAction_no_class_test_param_text:
'Test class not provided. Run the code action on a class annotated with @isTest.',
Expand Down
Loading

0 comments on commit ff73bb1

Please sign in to comment.