diff --git a/messages/run-common.md b/messages/run-common.md index 3fb219b72..807d9221b 100644 --- a/messages/run-common.md +++ b/messages/run-common.md @@ -46,6 +46,14 @@ throw an error when a violation threshold is reached, the --normalize-severity i Throws an error when violations are found with equal or greater severity than the provided value. Values are 1 (high), 2 (moderate), and 3 (low). Exit code is the most severe violation. Using this flag also invokes the --normalize-severity flag. +# internal.outfileMustBeValid + +The %s environment variable must be a well-formed filepath. + +# internal.outfileMustBeSupportedType + +The %s environment variable must be of a supported type: .csv; .xml; .json; .html; .sarif. + # validations.cannotWriteTableToFile Format 'table' can't be written to a file. Specify a different format. diff --git a/src/Constants.ts b/src/Constants.ts index 1ca5d1f33..b80f85d3f 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -13,7 +13,8 @@ export const PMD_CATALOG_FILE = 'PmdCatalog.json'; // TODO: We should flesh this one-off solution out into one that handles all the various env vars we use. // E.g., the ones defined in `EnvironmentVariable.ts` and `dfa.ts`. export const ENV_VAR_NAMES = { - SCANNER_PATH_OVERRIDE: 'SCANNER_PATH_OVERRIDE' + SCANNER_PATH_OVERRIDE: 'SCANNER_PATH_OVERRIDE', + SCANNER_INTERNAL_OUTFILE: 'SCANNER_INTERNAL_OUTFILE' }; export const INTERNAL_ERROR_CODE = 1; diff --git a/src/commands/scanner/run.ts b/src/commands/scanner/run.ts index 535f5d83e..967335f1c 100644 --- a/src/commands/scanner/run.ts +++ b/src/commands/scanner/run.ts @@ -9,6 +9,7 @@ import {Action} from "../../lib/ScannerCommand"; import {Display} from "../../lib/Display"; import {RunAction} from "../../lib/actions/RunAction"; import {RuleFilterFactory, RuleFilterFactoryImpl} from "../../lib/RuleFilterFactory"; +import {ResultsProcessorFactory, ResultsProcessorFactoryImpl} from "../../lib/output/ResultsProcessorFactory"; /** * Defines the "run" command for the "scanner" cli. @@ -91,6 +92,8 @@ export default class Run extends ScannerRunCommand { const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version); const ruleFilterFactory: RuleFilterFactory = new RuleFilterFactoryImpl(); const engineOptionsFactory: EngineOptionsFactory = new RunEngineOptionsFactory(inputProcessor); - return new RunAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory); + const resultsProcessorFactory: ResultsProcessorFactory = new ResultsProcessorFactoryImpl(); + return new RunAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory, + resultsProcessorFactory); } } diff --git a/src/commands/scanner/run/dfa.ts b/src/commands/scanner/run/dfa.ts index 80db54145..63a4572cc 100644 --- a/src/commands/scanner/run/dfa.ts +++ b/src/commands/scanner/run/dfa.ts @@ -8,6 +8,7 @@ import {Display} from "../../../lib/Display"; import {Action} from "../../../lib/ScannerCommand"; import {RunDfaAction} from "../../../lib/actions/RunDfaAction"; import {RuleFilterFactory, RuleFilterFactoryImpl} from "../../../lib/RuleFilterFactory"; +import {ResultsProcessorFactory, ResultsProcessorFactoryImpl} from "../../../lib/output/ResultsProcessorFactory"; /** * Defines the "run dfa" command for the "scanner" cli. @@ -79,6 +80,8 @@ export default class Dfa extends ScannerRunCommand { const inputProcessor: InputProcessor = new InputProcessorImpl(this.config.version); const ruleFilterFactory: RuleFilterFactory = new RuleFilterFactoryImpl(); const engineOptionsFactory: EngineOptionsFactory = new RunDfaEngineOptionsFactory(inputProcessor); - return new RunDfaAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory); + const resultsProcessorFactory: ResultsProcessorFactory = new ResultsProcessorFactoryImpl(); + return new RunDfaAction(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory, + resultsProcessorFactory); } } diff --git a/src/lib/InputProcessor.ts b/src/lib/InputProcessor.ts index 47cadaebf..d7ed2f177 100644 --- a/src/lib/InputProcessor.ts +++ b/src/lib/InputProcessor.ts @@ -3,7 +3,7 @@ import normalize = require('normalize-path'); import path = require('path'); import untildify = require("untildify"); import {RunOptions} from "./RuleManager"; -import {RunOutputOptions} from "./util/RunResultsProcessor"; +import {RunOutputOptions} from "./output/RunResultsProcessor"; import {inferFormatFromOutfile, OutputFormat} from "./output/OutputFormat"; /** @@ -62,6 +62,7 @@ export class InputProcessorImpl implements InputProcessor { public createRunOutputOptions(inputs: Inputs): RunOutputOptions { return { format: outputFormatFromInputs(inputs), + verboseViolations: inputs["verbose-violations"] as boolean, severityForError: inputs['severity-threshold'] as number, outfile: inputs.outfile as string }; diff --git a/src/lib/actions/AbstractRunAction.ts b/src/lib/actions/AbstractRunAction.ts index ba36a08fe..eff771a29 100644 --- a/src/lib/actions/AbstractRunAction.ts +++ b/src/lib/actions/AbstractRunAction.ts @@ -10,12 +10,15 @@ import {Display} from "../Display"; import {RuleFilter} from "../RuleFilter"; import {RuleFilterFactory} from "../RuleFilterFactory"; import {Controller} from "../../Controller"; -import {RunOutputOptions, RunResultsProcessor} from "../util/RunResultsProcessor"; +import {RunOutputOptions} from "../output/RunResultsProcessor"; import {InputProcessor} from "../InputProcessor"; import {EngineOptionsFactory} from "../EngineOptionsFactory"; import {INTERNAL_ERROR_CODE} from "../../Constants"; import {Results} from "../output/Results"; import {inferFormatFromOutfile, OutputFormat} from "../output/OutputFormat"; +import {ResultsProcessor} from "../output/ResultsProcessor"; +import {ResultsProcessorFactory} from "../output/ResultsProcessorFactory"; +import {JsonReturnValueHolder} from "../output/JsonReturnValueHolder"; /** * Abstract Action to share a common implementation behind the "run" and "run dfa" commands @@ -26,14 +29,17 @@ export abstract class AbstractRunAction implements Action { private readonly inputProcessor: InputProcessor; private readonly ruleFilterFactory: RuleFilterFactory; private readonly engineOptionsFactory: EngineOptionsFactory; + private readonly resultsProcessorFactory: ResultsProcessorFactory; protected constructor(logger: Logger, display: Display, inputProcessor: InputProcessor, - ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory) { + ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory, + resultsProcessorFactory: ResultsProcessorFactory) { this.logger = logger; this.display = display; this.inputProcessor = inputProcessor; this.ruleFilterFactory = ruleFilterFactory; this.engineOptionsFactory = engineOptionsFactory; + this.resultsProcessorFactory = resultsProcessorFactory; } protected abstract isDfa(): boolean; @@ -78,14 +84,18 @@ export abstract class AbstractRunAction implements Action { const engineOptions: EngineOptions = this.engineOptionsFactory.createEngineOptions(inputs); const outputOptions: RunOutputOptions = this.inputProcessor.createRunOutputOptions(inputs); + const jsonReturnValueHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const resultsProcessor: ResultsProcessor = this.resultsProcessorFactory.createResultsProcessor( + this.display, outputOptions, jsonReturnValueHolder); + // TODO: Inject RuleManager as a dependency to improve testability by removing coupling to runtime implementation const ruleManager: RuleManager = await Controller.createRuleManager(); try { const results: Results = await ruleManager.runRulesMatchingCriteria(filters, targetPaths, runOptions, engineOptions); this.logger.trace(`Processing output with format ${outputOptions.format}`); - return new RunResultsProcessor(this.display, outputOptions, inputs["verbose-violations"] as boolean) - .processResults(results); + await resultsProcessor.processResults(results); + return jsonReturnValueHolder.get(); } catch (e) { // Rethrow any errors as SF errors. diff --git a/src/lib/actions/RunAction.ts b/src/lib/actions/RunAction.ts index 82e2a9de1..e1c2cd566 100644 --- a/src/lib/actions/RunAction.ts +++ b/src/lib/actions/RunAction.ts @@ -6,14 +6,16 @@ import {Inputs} from "../../types"; import {Logger, SfError} from "@salesforce/core"; import {BundleName, getMessage} from "../../MessageCatalog"; import {RuleFilterFactory} from "../RuleFilterFactory"; +import {ResultsProcessorFactory} from "../output/ResultsProcessorFactory"; /** * The Action behind the "run" command */ export class RunAction extends AbstractRunAction { public constructor(logger: Logger, display: Display, inputProcessor: InputProcessor, - ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory) { - super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory); + ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory, + resultsProcessorFactory: ResultsProcessorFactory) { + super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory, resultsProcessorFactory); } public override async validateInputs(inputs: Inputs): Promise { diff --git a/src/lib/actions/RunDfaAction.ts b/src/lib/actions/RunDfaAction.ts index da9679572..af216fa63 100644 --- a/src/lib/actions/RunDfaAction.ts +++ b/src/lib/actions/RunDfaAction.ts @@ -8,14 +8,16 @@ import {FileHandler} from "../util/FileHandler"; import {Logger, SfError} from "@salesforce/core"; import {BundleName, getMessage} from "../../MessageCatalog"; import * as globby from "globby"; +import {ResultsProcessorFactory} from "../output/ResultsProcessorFactory"; /** * The Action behind the "run dfa" command */ export class RunDfaAction extends AbstractRunAction { public constructor(logger: Logger, display: Display, inputProcessor: InputProcessor, - ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory) { - super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory); + ruleFilterFactory: RuleFilterFactory, engineOptionsFactory: EngineOptionsFactory, + resultsProcessorFactory: ResultsProcessorFactory) { + super(logger, display, inputProcessor, ruleFilterFactory, engineOptionsFactory, resultsProcessorFactory); } public override async validateInputs(inputs: Inputs): Promise { diff --git a/src/lib/output/JsonReturnValueHolder.ts b/src/lib/output/JsonReturnValueHolder.ts new file mode 100644 index 000000000..a05d41bbf --- /dev/null +++ b/src/lib/output/JsonReturnValueHolder.ts @@ -0,0 +1,16 @@ +import {AnyJson} from "@salesforce/ts-types"; + +/** + * Container to hold the json return value for the --json flag used by some of the cli commands + */ +export class JsonReturnValueHolder { + private jsonReturnValue: AnyJson; + + public set(jsonReturnValue: AnyJson): void { + this.jsonReturnValue = jsonReturnValue; + } + + public get(): AnyJson { + return this.jsonReturnValue; + } +} diff --git a/src/lib/output/OutfileResultsProcessor.ts b/src/lib/output/OutfileResultsProcessor.ts new file mode 100644 index 000000000..0da855785 --- /dev/null +++ b/src/lib/output/OutfileResultsProcessor.ts @@ -0,0 +1,24 @@ +import {ResultsProcessor} from "./ResultsProcessor"; +import {Results} from "./Results"; +import {OutputFormat} from "./OutputFormat"; +import {FormattedOutput} from "../../types"; +import {FileHandler} from "../util/FileHandler"; + +/** + * Processes results to produce an output file + */ +export class OutfileResultsProcessor implements ResultsProcessor { + private readonly outputFormat: OutputFormat; + private readonly outfile: string; + private readonly verboseViolations: boolean; + public constructor(format:OutputFormat, outfile: string, verboseViolations: boolean) { + this.outputFormat = format; + this.outfile = outfile; + this.verboseViolations = verboseViolations; + } + + public async processResults(results: Results): Promise { + const fileContents: FormattedOutput = await results.toFormattedOutput(this.outputFormat, this.verboseViolations); + (new FileHandler()).writeFileSync(this.outfile, fileContents as string); + } +} diff --git a/src/lib/output/OutputFormat.ts b/src/lib/output/OutputFormat.ts index 9205fc875..297c91869 100644 --- a/src/lib/output/OutputFormat.ts +++ b/src/lib/output/OutputFormat.ts @@ -1,6 +1,6 @@ import {SfError} from "@salesforce/core"; import {BundleName, getMessage} from "../../MessageCatalog"; -import {INTERNAL_ERROR_CODE} from "../../Constants"; +import {ENV_VAR_NAMES, INTERNAL_ERROR_CODE} from "../../Constants"; export enum OutputFormat { CSV = 'csv', @@ -13,9 +13,21 @@ export enum OutputFormat { } export function inferFormatFromOutfile(outfile: string): OutputFormat { + return determineOutputFormat(outfile, + getMessage(BundleName.CommonRun, 'validations.outfileMustBeValid'), + getMessage(BundleName.CommonRun, 'validations.outfileMustBeSupportedType')) +} + +export function inferFormatFromInternalOutfile(outfile: string): OutputFormat { + return determineOutputFormat(outfile, + getMessage(BundleName.CommonRun, 'internal.outfileMustBeValid', [ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE]), + getMessage(BundleName.CommonRun, 'internal.outfileMustBeSupportedType', [ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE])); +} + +function determineOutputFormat(outfile: string, invalidFileMsg: string, invalidExtensionMsg: string): OutputFormat { const lastPeriod: number = outfile.lastIndexOf('.'); if (lastPeriod < 1 || lastPeriod + 1 === outfile.length) { - throw new SfError(getMessage(BundleName.CommonRun, 'validations.outfileMustBeValid'), null, null, INTERNAL_ERROR_CODE); + throw new SfError(invalidFileMsg, null, null, INTERNAL_ERROR_CODE); } const fileExtension: string = outfile.slice(lastPeriod + 1).toLowerCase(); switch (fileExtension) { @@ -26,6 +38,6 @@ export function inferFormatFromOutfile(outfile: string): OutputFormat { case OutputFormat.XML: return fileExtension; default: - throw new SfError(getMessage(BundleName.CommonRun, 'validations.outfileMustBeSupportedType'), null, null, INTERNAL_ERROR_CODE); + throw new SfError(invalidExtensionMsg, null, null, INTERNAL_ERROR_CODE); } } diff --git a/src/lib/output/Results.ts b/src/lib/output/Results.ts index aa3f9157a..3200bc944 100644 --- a/src/lib/output/Results.ts +++ b/src/lib/output/Results.ts @@ -33,6 +33,7 @@ export interface Results { export class RunResults implements Results { private readonly ruleResults: RuleResult[]; private readonly executedEngines: Set; + private readonly formattedResultsCache: Map = new Map; constructor(ruleResults: RuleResult[], executedEngines: Set, ) { this.ruleResults = ruleResults; @@ -100,6 +101,11 @@ export class RunResults implements Results { } public async toFormattedOutput(format: OutputFormat, verboseViolations: boolean): Promise { + const cacheKey: string = String(format) + '_' + String(verboseViolations); + if (this.formattedResultsCache.has(cacheKey)) { + return Promise.resolve(this.formattedResultsCache.get(cacheKey)); + } + let outputFormatter: OutputFormatter; switch (format) { case OutputFormat.CSV: @@ -126,6 +132,8 @@ export class RunResults implements Results { default: throw new SfError('Unrecognized output format.'); } - return await outputFormatter.format(this); + const formattedOutput: FormattedOutput = await outputFormatter.format(this); + this.formattedResultsCache.set(cacheKey, formattedOutput); + return formattedOutput; } } diff --git a/src/lib/output/ResultsProcessor.ts b/src/lib/output/ResultsProcessor.ts new file mode 100644 index 000000000..8ecdfb2e8 --- /dev/null +++ b/src/lib/output/ResultsProcessor.ts @@ -0,0 +1,25 @@ +import {Results} from "./Results"; + +/** + * Interface to process run results + */ +export interface ResultsProcessor { + processResults(results: Results): Promise; +} + +/** + * A composite results processor + */ +export class CompositeResultsProcessor implements ResultsProcessor { + private readonly delegates: ResultsProcessor[]; + + public constructor(delegateResultsProcessors: ResultsProcessor[]) { + this.delegates = delegateResultsProcessors; + } + + async processResults(results: Results): Promise { + for (const delegate of this.delegates) { + await delegate.processResults(results); + } + } +} diff --git a/src/lib/output/ResultsProcessorFactory.ts b/src/lib/output/ResultsProcessorFactory.ts new file mode 100644 index 000000000..40c72fe1d --- /dev/null +++ b/src/lib/output/ResultsProcessorFactory.ts @@ -0,0 +1,35 @@ +import {Display} from "../Display"; +import {JsonReturnValueHolder} from "./JsonReturnValueHolder"; +import {RunOutputOptions, RunResultsProcessor} from "./RunResultsProcessor"; +import {CompositeResultsProcessor, ResultsProcessor} from "./ResultsProcessor"; +import {ENV_VAR_NAMES} from "../../Constants"; +import {inferFormatFromInternalOutfile, OutputFormat} from "./OutputFormat"; +import {OutfileResultsProcessor} from "./OutfileResultsProcessor"; + +/** + * Interface for creating a ResultsProcessor + */ +export interface ResultsProcessorFactory { + createResultsProcessor(display: Display, runOutputOptions: RunOutputOptions, + jsonReturnValueHolder: JsonReturnValueHolder): ResultsProcessor +} + +/** + * Runtime implementation of the ResultsProcessorFactory interface + */ +export class ResultsProcessorFactoryImpl implements ResultsProcessorFactory { + public createResultsProcessor(display: Display, runOutputOptions: RunOutputOptions, + jsonReturnValueHolder: JsonReturnValueHolder): ResultsProcessor { + const resultsProcessors: ResultsProcessor[] = [new RunResultsProcessor(display, runOutputOptions, jsonReturnValueHolder)]; + this.addProcessorForInternalOutfileIfNeeded(resultsProcessors, runOutputOptions.verboseViolations); + return new CompositeResultsProcessor(resultsProcessors); + } + + private addProcessorForInternalOutfileIfNeeded(resultsProcessors: ResultsProcessor[], verboseViolations: boolean): void { + const internalOutfile: string = process.env[ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE]; + if (internalOutfile && internalOutfile.length > 0) { + const internalOutputFormat: OutputFormat = inferFormatFromInternalOutfile(internalOutfile); + resultsProcessors.push(new OutfileResultsProcessor(internalOutputFormat, internalOutfile, verboseViolations)); + } + } +} diff --git a/src/lib/util/RunResultsProcessor.ts b/src/lib/output/RunResultsProcessor.ts similarity index 78% rename from src/lib/util/RunResultsProcessor.ts rename to src/lib/output/RunResultsProcessor.ts index ffd415e4d..58267b02c 100644 --- a/src/lib/util/RunResultsProcessor.ts +++ b/src/lib/output/RunResultsProcessor.ts @@ -1,34 +1,47 @@ import {AnyJson} from '@salesforce/ts-types'; import {SfError} from '@salesforce/core'; -import fs = require('fs'); import {FormattedOutput, EngineExecutionSummary} from '../../types'; import {BundleName, getMessage} from "../../MessageCatalog"; import {Display} from "../Display"; import {INTERNAL_ERROR_CODE} from "../../Constants"; -import {OutputFormat} from "../output/OutputFormat"; -import {Results} from "../output/Results"; +import {OutputFormat} from "./OutputFormat"; +import {Results} from "./Results"; +import {ResultsProcessor} from "./ResultsProcessor"; +import {JsonReturnValueHolder} from "./JsonReturnValueHolder"; +import {FileHandler} from "../util/FileHandler"; export type RunOutputOptions = { format: OutputFormat; + verboseViolations: boolean; severityForError?: number; outfile?: string; } -export class RunResultsProcessor { +/** + * The primary ResultsProcessor used in production + * + * Note: We should consider separating this into multiple ResultProcessor classes to separate the responsibilities: + * --> creating the json return value + * --> creating the users outfile (where we can reuse the OutfileResultsProcessor) + * --> creating console output + * --> checking and throwing an exception if over severity threshold + * Right now all of these are entangled with one another below and a design change would be needed to do this. + */ +export class RunResultsProcessor implements ResultsProcessor { private readonly display: Display; private readonly opts: RunOutputOptions; - private readonly verboseViolations: boolean; + private jsonReturnValueHolder: JsonReturnValueHolder; - public constructor(display: Display, opts: RunOutputOptions, verboseViolations: boolean) { + public constructor(display: Display, opts: RunOutputOptions, jsonReturnValueHolder: JsonReturnValueHolder) { this.display = display; this.opts = opts; - this.verboseViolations = verboseViolations; + this.jsonReturnValueHolder = jsonReturnValueHolder; } - public async processResults(results: Results): Promise { + public async processResults(results: Results): Promise { const minSev: number = results.getMinSev(); const summaryMap: Map = results.getSummaryMap(); - const formattedOutput = await results.toFormattedOutput(this.opts.format, this.verboseViolations); + const formattedOutput = await results.toFormattedOutput(this.opts.format, this.opts.verboseViolations); const hasViolations = [...summaryMap.values()].some(summary => summary.violationCount !== 0); @@ -42,7 +55,8 @@ export class RunResultsProcessor { // ...log it to the console... this.display.displayInfo(msg); // ...and return it for use with the --json flag. - return msg; + this.jsonReturnValueHolder.set(msg); + return; } // If we have violations (or an outfile but no violations), we'll build an array of @@ -68,15 +82,16 @@ export class RunResultsProcessor { // Finally, we need to return something for use by the --json flag. if (this.opts.outfile) { // If we used an outfile, we should just return the summary message, since that says where the file is. - return msg; + this.jsonReturnValueHolder.set(msg); } else if (typeof formattedOutput === 'string') { // If the specified output format was JSON, then the results are a huge stringified JSON that we should parse // before returning. Otherwise, we should just return the result string. - return this.opts.format === OutputFormat.JSON ? JSON.parse(formattedOutput) as AnyJson : formattedOutput; + this.jsonReturnValueHolder.set( + this.opts.format === OutputFormat.JSON ? JSON.parse(formattedOutput) as AnyJson : formattedOutput); } else { // If the results are a JSON, return the `rows` property, since that's all of the data that would be displayed // in the table. - return formattedOutput.rows; + this.jsonReturnValueHolder.set(formattedOutput.rows); } } @@ -117,15 +132,10 @@ export class RunResultsProcessor { } private writeToOutfile(results: string | {columns; rows}): string { - try { - // At this point, we can cast `results` to a string, since it being an object would indicate that the format - // is `table`, and we already have validations preventing tables from being written to files. - fs.writeFileSync(this.opts.outfile, results as string); - } catch (e) { - // Rethrow any errors as SfdxErrors. - const message: string = e instanceof Error ? e.message : e as string; - throw new SfError(message, null, null, INTERNAL_ERROR_CODE); - } + // At this point, we can cast `results` to a string, since it being an object would indicate that the format + // is `table`, and we already have validations preventing tables from being written to files. + (new FileHandler()).writeFileSync(this.opts.outfile, results as string); + // Return a message indicating the action we took. return getMessage(BundleName.RunOutputProcessor, 'output.writtenToOutFile', [this.opts.outfile]); } diff --git a/src/lib/util/FileHandler.ts b/src/lib/util/FileHandler.ts index e0adaa8b7..498e7cfb3 100644 --- a/src/lib/util/FileHandler.ts +++ b/src/lib/util/FileHandler.ts @@ -1,5 +1,8 @@ -import {Stats, promises as fs, constants as fsConstants} from 'fs'; +import {Stats, promises as fsp, constants as fsConstants} from 'fs'; +import fs = require('fs'); import tmp = require('tmp'); +import {SfError} from "@salesforce/core"; +import {INTERNAL_ERROR_CODE} from "../../Constants"; type DuplicationFn = (src: string, target: string) => Promise; @@ -10,7 +13,7 @@ type DuplicationFn = (src: string, target: string) => Promise; export class FileHandler { async exists(filename: string): Promise { try { - await fs.access(filename, fsConstants.F_OK); + await fsp.access(filename, fsConstants.F_OK); return true; } catch (e) { return false; @@ -18,7 +21,7 @@ export class FileHandler { } stats(filename: string): Promise { - return fs.stat(filename); + return fsp.stat(filename); } async isDir(filename: string): Promise { @@ -30,24 +33,34 @@ export class FileHandler { } readDir(filename: string): Promise { - return fs.readdir(filename); + return fsp.readdir(filename); } readFileAsBuffer(filename: string): Promise { - return fs.readFile(filename); + return fsp.readFile(filename); } readFile(filename: string): Promise { - return fs.readFile(filename, 'utf-8'); + return fsp.readFile(filename, 'utf-8'); } async mkdirIfNotExists(dir: string): Promise { - await fs.mkdir(dir, {recursive: true}); + await fsp.mkdir(dir, {recursive: true}); return; } writeFile(filename: string, fileContent: string): Promise { - return fs.writeFile(filename, fileContent); + return fsp.writeFile(filename, fileContent); + } + + writeFileSync(filename: string, fileContent: string): void { + try { + fs.writeFileSync(filename, fileContent); + } catch (e) { + // Rethrow any errors as SfError. + const message: string = e instanceof Error ? e.message : e as string; + throw new SfError(message, null, null, INTERNAL_ERROR_CODE); + } } // Create a temp file that will automatically be cleaned up when the process exits. @@ -86,7 +99,7 @@ export class FileHandler { // at the moment. So we'll go with this semi-naive implementation, aware that it performs SLIGHTLY worse than // an optimal one, and prepared to address it if there's somehow a problem. // These are the file duplication functions available to us, in order of preference. - const dupFns: DuplicationFn[] = [fs.symlink, fs.link, fs.copyFile]; + const dupFns: DuplicationFn[] = [fsp.symlink, fsp.link, fsp.copyFile]; const errMsgs: string[] = []; // Iterate over the potential duplication methods.... diff --git a/test/commands/scanner/run.test.ts b/test/commands/scanner/run.test.ts index 0431cfa59..fa080381d 100644 --- a/test/commands/scanner/run.test.ts +++ b/test/commands/scanner/run.test.ts @@ -1,11 +1,13 @@ import {expect} from 'chai'; // @ts-ignore import {runCommand} from '../../TestUtils'; +import {BundleName, getMessage} from "../../../src/MessageCatalog"; +import * as os from "os"; +import {ENV_VAR_NAMES} from "../../../src/Constants"; import fs = require('fs'); import path = require('path'); import process = require('process'); import tildify = require('tildify'); -import {BundleName, getMessage} from "../../../src/MessageCatalog"; const pathToApexFolder = path.join('test', 'code-fixtures', 'apex'); const pathToSomeTestClass = path.join('test', 'code-fixtures', 'apex', 'SomeTestClass.cls'); @@ -16,18 +18,6 @@ const pathToYetAnotherTestClass = path.join('test', 'code-fixtures', 'apex', 'Ye describe('scanner run', function () { describe('E2E', () => { describe('Output Type: XML', () => { - function validateXmlOutput(xml: string): void { - // We'll split the output by the tag, so we can get individual violations. - const violations = xml.split(' tag, so we can get individual violations. @@ -300,17 +290,6 @@ describe('scanner run', function () { }); describe('Output Type: JSON', () => { - function validateJsonOutput(json: string): void { - const output = JSON.parse(json); - // Only PMD rules should have run. - expect(output.length).to.equal(1, 'Should only be violations from one engine'); - expect(output[0].engine).to.equal('pmd', 'Engine should be PMD'); - - expect(output[0].violations.length).to.equal(2, 'Should be 2 violations'); - expect(output[0].violations[0].line).to.equal(11, 'Violation #1 should occur on the expected line'); - expect(output[0].violations[1].line).to.equal(19, 'Violation #2 should occur on the expected line'); - } - function validateNoViolationsJsonOutput(json: string): void { const output = JSON.parse(json); // There should be no violations. @@ -621,4 +600,77 @@ describe('scanner run', function () { }); }); + + describe('Create internal outfile with SCANNER_INTERNAL_OUTFILE environment variable', () => { + let tmpDir: string = null; + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'InternalOutfileTest-')); + }); + afterEach(() => { + fs.rmSync(tmpDir, {recursive: true, force: true}); + delete process.env[ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE]; + }); + + it('Can write a user file and an internal file with different formats at same time', () => { + const internalOutfile = path.join(tmpDir, "internalOutfile.json"); + process.env[ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE] = internalOutfile; + const userOutfile = path.join(tmpDir, "userOutfile.xml"); + runCommand(`scanner run --target ${pathToSomeTestClass} --ruleset ApexUnit --outfile ${userOutfile}`); + + expect(fs.existsSync(userOutfile)).to.equal(true, 'The command should have created the expected user output file'); + const userFileContents = fs.readFileSync(userOutfile).toString(); + validateXmlOutput(userFileContents); + + expect(fs.existsSync(internalOutfile)).to.equal(true, 'The command should have created the expected internal output file'); + const internalFileContents = fs.readFileSync(internalOutfile).toString(); + validateJsonOutput(internalFileContents); + }); + + + it('Can write to internal file and write to console', () => { + const internalOutfile = path.join(tmpDir, "internalOutfile.json"); + process.env[ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE] = internalOutfile; + const output = runCommand(`scanner run --target ${pathToSomeTestClass} --ruleset ApexUnit --format xml`); + + validateXmlOutput(output.shellOutput.stdout); + + expect(fs.existsSync(internalOutfile)).to.equal(true, 'The command should have created the expected internal output file'); + const internalFileContents = fs.readFileSync(internalOutfile).toString(); + validateJsonOutput(internalFileContents); + }); + + it('Invalid internal file name gives appropriate error', () => { + const internalOutfile = path.join(tmpDir, "internalOutfile.notSupported"); + process.env[ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE] = internalOutfile; + const userOutfile = path.join(tmpDir, "userOutfile.xml"); + const output = runCommand(`scanner run --target ${pathToSomeTestClass} --ruleset ApexUnit --outfile ${userOutfile}`); + + expect(output.shellOutput.stderr).contains( + getMessage(BundleName.CommonRun, 'internal.outfileMustBeSupportedType', [ENV_VAR_NAMES.SCANNER_INTERNAL_OUTFILE])); + }); + }); }); + +function validateXmlOutput(xml: string): void { + // We'll split the output by the tag, so we can get individual violations. + const violations = xml.split(' { + it('Empty composite should not blow up', async () => { + const compositeResultsProcessor: CompositeResultsProcessor = new CompositeResultsProcessor([]); + await compositeResultsProcessor.processResults(new FakeResults()); + }); + + it('Each delegate is called correctly', async () => { + const results: Results = new FakeResults(); + const resultsProcessors: FakeResultsProcessor[] = [ + new FakeResultsProcessor(), new FakeResultsProcessor(), new FakeResultsProcessor()]; + const compositeResultsProcessor: CompositeResultsProcessor = new CompositeResultsProcessor(resultsProcessors); + await compositeResultsProcessor.processResults(results); + for (let i = 0; i < resultsProcessors.length; i++) { + expect(resultsProcessors[i].lastResultsProcessed).to.equal(results); + } + }); +}); + +class FakeResultsProcessor implements ResultsProcessor { + lastResultsProcessed: Results = null; + + processResults(results: Results): Promise { + this.lastResultsProcessed = results; + return Promise.resolve(); + } +} diff --git a/test/lib/output/FakeResults.ts b/test/lib/output/FakeResults.ts new file mode 100644 index 000000000..b4c11901d --- /dev/null +++ b/test/lib/output/FakeResults.ts @@ -0,0 +1,60 @@ +import {Results} from "../../../src/lib/output/Results"; +import {EngineExecutionSummary, FormattedOutput, RuleResult} from "../../../src/types"; +import {OutputFormat} from "../../../src/lib/output/OutputFormat"; + +export class FakeResults implements Results { + private minSev: number = 0; + private summaryMap: Map; + private formattedOutputMap: Map = new Map(); + + withMinSev(minSev: number): FakeResults { + this.minSev = minSev; + return this; + } + + withSummaryMap(summaryMap: Map): FakeResults { + this.summaryMap = summaryMap; + return this; + } + + withFormattedOutput(formattedOutput: FormattedOutput): FakeResults { + this.formattedOutputMap.set("default", formattedOutput); + return this; + } + + withFormattedOutputForFormat(format: OutputFormat, formattedOutput: FormattedOutput): FakeResults { + this.formattedOutputMap.set(format as string, formattedOutput); + return this; + } + + getExecutedEngines(): Set { + throw new Error("Not implemented"); + } + + getMinSev(): number { + return this.minSev; + } + + getRuleResults(): RuleResult[] { + throw new Error("Not implemented"); + } + + getSummaryMap(): Map { + return this.summaryMap; + } + + isEmpty(): boolean { + throw new Error("Not implemented"); + } + + violationsAreDfa(): boolean { + throw new Error("Not implemented"); + } + + toFormattedOutput(format: OutputFormat, _verboseViolations: boolean): Promise { + if(this.formattedOutputMap.has(format as string)) { + return Promise.resolve(this.formattedOutputMap.get(format as string)); + } + return Promise.resolve(this.formattedOutputMap.get("default")); + } +} diff --git a/test/lib/output/OutfileResultsProcessor.test.ts b/test/lib/output/OutfileResultsProcessor.test.ts new file mode 100644 index 000000000..524abcacf --- /dev/null +++ b/test/lib/output/OutfileResultsProcessor.test.ts @@ -0,0 +1,68 @@ +import {FakeResults} from "./FakeResults"; +import {Results} from "../../../src/lib/output/Results"; +import {ResultsProcessor} from "../../../src/lib/output/ResultsProcessor"; +import {OutfileResultsProcessor} from "../../../src/lib/output/OutfileResultsProcessor"; +import * as os from "os"; +import * as path from "path"; +import {expect} from "chai"; +import {OutputFormat} from "../../../src/lib/output/OutputFormat"; +import fs = require('fs'); + +describe('OutfileResultsProcessor Tests', () => { + let tmpDir: string = null; + const results: Results = new FakeResults() + .withFormattedOutputForFormat(OutputFormat.CSV, "dummy csv contents") + .withFormattedOutputForFormat(OutputFormat.HTML, "dummy html contents") + .withFormattedOutputForFormat(OutputFormat.JSON, "dummy json contents") + .withFormattedOutputForFormat(OutputFormat.JUNIT, "dummy junit contents") + .withFormattedOutputForFormat(OutputFormat.SARIF, "dummy sarif contents") + .withFormattedOutputForFormat(OutputFormat.XML, "dummy xml contents"); + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'OutfileResultsProcessorTest-')); + }); + afterEach(() => { + fs.rmSync(tmpDir, {recursive: true, force: true}); + }); + + it('csv file', async () => { + const outfile: string = path.join(tmpDir, "out_file.csv"); + const resultsProcessor: ResultsProcessor = new OutfileResultsProcessor(OutputFormat.CSV, outfile, false); + await resultsProcessor.processResults(results); + expect(fs.readFileSync(outfile).toString()).to.equal('dummy csv contents'); + }); + + it('html file', async () => { + const outfile: string = path.join(tmpDir, "out_file.HTML"); + const resultsProcessor: ResultsProcessor = new OutfileResultsProcessor(OutputFormat.HTML, outfile, false); + await resultsProcessor.processResults(results); + expect(fs.readFileSync(outfile).toString()).to.equal('dummy html contents'); + }); + + it('json file', async () => { + const outfile: string = path.join(tmpDir, "out_file.json"); + const resultsProcessor: ResultsProcessor = new OutfileResultsProcessor(OutputFormat.JSON, outfile, false); + await resultsProcessor.processResults(results); + expect(fs.readFileSync(outfile).toString()).to.equal('dummy json contents'); + }); + + it('junit xml file', async () => { + const outfile: string = path.join(tmpDir, "out_file.xml"); + const resultsProcessor: ResultsProcessor = new OutfileResultsProcessor(OutputFormat.JUNIT, outfile, false); + await resultsProcessor.processResults(results); + expect(fs.readFileSync(outfile).toString()).to.equal('dummy junit contents'); + }); + + it('sarif file', async () => { + const outfile: string = path.join(tmpDir, "out_file.sarif"); + const resultsProcessor: ResultsProcessor = new OutfileResultsProcessor(OutputFormat.SARIF, outfile, false); + await resultsProcessor.processResults(results); + expect(fs.readFileSync(outfile).toString()).to.equal('dummy sarif contents'); + }); + + it('xml file', async () => { + const outfile: string = path.join(tmpDir, "out_file.xMl"); + const resultsProcessor: ResultsProcessor = new OutfileResultsProcessor(OutputFormat.XML, outfile, false); + await resultsProcessor.processResults(results); + expect(fs.readFileSync(outfile).toString()).to.equal('dummy xml contents'); + }); +}); diff --git a/test/lib/util/RunOutputProcessor.test.ts b/test/lib/output/RunResultsProcessor.test.ts similarity index 78% rename from test/lib/util/RunOutputProcessor.test.ts rename to test/lib/output/RunResultsProcessor.test.ts index 4785c918c..f16da1bf9 100644 --- a/test/lib/util/RunOutputProcessor.test.ts +++ b/test/lib/output/RunResultsProcessor.test.ts @@ -1,7 +1,7 @@ import {expect} from 'chai'; -import {RunOutputOptions, RunResultsProcessor} from '../../../src/lib/util/RunResultsProcessor'; -import {EngineExecutionSummary, FormattedOutput, RuleResult} from '../../../src/types'; +import {RunOutputOptions} from '../../../src/lib/output/RunResultsProcessor'; +import {EngineExecutionSummary} from '../../../src/types'; import {AnyJson} from '@salesforce/ts-types'; import Sinon = require('sinon'); import fs = require('fs'); @@ -10,6 +10,11 @@ import {FakeDisplay} from "../FakeDisplay"; import {PATHLESS_COLUMNS} from "../../../src/lib/output/TableOutputFormatter"; import {OutputFormat} from "../../../src/lib/output/OutputFormat"; import {Results} from "../../../src/lib/output/Results"; +import {FakeResults} from "./FakeResults"; +import {JsonReturnValueHolder} from "../../../src/lib/output/JsonReturnValueHolder"; +import {ResultsProcessor} from "../../../src/lib/output/ResultsProcessor"; +import {ResultsProcessorFactoryImpl} from "../../../src/lib/output/ResultsProcessorFactory"; +import {Display} from "../../../src/lib/Display"; const FAKE_SUMMARY_MAP: Map = new Map(); FAKE_SUMMARY_MAP.set('pmd', {fileCount: 1, violationCount: 1}); @@ -78,55 +83,6 @@ const FAKE_JSON_OUTPUT = `[{ }] }]`; -class FakeResults implements Results { - private minSev: number = 0; - private summaryMap: Map; - private formattedOutput: FormattedOutput; - - withMinSev(minSev: number): FakeResults { - this.minSev = minSev; - return this; - } - - withSummaryMap(summaryMap: Map): FakeResults { - this.summaryMap = summaryMap; - return this; - } - - withFormattedOutput(formattedOutput: FormattedOutput): FakeResults { - this.formattedOutput = formattedOutput; - return this; - } - - getExecutedEngines(): Set { - throw new Error("Not implemented"); - } - - getMinSev(): number { - return this.minSev; - } - - getRuleResults(): RuleResult[] { - throw new Error("Not implemented"); - } - - getSummaryMap(): Map { - return this.summaryMap; - } - - isEmpty(): boolean { - throw new Error("Not implemented"); - } - - violationsAreDfa(): boolean { - throw new Error("Not implemented"); - } - - toFormattedOutput(_format: OutputFormat, _verboseViolations: boolean): Promise { - return Promise.resolve(this.formattedOutput); - } -} - describe('RunOutputProcessor', () => { let fakeFiles: {path; data}[] = []; let display: FakeDisplay; @@ -151,9 +107,11 @@ describe('RunOutputProcessor', () => { describe('Writing to console', () => { it('Empty results yield expected message', async () => { const opts: RunOutputOptions = { - format: OutputFormat.TABLE + format: OutputFormat.TABLE, + verboseViolations: false }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); const summaryMap: Map = new Map(); summaryMap.set('pmd', {fileCount: 0, violationCount: 0}); summaryMap.set('eslint', {fileCount: 0, violationCount: 0}); @@ -161,7 +119,8 @@ describe('RunOutputProcessor', () => { .withMinSev(0).withSummaryMap(summaryMap).withFormattedOutput(''); // THIS IS THE PART BEING TESTED. - const output: AnyJson = await rop.processResults(fakeResults); + await rrp.processResults(fakeResults); + const output: AnyJson = jsonHolder.get(); // We expect that the message logged to the console and the message returned should both be the default const expectedMsg = getMessage(BundleName.RunOutputProcessor, 'output.noViolationsDetected', ['pmd, eslint']); @@ -175,12 +134,15 @@ describe('RunOutputProcessor', () => { it('Table-type output should be followed by summary', async () => { const opts: RunOutputOptions = { - format: OutputFormat.TABLE + format: OutputFormat.TABLE, + verboseViolations: false }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED. - const output: AnyJson = await rop.processResults(fakeTableResults); + await rrp.processResults(fakeTableResults); + const output: AnyJson = jsonHolder.get(); const expectedTableSummary = `${getMessage(BundleName.RunOutputProcessor, 'output.engineSummaryTemplate', ['pmd', 1, 1])} ${getMessage(BundleName.RunOutputProcessor, 'output.engineSummaryTemplate', ['eslint-typescript', 2, 1])} @@ -197,13 +159,16 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToConsole')}`; it('Throws severity-based exception on request', async () => { const opts: RunOutputOptions = { format: OutputFormat.TABLE, + verboseViolations: false, severityForError: 1 }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED. try { - const output: AnyJson = await rop.processResults(fakeTableResults); + await rrp.processResults(fakeTableResults); + const output: AnyJson = jsonHolder.get(); expect(true).to.equal(false, `Unexpectedly returned ${output} instead of throwing error`); } catch (e) { expect(display.getOutputText()).to.satisfy(msg => msg.startsWith("[Table]")); @@ -227,13 +192,16 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToConsole')}`; // need to change. it('CSV-type output should NOT be followed by summary', async () => { const opts: RunOutputOptions = { - format: OutputFormat.CSV + format: OutputFormat.CSV, + verboseViolations: false }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED. - const output: AnyJson = await rop.processResults(fakeCsvResults); + await rrp.processResults(fakeCsvResults); + const output: AnyJson = jsonHolder.get(); expect(display.getOutputText()).to.equal("[Info]: " + FAKE_CSV_OUTPUT); expect(output).to.equal(FAKE_CSV_OUTPUT, 'CSV should be returned as a string'); }); @@ -241,14 +209,17 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToConsole')}`; it('Throws severity-based exception on request', async () => { const opts: RunOutputOptions = { format: OutputFormat.CSV, + verboseViolations: false, severityForError: 2 }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED. try { - const output: AnyJson = await rop.processResults(fakeCsvResults); + await rrp.processResults(fakeCsvResults); + const output: AnyJson = jsonHolder.get(); expect(true).to.equal(false, `Unexpectedly returned ${output} instead of throwing error`); } catch (e) { expect(display.getOutputText()).to.equal("[Info]: " + FAKE_CSV_OUTPUT); @@ -267,13 +238,16 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToConsole')}`; // need to change. it('JSON-type output with no violations should output be an empty violation set', async () => { const opts: RunOutputOptions = { - format: OutputFormat.JSON + format: OutputFormat.JSON, + verboseViolations: false }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED - const output: AnyJson = await rop.processResults(fakeJsonResults); + await rrp.processResults(fakeJsonResults); + const output: AnyJson = jsonHolder.get(); expect(display.getOutputText()).to.equal("[Info]: " + FAKE_JSON_OUTPUT); expect(output).to.deep.equal(JSON.parse(FAKE_JSON_OUTPUT), 'JSON should be returned as a parsed object'); @@ -282,14 +256,17 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToConsole')}`; it('Throws severity-based exception on request', async () => { const opts: RunOutputOptions = { format: OutputFormat.JSON, + verboseViolations: false, severityForError: 1 }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED try { - const output: AnyJson = await rop.processResults(fakeJsonResults); + await rrp.processResults(fakeJsonResults); + const output: AnyJson = jsonHolder.get(); expect(true).to.equal(false, `Unexpectedly returned ${output} instead of throwing error`); } catch (e) { expect(display.getOutputText()).to.equal("[Info]: " + FAKE_JSON_OUTPUT); @@ -304,9 +281,11 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToConsole')}`; it('Empty results yield expected message', async () => { const opts: RunOutputOptions = { format: OutputFormat.CSV, + verboseViolations: false, outfile: fakeFilePath }; - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); const summaryMap: Map = new Map(); summaryMap.set('pmd', {fileCount: 0, violationCount: 0}); summaryMap.set('eslint', {fileCount: 0, violationCount: 0}); @@ -314,7 +293,8 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToConsole')}`; .withMinSev(0).withSummaryMap(summaryMap).withFormattedOutput('"Problem","Severity","File","Line","Column","Rule","Description","URL","Category","Engine"'); // THIS IS THE PART BEING TESTED. - const output: AnyJson = await rop.processResults(fakeResults); + await rrp.processResults(fakeResults); + const output: AnyJson = jsonHolder.get(); // We expect the empty CSV output followed by the default engine summary and written-to-file messages are logged to the console const expectedMsg = `${getMessage(BundleName.RunOutputProcessor, 'output.engineSummaryTemplate', ['pmd', 0, 0])} @@ -333,13 +313,15 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToOutFile', [fakeFile it('Results are properly written to file', async () => { const opts: RunOutputOptions = { format: OutputFormat.CSV, + verboseViolations: false, outfile: fakeFilePath }; - - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED. - const output: AnyJson = await rop.processResults(fakeCsvResults); + await rrp.processResults(fakeCsvResults); + const output: AnyJson = jsonHolder.get(); const expectedCsvSummary = `${getMessage(BundleName.RunOutputProcessor, 'output.engineSummaryTemplate', ['pmd', 1, 1])} ${getMessage(BundleName.RunOutputProcessor, 'output.engineSummaryTemplate', ['eslint-typescript', 2, 1])} @@ -353,15 +335,17 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToOutFile', [fakeFile it('Throws severity-based exception on request', async () => { const opts: RunOutputOptions = { format: OutputFormat.CSV, + verboseViolations: false, severityForError: 1, outfile: fakeFilePath }; - - const rop = new RunResultsProcessor(display, opts, false); + const jsonHolder: JsonReturnValueHolder = new JsonReturnValueHolder(); + const rrp = createResultsProcessor(display, opts, jsonHolder); // THIS IS THE PART BEING TESTED. try { - const output: AnyJson = await rop.processResults(fakeCsvResults); + await rrp.processResults(fakeCsvResults); + const output: AnyJson = jsonHolder.get(); expect(true).to.equal(false, `Unexpectedly returned ${output} instead of throwing error`); } catch (e) { expect(display.getOutputText()).to.equal(""); @@ -378,3 +362,8 @@ ${getMessage(BundleName.RunOutputProcessor, 'output.writtenToOutFile', [fakeFile }); }); }); + +function createResultsProcessor(display: Display, runOutputOptions: RunOutputOptions, + jsonReturnValueHolder: JsonReturnValueHolder): ResultsProcessor { + return (new ResultsProcessorFactoryImpl()).createResultsProcessor(display, runOutputOptions, jsonReturnValueHolder); +}