Skip to content

Commit

Permalink
Merge pull request #130 from forcedotcom/jj/W-15640497
Browse files Browse the repository at this point in the history
NEW (Extension) @W-15640497@ Delta runs on SFGE - Phase 1
  • Loading branch information
jag-j authored Sep 20, 2024
2 parents b2cf982 + cccc728 commit 6ed70e6
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 36 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"mocha": "^10.1.0",
"nyc": "^15.1.0",
"ovsx": "^0.8.3",
"proxyquire": "^2.1.3",
"sinon": "^15.1.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.3"
Expand Down
35 changes: 35 additions & 0 deletions src/deltarun/delta-run-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as fs from 'fs';

export function getDeltaRunTarget(sfgecachepath:string, savedFilesCache:Set<string>): string[] {
// Read and parse the JSON file at sfgecachepath
const fileContent = fs.readFileSync(sfgecachepath, 'utf-8');
const parsedData = JSON.parse(fileContent) as CacheData;

const matchingEntries: string[] = [];

// Iterate over each file entry in the data
parsedData.data.forEach((entry: { filename: string, entries: string[] }) => {
// Check if the filename is in the savedFilesCache
if (savedFilesCache.has(entry.filename)) {
// If it matches, add the individual entries to the result array
matchingEntries.push(...entry.entries);
}
});

return matchingEntries;
}

interface CacheEntry {
filename: string;
entries: string[];
}

interface CacheData {
data: CacheEntry[];
}
111 changes: 79 additions & 32 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { CoreExtensionService, TelemetryService } from './lib/core-extension-ser
import * as Constants from './lib/constants';
import * as path from 'path';
import { SIGKILL } from 'constants';
import * as ApexGuruFunctions from './apexguru/apex-guru-service'
import * as ApexGuruFunctions from './apexguru/apex-guru-service';
import * as DeltaRunFunctions from './deltarun/delta-run-service';
import * as os from 'os';
import * as fs from 'fs';

Expand All @@ -42,6 +43,9 @@ let outputChannel: vscode.LogOutputChannel;

let sfgeCachePath: string = null;

// Create a Set to store saved file paths
const savedFilesCache: Set<string> = new Set();

