Skip to content

Commit

Permalink
Visual display of apex code coverage (#1145)
Browse files Browse the repository at this point in the history
@W-5951754@
  • Loading branch information
lcampos authored Mar 16, 2019
1 parent cdedef7 commit be06b64
Show file tree
Hide file tree
Showing 9 changed files with 505 additions and 1 deletion.
191 changes: 191 additions & 0 deletions packages/salesforcedx-vscode-apex/src/codecoverage/colorizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* 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 * as fs from 'fs';
import * as path from 'path';
import {
Range,
TextDocument,
TextEditor,
TextLine,
window,
workspace
} from 'vscode';
import { nls } from '../messages';
import {
coveredLinesDecorationType,
uncoveredLinesDecorationType
} from './decorations';
import { StatusBarToggle } from './statusBarToggle';

const apexDirPath = path.join(
workspace!.workspaceFolders![0].uri.fsPath,
'.sfdx',
'tools',
'testresults',
'apex'
);

export function getLineRange(
document: TextDocument,
lineNumber: number
): Range {
let adjustedLineNumber: number;
let firstLine: TextLine;
try {
adjustedLineNumber = lineNumber - 1;
firstLine = document.lineAt(adjustedLineNumber);
} catch (e) {
throw new Error(nls.localize('colorizer_out_of_sync_code_coverage_data'));
}

return new Range(
adjustedLineNumber,
firstLine.range.start.character,
adjustedLineNumber,
firstLine.range.end.character
);
}

export type CoverageTestResult = {
coverage: {
coverage: CoverageItem[];
};
};

export type CoverageItem = {
id: string;
name: string;
totalLines: number;
lines: { [key: string]: number };
};

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

function getCoverageData() {
const testRunId = getTestRunId();
const testResultFilePath = path.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) {
throw new Error(
nls.localize('colorizer_no_code_coverage_on_test_results', testRunId)
);
}
return codeCoverage.coverage ? codeCoverage.coverage.coverage : '';
}

function isApexMetadata(filePath: string): boolean {
return filePath.endsWith('.cls') || filePath.endsWith('.trigger');
}

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);
return filePathWithOutType.substring(indexOfLastFolder + 1);
}
return '';
}

export class CodeCoverage {
private statusBar: StatusBarToggle;
public coveredLines: Range[];
public uncoveredLines: Range[];

constructor(statusBar: StatusBarToggle) {
this.statusBar = statusBar;
this.coveredLines = Array<Range>();
this.uncoveredLines = Array<Range>();

window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this);
this.onDidChangeActiveTextEditor(window.activeTextEditor);
}

public onDidChangeActiveTextEditor(editor?: TextEditor) {
if (editor && this.statusBar.isHighlightingEnabled) {
this.colorizer(editor);
}
}

public toggleCoverage() {
if (this.statusBar.isHighlightingEnabled) {
this.statusBar.toggle(false);
this.coveredLines = [];
this.uncoveredLines = [];

const editor = window.activeTextEditor;
if (editor) {
editor.setDecorations(coveredLinesDecorationType, this.coveredLines);
editor.setDecorations(
uncoveredLinesDecorationType,
this.uncoveredLines
);
}
} else {
this.colorizer(window.activeTextEditor);
this.statusBar.toggle(true);
}
}

public colorizer(editor?: TextEditor) {
try {
if (editor && isApexMetadata(editor.document.uri.fsPath)) {
const codeCovArray = getCoverageData() as CoverageItem[];
const codeCovItem = codeCovArray.find(
covItem =>
covItem.name === getApexMemberName(editor.document.uri.fsPath)
);

if (!codeCovItem) {
throw new Error(
nls.localize('colorizer_no_code_coverage_current_file')
);
}

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))
);
}
}
}

editor.setDecorations(coveredLinesDecorationType, this.coveredLines);
editor.setDecorations(
uncoveredLinesDecorationType,
this.uncoveredLines
);
}
} catch (e) {
// telemetry
window.showWarningMessage(e.message);
}
}
}
27 changes: 27 additions & 0 deletions packages/salesforcedx-vscode-apex/src/codecoverage/decorations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* 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 { window } from 'vscode';

const lime = (opacity: number): string => `rgba(45, 121, 11, ${opacity})`;
const red = (opacity: number): string => `rgba(253, 72, 73, ${opacity})`;

export const coveredLinesDecorationType = window.createTextEditorDecorationType(
{
backgroundColor: lime(0.5),
borderRadius: '.2em',
overviewRulerColor: lime(0.5)
}
);

