From 9ba768c48973fc5f23a0a7aaa43513a7824344fb Mon Sep 17 00:00:00 2001 From: gitphill Date: Fri, 10 Jan 2020 13:11:44 +0000 Subject: [PATCH] feat: add exclude arg and use to ignore Ignore directories when using --all-projects. Added tests for test and monitor with --exclude. Added arg validation and help text --- help/help.txt | 5 +++ src/cli/copy.ts | 2 +- src/cli/index.ts | 23 +++++++++- src/lib/errors/exclude-flag-bad-input.ts | 13 ++++++ src/lib/errors/exclude-flag-invalid-input.ts | 13 ++++++ src/lib/errors/index.ts | 3 ++ src/lib/errors/option-missing-error.ts | 10 +++++ src/lib/plugins/get-deps-from-plugin.ts | 9 +++- src/lib/types.ts | 1 + test/acceptance/cli-args.test.ts | 45 ++++++++++++++++++- .../cli-monitor.all-projects.spec.ts | 35 ++++++++++++++- .../cli-test/cli-test.all-projects.spec.ts | 29 ++++++++++++ 12 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 src/lib/errors/exclude-flag-bad-input.ts create mode 100644 src/lib/errors/exclude-flag-invalid-input.ts create mode 100644 src/lib/errors/option-missing-error.ts diff --git a/help/help.txt b/help/help.txt index ac34c6a3ab..a4f2e608db 100644 --- a/help/help.txt +++ b/help/help.txt @@ -131,6 +131,11 @@ Experimental options: (test & monitor commands only) Use with --all-projects to indicate how many sub-directories to search. Defaults to 1 (the current working directory). + --exclude= + (test & monitor commands only) + Can only be used with --all-projects to indicate sub-directories to exclude. + Directories must be comma seperated. + If using with --detection-depth exclude ignores directories at any level deep. Examples: diff --git a/src/cli/copy.ts b/src/cli/copy.ts index a2ec556882..615e3c3b0d 100644 --- a/src/cli/copy.ts +++ b/src/cli/copy.ts @@ -6,6 +6,6 @@ const program = { win32: 'clip', }[process.platform]; -export function copy(str) { +export function copy(str: string) { return execSync(program, { input: str }); } diff --git a/src/cli/index.ts b/src/cli/index.ts index be9ce55fe6..65fc0c79fe 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import 'source-map-support/register'; import * as Debug from 'debug'; +import * as pathLib from 'path'; // assert supported node runtime version import * as runtime from './runtime'; @@ -15,9 +16,15 @@ import errors = require('../lib/errors/legacy-errors'); import ansiEscapes = require('ansi-escapes'); import { isPathToPackageFile } from '../lib/detect'; import { updateCheck } from '../lib/updater'; -import { MissingTargetFileError, FileFlagBadInputError } from '../lib/errors'; -import { UnsupportedOptionCombinationError } from '../lib/errors/unsupported-option-combination-error'; +import { + MissingTargetFileError, + FileFlagBadInputError, + OptionMissingErrorError, + UnsupportedOptionCombinationError, + ExcludeFlagBadInputError, +} from '../lib/errors'; import stripAnsi from 'strip-ansi'; +import { ExcludeFlagInvalidInputError } from '../lib/errors/exclude-flag-invalid-input'; const debug = Debug('snyk'); const EXIT_CODES = { @@ -183,6 +190,18 @@ async function main() { ]); } + if (args.options.exclude) { + if (typeof args.options.exclude !== 'string') { + throw new ExcludeFlagBadInputError(); + } + if (!args.options.allProjects) { + throw new OptionMissingErrorError('--exclude', '--all-projects'); + } + if (args.options.exclude.indexOf(pathLib.sep) > -1) { + throw new ExcludeFlagInvalidInputError(); + } + } + if ( args.options.file && typeof args.options.file === 'string' && diff --git a/src/lib/errors/exclude-flag-bad-input.ts b/src/lib/errors/exclude-flag-bad-input.ts new file mode 100644 index 0000000000..8e1f610a29 --- /dev/null +++ b/src/lib/errors/exclude-flag-bad-input.ts @@ -0,0 +1,13 @@ +import { CustomError } from './custom-error'; + +export class ExcludeFlagBadInputError extends CustomError { + private static ERROR_CODE = 422; + private static ERROR_MESSAGE = + 'Empty --exclude argument. Did you mean --exclude=subdirectory ?'; + + constructor() { + super(ExcludeFlagBadInputError.ERROR_MESSAGE); + this.code = ExcludeFlagBadInputError.ERROR_CODE; + this.userMessage = ExcludeFlagBadInputError.ERROR_MESSAGE; + } +} diff --git a/src/lib/errors/exclude-flag-invalid-input.ts b/src/lib/errors/exclude-flag-invalid-input.ts new file mode 100644 index 0000000000..6fb17f1211 --- /dev/null +++ b/src/lib/errors/exclude-flag-invalid-input.ts @@ -0,0 +1,13 @@ +import { CustomError } from './custom-error'; + +export class ExcludeFlagInvalidInputError extends CustomError { + private static ERROR_CODE = 422; + private static ERROR_MESSAGE = + 'The --exclude argument must be a comma seperated list of directory names and cannot contain a path.'; + + constructor() { + super(ExcludeFlagInvalidInputError.ERROR_MESSAGE); + this.code = ExcludeFlagInvalidInputError.ERROR_CODE; + this.userMessage = ExcludeFlagInvalidInputError.ERROR_MESSAGE; + } +} diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts index 0fd95a1e34..79838fe569 100644 --- a/src/lib/errors/index.ts +++ b/src/lib/errors/index.ts @@ -14,3 +14,6 @@ export { UnsupportedPackageManagerError } from './unsupported-package-manager-er export { FailedToRunTestError } from './failed-to-run-test-error'; export { TooManyVulnPaths } from './too-many-vuln-paths'; export { AuthFailedError } from './authentication-failed-error'; +export { OptionMissingErrorError } from './option-missing-error'; +export { ExcludeFlagBadInputError } from './exclude-flag-bad-input'; +export { UnsupportedOptionCombinationError } from './unsupported-option-combination-error'; diff --git a/src/lib/errors/option-missing-error.ts b/src/lib/errors/option-missing-error.ts new file mode 100644 index 0000000000..42df88eee6 --- /dev/null +++ b/src/lib/errors/option-missing-error.ts @@ -0,0 +1,10 @@ +import { CustomError } from './custom-error'; + +export class OptionMissingErrorError extends CustomError { + constructor(option: string, required: string) { + const msg = `The ${option} option can only be use in combination with ${required}.`; + super(msg); + this.code = 422; + this.userMessage = msg; + } +} diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index 5a68af5de9..d1bae40939 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -25,7 +25,13 @@ export async function getDepsFromPlugin( if (options.allProjects) { const levelsDeep = options.detectionDepth || 1; // default to 1 level deep - const targetFiles = await find(root, [], AUTO_DETECTABLE_FILES, levelsDeep); + const ignore = options.exclude ? options.exclude.split(',') : []; + const targetFiles = await find( + root, + ignore, + AUTO_DETECTABLE_FILES, + levelsDeep, + ); debug( `auto detect manifest files, found ${targetFiles.length}`, targetFiles, @@ -41,6 +47,7 @@ export async function getDepsFromPlugin( detectPackageManagerFromFile(file), ), levelsDeep, + ignore, }; analytics.add('allProjects', analyticData); return inspectRes; diff --git a/src/lib/types.ts b/src/lib/types.ts index 881afa0eed..181e6261c0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -65,6 +65,7 @@ export interface Options { scanAllUnmanaged?: boolean; allProjects?: boolean; detectionDepth?: number; + exclude?: string; } // TODO(kyegupov): catch accessing ['undefined-properties'] via noImplicitAny diff --git a/test/acceptance/cli-args.test.ts b/test/acceptance/cli-args.test.ts index f1af140b16..9751de961a 100644 --- a/test/acceptance/cli-args.test.ts +++ b/test/acceptance/cli-args.test.ts @@ -97,7 +97,7 @@ const argsNotAllowedWithAllProjects = [ ]; argsNotAllowedWithAllProjects.forEach((arg) => { - test(`using --${arg} and --all-projects throws exception`, (t) => { + test(`using --${arg} and --all-projects displays error message`, (t) => { t.plan(2); exec(`node ${main} test --${arg} --all-projects`, (err, stdout) => { if (err) { @@ -121,3 +121,46 @@ argsNotAllowedWithAllProjects.forEach((arg) => { }); }); }); + +test('`test --exclude without --all-project displays error message`', (t) => { + t.plan(1); + exec(`node ${main} test --exclude=test`, (err, stdout) => { + if (err) { + throw err; + } + t.equals( + stdout.trim(), + 'The --exclude option can only be use in combination with --all-projects.', + ); + }); +}); + +test('`test --exclude without any value displays error message`', (t) => { + t.plan(1); + exec(`node ${main} test --all-projects --exclude`, (err, stdout) => { + if (err) { + throw err; + } + t.equals( + stdout.trim(), + 'Empty --exclude argument. Did you mean --exclude=subdirectory ?', + ); + }); +}); + +test('`test --exclude=path/to/dir displays error message`', (t) => { + t.plan(1); + const exclude = 'path/to/dir'.replace(/\//g, sep); + exec( + `node ${main} test --all-projects --exclude=${exclude}`, + (err, stdout) => { + if (err) { + throw err; + } + t.equals( + stdout.trim(), + 'The --exclude argument must be a comma seperated list of directory names and cannot contain a path.', + ); + }, + ); +}); diff --git a/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts b/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts index b39875f5ba..8bba3ade7a 100644 --- a/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts +++ b/test/acceptance/cli-monitor/cli-monitor.all-projects.spec.ts @@ -80,7 +80,6 @@ export const AllProjectsTests: AcceptanceTests = { 'calls maven plugin twice', ); // maven - console.log(result); t.match(result, 'maven/some/project-id', 'maven project was monitored '); const requests = params.server.popRequests(2); @@ -250,5 +249,39 @@ export const AllProjectsTests: AcceptanceTests = { t.fail('should have passed', error); } }, + '`monitor maven-multi-app --all-projects --detection-depth=2 --exclude=simple-child`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); + t.teardown(spyPlugin.restore); + const result = await params.cli.monitor('maven-multi-app', { + allProjects: true, + detectionDepth: 2, + exclude: 'simple-child', + }); + t.ok( + spyPlugin.withArgs('rubygems').notCalled, + 'did not call rubygems plugin', + ); + t.ok(spyPlugin.withArgs('npm').notCalled, 'did not call npm plugin'); + t.equals( + spyPlugin.withArgs('maven').callCount, + 1, + 'calls maven plugin once, excluding simple-child', + ); + t.match(result, 'maven/some/project-id', 'maven project was monitored '); + const request = params.server.popRequest(); + t.ok(request, 'Monitor POST request'); + t.match(request.url, '/monitor/', 'puts at correct url'); + t.notOk(request.body.targetFile, "doesn't send the targetFile"); + t.equal(request.method, 'PUT', 'makes PUT request'); + t.equal( + request.headers['x-snyk-cli-version'], + params.versionNumber, + 'sends version number', + ); + }, }, }; diff --git a/test/acceptance/cli-test/cli-test.all-projects.spec.ts b/test/acceptance/cli-test/cli-test.all-projects.spec.ts index 01541106ee..6c46663211 100644 --- a/test/acceptance/cli-test/cli-test.all-projects.spec.ts +++ b/test/acceptance/cli-test/cli-test.all-projects.spec.ts @@ -336,6 +336,35 @@ export const AllProjectsTests: AcceptanceTests = { } }, + '`test large-mono-repo with --all-projects, --detection-depth=7 and --exclude=bundler-app,maven-project-1`': ( + params, + utils, + ) => async (t) => { + utils.chdirWorkspaces(); + const spyPlugin = sinon.spy(params.plugins, 'loadPlugin'); + t.teardown(spyPlugin.restore); + await params.cli.test('large-mono-repo', { + allProjects: true, + detectionDepth: 7, + exclude: 'bundler-app,maven-project-1', + }); + t.equals( + spyPlugin.withArgs('rubygems').callCount, + 0, + 'does not call rubygems', + ); + t.equals( + spyPlugin.withArgs('npm').callCount, + 19, + 'calls npm plugin 19 times', + ); + t.equals( + spyPlugin.withArgs('maven').callCount, + 1, + 'calls maven plugin once, excluding the rest', + ); + }, + '`test empty --all-projects`': (params, utils) => async (t) => { utils.chdirWorkspaces(); try {