Skip to content

Commit

Permalink
NEW @W-16360876@ FlowTest supports describeRules method
Browse files Browse the repository at this point in the history
  • Loading branch information
jfeingold35 committed Sep 25, 2024
1 parent edd3295 commit 5ae8f6f
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 45 deletions.
13 changes: 8 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions packages/code-analyzer-flowtest-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@
"@salesforce/code-analyzer-engine-api": "0.10.0",
"@types/node": "^20.0.0",
"@types/semver": "^7.5.8",
"@types/which": "^3.0.4",
"semver": "^7.6.3",
"which": "^4.0.0"
"semver": "^7.6.3"
},
"devDependencies": {
"@eslint/js": "^8.57.0",
"@types/jest": "^29.0.0",
"@types/which": "^3.0.4",
"eslint": "^8.57.0",
"jest": "^29.0.0",
"rimraf": "*",
"ts-jest": "^29.0.0",
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0"
"typescript-eslint": "^7.8.0",
"which": "^4.0.0"
},
"engines": {
"node": ">=20.0.0"
Expand Down
61 changes: 49 additions & 12 deletions packages/code-analyzer-flowtest-engine/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ import {
DescribeOptions,
Engine,
EngineRunResults,
LogLevel,
RuleDescription,
RunOptions
RuleType,
RunOptions,
SeverityLevel
} from "@salesforce/code-analyzer-engine-api";
import {FlowTestConfig} from "./config";
import {FlowTestCommandWrapper, FlowTestRuleDescriptor} from "./python/FlowTestCommandWrapper";

export class FlowTestEngine extends Engine {
public static readonly NAME: string = 'flowtest';
private readonly config: FlowTestConfig;
private readonly commandWrapper: FlowTestCommandWrapper;

public constructor(config: FlowTestConfig) {
public constructor(commandWrapper: FlowTestCommandWrapper) {
super();
this.config = config;
this.commandWrapper = commandWrapper;
}

public getName(): string {
Expand All @@ -23,24 +24,60 @@ export class FlowTestEngine extends Engine {

public async describeRules(_describeOptions: DescribeOptions): Promise<RuleDescription[]> {
this.emitDescribeRulesProgressEvent(0);
const pythonCommand: string = this.config.python_command;
this.emitDescribeRulesProgressEvent(10);
this.emitLogEvent(LogLevel.Info, `Temporary message: Python command identified as ${pythonCommand}`);
const flowTestRules: FlowTestRuleDescriptor[] = await this.commandWrapper.getFlowTestRuleDescriptions();
this.emitDescribeRulesProgressEvent(75);
const convertedRules = this.convertFlowTestRulesToCodeAnalyzerRules(flowTestRules);
this.emitRunRulesProgressEvent(100);
return [];
return convertedRules;
}

public async runRules(_ruleNames: string[], _runOptions: RunOptions): Promise<EngineRunResults> {
this.emitRunRulesProgressEvent(0);
const pythonCommand = this.config.python_command;

this.emitRunRulesProgressEvent(10);
this.emitLogEvent(LogLevel.Info, `Temporary message: Python command identified as ${pythonCommand}`);
this.emitRunRulesProgressEvent(100);
return {
violations: []
};
}

private convertFlowTestRulesToCodeAnalyzerRules(flowTestRules: FlowTestRuleDescriptor[]): RuleDescription[] {
return flowTestRules.map(flowTestRule => {
return {
// The name maps directly over.
name: flowTestRule.query_name,
severityLevel: this.convertFlowTestSeverityToCodeAnalyzerSeverity(flowTestRule.severity),
// All rules in FlowTest are obviously Flow-type.
type: RuleType.Flow,
// All rules are Recommended, but not all rules are Security rules.
tags: flowTestRule.is_security.toLowerCase() === 'true' ? ['Recommended', 'Security'] : ['Recommended'],
// The description maps directly over.
description: flowTestRule.query_description,
resourceUrls: this.convertHelpUrlToResourceUrls(flowTestRule.help_url)
}
});
}

private convertFlowTestSeverityToCodeAnalyzerSeverity(flowTestSeverity: string): SeverityLevel {
switch (flowTestSeverity) {
case 'Flow_High_Severity':
return SeverityLevel.High;
case 'Flow_Moderate_Severity':
return SeverityLevel.Moderate;
case 'Flow_Low_Severity':
return SeverityLevel.Low
}
throw new Error(`Developer error: invalid severity level ${flowTestSeverity}`);
}

private convertHelpUrlToResourceUrls(helpUrl: string): string[] {
// Treat the hardcoded string "none" as equivalent to an empty string.
if (helpUrl.toLowerCase() === 'none' || helpUrl === '') {
return [];
} else {
return [helpUrl];
}
}
}


4 changes: 3 additions & 1 deletion packages/code-analyzer-flowtest-engine/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {FlowTestEngine} from "./engine";
import {getMessage} from './messages';
import {FLOWTEST_ENGINE_CONFIG_DESCRIPTION, FlowTestConfig, validateAndNormalizeConfig} from "./config";
import {RunTimeFlowTestCommandWrapper} from "./python/FlowTestCommandWrapper";
import {PythonVersionIdentifier, RuntimePythonVersionIdentifier} from "./python/PythonVersionIdentifier";


Expand Down Expand Up @@ -35,7 +36,8 @@ export class FlowTestEnginePlugin extends EnginePluginV1 {

public async createEngine(engineName: string, resolvedConfig: ConfigObject): Promise<Engine> {
validateEngineName(engineName);
return new FlowTestEngine(resolvedConfig as FlowTestConfig);
const wrapper: RunTimeFlowTestCommandWrapper = new RunTimeFlowTestCommandWrapper((resolvedConfig as FlowTestConfig).python_command);
return new FlowTestEngine(wrapper);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import {PythonCommandExecutor} from './PythonCommandExecutor';


export interface FlowTestCommandWrapper {
getFlowTestHelpText(): Promise<string>;
getFlowTestRuleDescriptions(): Promise<FlowTestRuleDescriptor[]>;
}

export type FlowTestRuleDescriptor = {
query_id: string;
query_name: string;
severity: string;
query_description: string;
help_url: string;
query_version: string;
is_security: string;
}

const PATH_TO_PIPX_PYZ = path.join(__dirname, '..', '..', 'pipx.pyz');
Expand All @@ -16,7 +26,7 @@ export class RunTimeFlowTestCommandWrapper implements FlowTestCommandWrapper {
this.pythonCommandExecutor = new PythonCommandExecutor(pythonCommand);
}

public async getFlowTestHelpText(): Promise<string> {
public async getFlowTestRuleDescriptions(): Promise<FlowTestRuleDescriptor[]> {
const pythonArgs: string[] = [
PATH_TO_PIPX_PYZ,
'run',
Expand All @@ -25,14 +35,14 @@ export class RunTimeFlowTestCommandWrapper implements FlowTestCommandWrapper {
PATH_TO_FLOWTEST_ROOT,
'--',
'flowtest',
'-h'
'-p'
];

let stdout: string = '';
const processStdout = (stdoutMsg: string) => {
stdout += stdoutMsg;
}
await this.pythonCommandExecutor.exec(pythonArgs, processStdout);
return stdout;
return (JSON.parse(stdout) as FlowTestRuleDescriptor[]).sort((a, b) => a.query_name.localeCompare(b.query_name));
}
}
123 changes: 114 additions & 9 deletions packages/code-analyzer-flowtest-engine/test/engine.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,128 @@
import {Workspace} from "@salesforce/code-analyzer-engine-api";
import {RuleDescription, RuleType, SeverityLevel, Workspace} from "@salesforce/code-analyzer-engine-api";
import {FlowTestEngine} from "../src/engine";
import {FlowTestCommandWrapper, FlowTestRuleDescriptor} from "../src/python/FlowTestCommandWrapper";
import {changeWorkingDirectoryToPackageRoot} from "./test-helpers";

changeWorkingDirectoryToPackageRoot();

const WELL_FORMED_RULES: FlowTestRuleDescriptor[] = [{
query_id: 'FakeId1',
query_name: 'Fake_Flow_Rule_1',
severity: 'Flow_High_Severity',
query_description: 'Fake Description 1',
help_url: 'https://www.salesforce.com',
query_version: "0",
is_security: "True"
}, {
query_id: 'FakeId2',
query_name: 'Fake_Flow_Rule_2',
severity: 'Flow_Moderate_Severity',
query_description: 'Fake Description 2',
help_url: 'https://www.github.com/forcedotcom/code-analyzer-core',
query_version: "0",
is_security: "True"
}, {
query_id: 'FakeId3',
query_name: 'Fake_Flow_Rule_3',
severity: 'Flow_Low_Severity',
query_description: 'Fake Description 3',
help_url: 'None',
query_version: "0",
is_security: "False"
}, {
query_id: 'FakeId4',
query_name: 'Fake_Flow_Rule_4',
severity: 'Flow_Low_Severity',
query_description: 'Fake Description 4',
help_url: '',
query_version: "0",
is_security: "False"
}];

const MALFORMED_RULES: FlowTestRuleDescriptor[] = [{
query_id: 'FakeId1',
query_name: 'Fake_Flow_Rule_1',
severity: 'InvalidSeverityValue',
query_description: 'This Rule has an invalid Severity',
help_url: 'https://www.salesforce.com',
query_version: "0",
is_security: "True"
}];

describe('Tests for the FlowTestEngine', () => {
it('getName() returns correct name', () => {
const engine: FlowTestEngine = new FlowTestEngine({
python_command: 'pre-validated value'
});
const engine: FlowTestEngine = new FlowTestEngine(new StubCommandWrapper([]));
expect(engine.getName()).toEqual('flowtest');
});

describe('#describeRules()', () => {
it('Parses well-formed FlowTest rule descriptors into Code Analyzer rule descriptors', async () => {
// Construct the engine, injecting a Stub wrapper to replace the real one.
const engine: FlowTestEngine = new FlowTestEngine(new StubCommandWrapper(WELL_FORMED_RULES));

const ruleDescriptors: RuleDescription[] = await engine.describeRules({});

expect(ruleDescriptors).toHaveLength(4);
expect(ruleDescriptors[0]).toEqual({
name: 'Fake_Flow_Rule_1',
severityLevel: SeverityLevel.High,
type: RuleType.Flow,
tags: ['Recommended', 'Security'],
description: 'Fake Description 1',
resourceUrls: ['https://www.salesforce.com']
});
expect(ruleDescriptors[1]).toEqual({
name: 'Fake_Flow_Rule_2',
severityLevel: SeverityLevel.Moderate,
type: RuleType.Flow,
tags: ['Recommended', 'Security'],
description: 'Fake Description 2',
resourceUrls: ['https://www.github.com/forcedotcom/code-analyzer-core']
});
expect(ruleDescriptors[2]).toEqual({
name: 'Fake_Flow_Rule_3',
severityLevel: SeverityLevel.Low,
type: RuleType.Flow,
tags: ['Recommended'],
description: 'Fake Description 3',
resourceUrls: []
});
expect(ruleDescriptors[3]).toEqual({
name: 'Fake_Flow_Rule_4',
severityLevel: SeverityLevel.Low,
type: RuleType.Flow,
tags: ['Recommended'],
description: 'Fake Description 4',
resourceUrls: []
});
});

it.each([
{ruleIndex: 0, defect: 'invalid severity level'}
])('Throws coherent error for malformed FlowTest rule descriptors. Case: $defect', async ({ruleIndex, defect}) => {
// Construct the engine, injecting a Stub wrapper to replace the real one.
const engine: FlowTestEngine = new FlowTestEngine(new StubCommandWrapper([MALFORMED_RULES[ruleIndex]]));

// Expect the Describe call to fail with a message containing the defect description.
await expect(engine.describeRules({})).rejects.toThrow(defect);
});
});

it('TEMPORARY TEST FOR CODE COVERAGE', async () => {
// Will delete this test as soon as engine is implemented.
const engine: FlowTestEngine = new FlowTestEngine({
python_command: 'pre-validated value'
});
expect(await engine.describeRules({})).toEqual([]);
const engine: FlowTestEngine = new FlowTestEngine(new StubCommandWrapper([]));
expect(await engine.runRules([], {workspace: new Workspace([__dirname])})).toEqual({violations: []});
});
});
});

class StubCommandWrapper implements FlowTestCommandWrapper {
private readonly rules: FlowTestRuleDescriptor[];

public constructor(rules: FlowTestRuleDescriptor[]) {
this.rules = rules;
}

public getFlowTestRuleDescriptions(): Promise<FlowTestRuleDescriptor[]> {
return Promise.resolve(this.rules);
}
}
Loading

0 comments on commit 5ae8f6f

Please sign in to comment.