export const uncoveredLinesDecorationType = window.createTextEditorDecorationType(
{
backgroundColor: red(0.5),
borderRadius: '.2em',
overviewRulerColor: red(0.5)
}
);
9 changes: 9 additions & 0 deletions packages/salesforcedx-vscode-apex/src/codecoverage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* 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
*/

export { CodeCoverage } from './colorizer';
export { StatusBarToggle } from './statusBarToggle';
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* 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 { Disposable, StatusBarItem, window } from 'vscode';
import { nls } from '../messages';

export class StatusBarToggle implements Disposable {
private static readonly toggleCodeCovCommand =
'sfdx.force.apex.toggle.colorizer';
private static readonly showIcon = '$(tasklist)';
private static readonly hideIcon = '$(three-bars)';
private static readonly toolTip = nls.localize(
'colorizer_statusbar_hover_text'
);
private isEnabled: boolean;
private statusBarItem: StatusBarItem;

constructor() {
this.statusBarItem = window.createStatusBarItem();
this.statusBarItem.command = StatusBarToggle.toggleCodeCovCommand;
this.statusBarItem.text = StatusBarToggle.showIcon;
this.statusBarItem.tooltip = StatusBarToggle.toolTip;
this.statusBarItem.show();
this.isEnabled = false;
}

public get isHighlightingEnabled(): boolean {
return this.isEnabled;
}

public toggle(active: boolean) {
if (active) {
this.statusBarItem.text = StatusBarToggle.hideIcon;
this.isEnabled = true;
} else {
this.statusBarItem.text = StatusBarToggle.showIcon;
this.isEnabled = false;
}
}

public dispose() {
this.statusBarItem.dispose();
}
}
10 changes: 10 additions & 0 deletions packages/salesforcedx-vscode-apex/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { LanguageClient } from 'vscode-languageclient/lib/main';
import { CodeCoverage, StatusBarToggle } from './codecoverage';
import {
forceApexTestClassRunCodeAction,
forceApexTestClassRunCodeActionDelegate,
Expand Down Expand Up @@ -109,6 +110,14 @@ export async function activate(context: vscode.ExtensionContext) {
function registerCommands(
extensionContext: vscode.ExtensionContext
): vscode.Disposable {
// Colorize code coverage
const statusBarToggle = new StatusBarToggle();
const colorizer = new CodeCoverage(statusBarToggle);
const forceApexToggleColorizerCmd = vscode.commands.registerCommand(
'sfdx.force.apex.toggle.colorizer',
() => colorizer.toggleCoverage()
);

// Customer-facing commands
const forceApexTestClassRunDelegateCmd = vscode.commands.registerCommand(
'sfdx.force.apex.test.class.run.delegate',
Expand All @@ -135,6 +144,7 @@ function registerCommands(
forceApexTestMethodRunCodeAction
);
return vscode.Disposable.from(
forceApexToggleColorizerCmd,
forceApexTestLastClassRunCmd,
forceApexTestClassRunCmd,
forceApexTestClassRunDelegateCmd,
Expand Down
11 changes: 10 additions & 1 deletion packages/salesforcedx-vscode-apex/src/messages/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,14 @@ export const messages = {

client_name: 'Apex Language Server',
cannot_determine_workspace:
'Unable to determine workspace folders for workspace'
'Unable to determine workspace folders for workspace',
colorizer_no_code_coverage_on_project:
'No test run information was found for this project. Set "salesforcedx-vscode-core.retrieve-test-code-coverage": true in your user or workspace settings, then run Apex tests from the Apex Tests sidebar or using the Run Tests or Run All Tests code lens within a test class file.',
colorizer_no_code_coverage_on_test_results:
'No code coverage information was found for test run %s. Set "salesforcedx-vscode-core.retrieve-test-code-coverage": true in your user or workspace settings, then run Apex tests from the Apex Tests sidebar or using the Run Tests or Run All Tests code lens within a test class file.',
colorizer_out_of_sync_code_coverage_data:
'It looks like this file has been updated, please re-run your Apex test to update code coverage numbers',
colorizer_no_code_coverage_current_file:
'No code coverage information was found for this file. Set "salesforcedx-vscode-core.retrieve-test-code-coverage": true in your user or workspace settings. Then, run Apex tests that include methods in this file from the Apex Tests sidebar or using the Run Tests or Run All Tests code lens within the file.',
colorizer_statusbar_hover_text: 'Highlight Apex Code Coverage'
};
Loading

0 comments on commit be06b64

Please sign in to comment.