/**
* This method is invoked when the extension is first activated (this is currently configured to be when a sfdx project is loaded).
* The activation trigger can be changed by changing activationEvents in package.json
Expand Down Expand Up @@ -113,36 +117,15 @@ export async function activate(context: vscode.ExtensionContext): Promise<vscode

const runDfaOnSelectedMethodCmd = vscode.commands.registerCommand(Constants.COMMAND_RUN_DFA_ON_SELECTED_METHOD, async () => {
if (await _shouldProceedWithDfaRun(context)) {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: messages.graphEngine.spinnerText,
cancellable: true
}, async (progress, token) => {
token.onCancellationRequested(async () => {
await _stopExistingDfaRun(context);
});
customCancellationToken = new vscode.CancellationTokenSource();
customCancellationToken.token.onCancellationRequested(async () => {
customCancellationToken?.dispose();
customCancellationToken = null;
await vscode.window.showInformationMessage(messages.graphEngine.noViolationsFound);
return;
});
const methodLevelTarget: string = await targeting.getSelectedMethod();
// Pull out the file from the target and use it to identify the project directory.
const currentFile: string = methodLevelTarget.substring(0, methodLevelTarget.lastIndexOf('#'));
const projectDir: string = targeting.getProjectDir(currentFile);

return _runAndDisplayDfa(context, {
commandName: Constants.COMMAND_RUN_DFA_ON_SELECTED_METHOD
}, customCancellationToken, methodLevelTarget, projectDir);
});
const methodLevelTarget: string[] = [await targeting.getSelectedMethod()];
await runMethodLevelDfa(context, methodLevelTarget);
}
});

sfgeCachePath = path.join(createTempDirectory(), 'sfca-graph-engine-cache.json');
const runDfaOnWorkspaceCmd = vscode.commands.registerCommand(Constants.COMMAND_RUN_DFA, async () => {
await _runDfa(context);
savedFilesCache.clear();
});
context.subscriptions.push(runOnActiveFile, runOnSelected, runDfaOnSelectedMethodCmd, runDfaOnWorkspaceCmd, removeDiagnosticsOnActiveFile, removeDiagnosticsOnSelectedFile, removeDiagnosticsInRange);

Expand All @@ -166,12 +149,44 @@ export async function activate(context: vscode.ExtensionContext): Promise<vscode
});
context.subscriptions.push(runApexGuruOnSelectedFile, runApexGuruOnCurrentFile);
}

const documentSaveListener = vscode.workspace.onDidSaveTextDocument(document => {
const filePath = document.uri.fsPath;
savedFilesCache.add(filePath);
});
context.subscriptions.push(documentSaveListener);

TelemetryService.sendExtensionActivationEvent(extensionHrStart);
outputChannel.appendLine(`Extension sfdx-code-analyzer-vscode activated.`);
return Promise.resolve(context);
}

async function runMethodLevelDfa(context: vscode.ExtensionContext, methodLevelTarget: string[]) {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: messages.graphEngine.spinnerText,
cancellable: true
}, async (progress, token) => {
token.onCancellationRequested(async () => {
await _stopExistingDfaRun(context);
});
customCancellationToken = new vscode.CancellationTokenSource();
customCancellationToken.token.onCancellationRequested(async () => {
customCancellationToken?.dispose();
customCancellationToken = null;
await vscode.window.showInformationMessage(messages.graphEngine.noViolationsFound);
return;
});
// Pull out the file from the target and use it to identify the project directory.
const currentFile: string = methodLevelTarget[0].substring(0, methodLevelTarget.lastIndexOf('#'));
const projectDir: string = targeting.getProjectDir(currentFile);

return _runAndDisplayDfa(context, {
commandName: Constants.COMMAND_RUN_DFA_ON_SELECTED_METHOD
}, customCancellationToken, methodLevelTarget, projectDir);
});
}

export function createTempDirectory(): string {
const tempFolderPrefix = path.join(os.tmpdir(), Constants.EXTENSION_PACK_ID);
try {
Expand All @@ -194,10 +209,14 @@ async function _runDfa(context: vscode.ExtensionContext) {
);

// Default to "Yes" if no choice is made
const rerunFailedOnly = choice == '***Yes***';
if (rerunFailedOnly) {
// Do nothing for now. This will be implemented as part of W-15639759
return;
const rerunChangedOnly = choice == '***Yes***';
if (rerunChangedOnly) {
const deltaRunTargets = DeltaRunFunctions.getDeltaRunTarget(sfgeCachePath, savedFilesCache);
if (deltaRunTargets.length == 0) {
void vscode.window.showInformationMessage('***No local changes found that would change the outcome of previous SFGE full run.***');
return
}
await runDfaOnSelectMethods(context, deltaRunTargets);
} else {
void vscode.window.showWarningMessage('***A full run of the graph engine will happen in the background. You can cancel this by clicking on the status progress.***');
await runDfaOnWorkspace(context);
Expand All @@ -207,6 +226,31 @@ async function _runDfa(context: vscode.ExtensionContext) {
}
}

async function runDfaOnSelectMethods(context: vscode.ExtensionContext, selectedMethods: string[]) {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: messages.graphEngine.spinnerText,
cancellable: true
}, async (progress, token) => {
token.onCancellationRequested(async () => {
await _stopExistingDfaRun(context);
});
customCancellationToken = new vscode.CancellationTokenSource();
customCancellationToken.token.onCancellationRequested(async () => {
customCancellationToken?.dispose();
customCancellationToken = null;
await vscode.window.showInformationMessage(messages.graphEngine.noViolationsFound);
return;
});

// We only have one project loaded on VSCode at once. So, projectDir should have only one entry and we use
// the root directory of that project as the projectDir argument to run DFA.
return _runAndDisplayDfa(context, {
commandName: Constants.COMMAND_RUN_DFA
}, customCancellationToken, selectedMethods, targeting.getProjectDir(), sfgeCachePath);
});
}

async function runDfaOnWorkspace(context: vscode.ExtensionContext) {
await vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
Expand All @@ -228,7 +272,7 @@ async function runDfaOnWorkspace(context: vscode.ExtensionContext) {
// the root directory of that project as the projectDir argument to run DFA.
return _runAndDisplayDfa(context, {
commandName: Constants.COMMAND_RUN_DFA
}, customCancellationToken, null, targeting.getProjectDir());
}, customCancellationToken, null, targeting.getProjectDir(), sfgeCachePath);
});
}

Expand Down Expand Up @@ -371,14 +415,16 @@ export async function _runAndDisplayPathless(selections: vscode.Uri[], runInfo:
* @param runInfo A collection of services and information used to properly run the command
* @param runInfo.commandName The specific command being run
*/
export async function _runAndDisplayDfa(context:vscode.ExtensionContext ,runInfo: RunInfo, cancelToken: vscode.CancellationTokenSource, methodLevelTarget: string, projectDir: string): Promise<void> {
export async function _runAndDisplayDfa(context:vscode.ExtensionContext ,runInfo: RunInfo,
cancelToken: vscode.CancellationTokenSource, methodLevelTarget: string[], projectDir: string,
cacheFilePath?: string): Promise<void> {
const {
commandName
} = runInfo;
const startTime = Date.now();
try {
await verifyPluginInstallation();
const results = await new ScanRunner().runDfa([methodLevelTarget], projectDir, context, sfgeCachePath);
const results = await new ScanRunner().runDfa(methodLevelTarget, projectDir, context, cacheFilePath);
if (results.length > 0) {
const panel = vscode.window.createWebviewPanel(
'dfaResults',
Expand Down Expand Up @@ -479,6 +525,7 @@ async function summarizeResultsAsToast(targets: string[], results: RuleResult[])
// This method is called when your extension is deactivated
export function deactivate() {
TelemetryService.dispose();
savedFilesCache.clear();
}

export function _isValidFileForAnalysis(documentUri: vscode.Uri) {
Expand Down
98 changes: 98 additions & 0 deletions src/test/suite/deltarun/delta-run-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import {expect} from 'chai';
import Sinon = require('sinon');
import proxyquire from 'proxyquire';

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';

suite('Delta Run Test Suite', () => {
suite('#getDeltaRunTarget', () => {
let readFileSyncStub: Sinon.SinonStub;
let getDeltaRunTarget: Function;

// Set up stubs and mock the fs module
setup(() => {
readFileSyncStub = Sinon.stub();

// Load the module with the mocked fs dependency
const mockedModule = proxyquire('../../../deltarun/delta-run-service', {
fs: {
readFileSync: readFileSyncStub
}
});

// Get the function from the module
getDeltaRunTarget = mockedModule.getDeltaRunTarget;
});

teardown(() => {
Sinon.restore();
});

test('Returns matching entries when files in cache match JSON data', () => {
// Setup the mock return value for readFileSync
const sfgecachepath = '/path/to/sfgecache.json';
const savedFilesCache = new Set<string>([
'/some/user/path/HelloWorld.cls'
]);

const jsonData = `{
"data": [
{
"entries": ["/some/user/path/HelloWorld.cls#getProducts", "/some/user/path/HelloWorld.cls#getSimilarProducts"],
"filename": "/some/user/path/HelloWorld.cls"
}
]
}`;

readFileSyncStub.withArgs(sfgecachepath, 'utf-8').returns(jsonData);

// Test
const result = getDeltaRunTarget(sfgecachepath, savedFilesCache);

// Assertions
expect(result).to.deep.equal([
'/some/user/path/HelloWorld.cls#getProducts',
'/some/user/path/HelloWorld.cls#getSimilarProducts'
]);

Sinon.assert.calledOnce(readFileSyncStub);
});

test('Returns an empty array when no matching files are found in cache', () => {
// ===== SETUP =====
const sfgecachepath = '/path/to/sfgecache.json';
const savedFilesCache = new Set<string>([
'/some/user/path/HelloWorld.cls'
]);

const jsonData = `{
"data": [
{
"filename": "/some/user/path/NotHelloWorld.cls",
"entries": ["/some/user/path/NotHelloWorld.cls#getProducts"]
}
]
}`;

// Stub the file read to return the JSON data
readFileSyncStub.withArgs(sfgecachepath, 'utf-8').returns(jsonData);

// ===== TEST =====
const result = getDeltaRunTarget(sfgecachepath, savedFilesCache);

// ===== ASSERTIONS =====
expect(result).to.deep.equal([]);

Sinon.assert.calledOnce(readFileSyncStub);
});
});
});
6 changes: 3 additions & 3 deletions src/test/suite/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {SfCli} from '../../lib/sf-cli';
import Sinon = require('sinon');
import { _runAndDisplayPathless, _runAndDisplayDfa, _clearDiagnostics, _shouldProceedWithDfaRun, _stopExistingDfaRun, _isValidFileForAnalysis, verifyPluginInstallation, _clearDiagnosticsForSelectedFiles, _removeDiagnosticsInRange, RunInfo } from '../../extension';
import {messages} from '../../lib/messages';
import {CoreExtensionService, TelemetryService} from '../../lib/core-extension-service';
import {TelemetryService} from '../../lib/core-extension-service';
import * as Constants from '../../lib/constants';
import * as targeting from '../../lib/targeting';

Expand Down Expand Up @@ -238,7 +238,7 @@ suite('Extension Test Suite', () => {
// Attempt to run the appropriate extension command.
await _runAndDisplayDfa(null, {
commandName: fakeTelemetryName
}, null, 'someMethod', 'some/project/dir');
}, null, ['someMethod'], 'some/project/dir');

// ===== ASSERTIONS =====
Sinon.assert.callCount(errorSpy, 1);
Expand All @@ -263,7 +263,7 @@ suite('Extension Test Suite', () => {
try {
await _runAndDisplayDfa(null, {
commandName: fakeTelemetryName
}, null, 'someMethod', 'some/project/dir');
}, null, ['someMethod'], 'some/project/dir');
} catch (e) {
err = e;
}
Expand Down
Loading

0 comments on commit 6ed70e6

Please sign in to comment.