Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: run apex tests using the library #2828

Merged
merged 10 commits into from
Jan 6, 2021
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;
Copy link
Collaborator Author

@AnanyaJha AnanyaJha Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the LibraryCommandletExecutor currently lives in the core package, it uses the default Salesforce CLI channel from the channel services. If we switch to using the new channelService we have in the utils package right now, the experience is wonky because the Starting command.../Ending command... text appears in the default Salesforce CLI channel, while the output from the test run appears in the new channel that we've created. And since the command palette portion of the test:run functionality still lives in the core package as well, all the output from that command is displayed via the Salesforce CLI channel too

In a follow up PR, I think we should migrate the LibraryCommandletExecutor to the utils package, the Apex commands (test:run command) to the apex extension, and then switch this command to use the new channelService so it doesn't cause customers any confusion on which channel they should be looking at for apex test results

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is part of the work we'll be doing in January to address some perf issues in the Apex extension.


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<{}> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reworked this executor a bit so that it could be used for the sidebar as well

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