diff --git a/.changeset/little-roses-rule.md b/.changeset/little-roses-rule.md new file mode 100644 index 0000000000..4acec131e6 --- /dev/null +++ b/.changeset/little-roses-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added a new `repo test` command. diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 4b72748c40..b5d9421bdf 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -390,6 +390,7 @@ Commands: build [options] lint [options] clean + test [options] help [command] ``` @@ -425,6 +426,115 @@ Options: -h, --help ``` +### `backstage-cli repo test` + +``` +Usage: backstage-cli [--config=] [TestPathPattern] + +Options: + -h, --help + --version + --all + --automock + -b, --bail + --cache + --cacheDirectory + --changedFilesWithAncestor + --changedSince + --ci + --clearCache + --clearMocks + --collectCoverage + --collectCoverageFrom + --color + --colors + -c, --config + --coverage + --coverageDirectory + --coveragePathIgnorePatterns + --coverageProvider + --coverageReporters + --coverageThreshold + --debug + --detectLeaks + --detectOpenHandles + --env + --errorOnDeprecated + -e, --expand + --filter + --findRelatedTests + --forceExit + --globalSetup + --globalTeardown + --globals + --haste + --ignoreProjects + --init + --injectGlobals + --json + --lastCommit + --listTests + --logHeapUsage + --maxConcurrency + -w, --maxWorkers + --moduleDirectories + --moduleFileExtensions + --moduleNameMapper + --modulePathIgnorePatterns + --modulePaths + --noStackTrace + --notify + --notifyMode + -o, --onlyChanged + -f, --onlyFailures + --outputFile + --passWithNoTests + --preset + --prettierPath + --projects + --reporters + --resetMocks + --resetModules + --resolver + --restoreMocks + --rootDir + --roots + -i, --runInBand + --runTestsByPath + --runner + --selectProjects + --setupFiles + --setupFilesAfterEnv + --shard + --showConfig + --silent + --skipFilter + --snapshotSerializers + --testEnvironment + --testEnvironmentOptions + --testFailureExitCode + --testLocationInResults + --testMatch + -t, --testNamePattern + --testPathIgnorePatterns + --testPathPattern + --testRegex + --testResultsProcessor + --testRunner + --testSequencer + --testTimeout + --transform + --transformIgnorePatterns + --unmockedModulePathPatterns + -u, --updateSnapshot + --useStderr + --verbose + --watch + --watchAll + --watchPathIgnorePatterns + --watchman +``` + ### `backstage-cli test` ``` diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 8f3317c437..dfa53096bd 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -72,6 +72,17 @@ export function registerRepoCommand(program: Command) { .action( lazy(() => import('./repo/list-deprecations').then(m => m.command)), ); + + command + .command('test') + .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args + .option( + '--since ', + 'Only test packages that changed since the specified ref', + ) + .helpOption(', --backstage-cli-help') // Let Jest handle help + .description('Run tests, forwarding args to Jest, defaulting to watch mode') + .action(lazy(() => import('./repo/test').then(m => m.command))); } export function registerScriptCommand(program: Command) { diff --git a/packages/cli/src/commands/repo/test.ts b/packages/cli/src/commands/repo/test.ts new file mode 100644 index 0000000000..0726d10777 --- /dev/null +++ b/packages/cli/src/commands/repo/test.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command, OptionValues } from 'commander'; +import { PackageGraph } from '../../lib/monorepo'; +import { paths } from '../../lib/paths'; +import { runCheck } from '../../lib/run'; + +function includesAnyOf(hayStack: string[], ...needles: string[]) { + for (const needle of needles) { + if (hayStack.includes(needle)) { + return true; + } + } + return false; +} + +function removeOptionArg(args: string[], option: string) { + let changed = false; + do { + changed = false; + + const index = args.indexOf(option); + if (index >= 0) { + changed = true; + args.splice(index, 2); + } + const indexEq = args.findIndex(arg => arg.startsWith(`${option}=`)); + if (indexEq >= 0) { + changed = true; + args.splice(indexEq, 1); + } + } while (changed); +} + +export async function command(opts: OptionValues, cmd: Command): Promise { + // all args are forwarded to jest + let parent = cmd; + while (parent.parent) { + parent = parent.parent; + } + const allArgs = parent.args as string[]; + const args = allArgs.slice(allArgs.indexOf('test') + 1); + + // Only include our config if caller isn't passing their own config + if (!includesAnyOf(args, '-c', '--config')) { + args.push('--config', paths.resolveOwn('config/jest.js')); + } + + if (!includesAnyOf(args, '--no-passWithNoTests', '--passWithNoTests=false')) { + args.push('--passWithNoTests'); + } + + // Run in watch mode unless in CI, coverage mode, or running all tests + if ( + !process.env.CI && + !args.includes('--coverage') && + // explicitly no watching + !includesAnyOf(args, '--no-watch', '--watch=false', '--watchAll=false') && + // already watching + !includesAnyOf(args, '--watch', '--watchAll') + ) { + const isGitRepo = () => + runCheck('git', 'rev-parse', '--is-inside-work-tree'); + const isMercurialRepo = () => runCheck('hg', '--cwd', '.', 'root'); + + if ((await isGitRepo()) || (await isMercurialRepo())) { + args.push('--watch'); + } else { + args.push('--watchAll'); + } + } + + if (opts.since) { + removeOptionArg(args, '--since'); + } + + if (opts.since && !args.some(arg => arg.startsWith('--selectProjects'))) { + const packages = await PackageGraph.listTargetPackages(); + const graph = PackageGraph.fromPackages(packages); + const changedPackages = await graph.listChangedPackages({ + ref: opts.since, + }); + + const packageNames = Array.from( + graph.collectPackageNames( + changedPackages.map(pkg => pkg.name), + pkg => pkg.allLocalDependents.keys(), + ), + ); + args.push('--selectProjects', ...packageNames); + } + + // This is the only thing that is not implemented by jest.run(), so we do it here instead + // https://github.com/facebook/jest/blob/cd8828f7bbec6e55b4df5e41e853a5133c4a3ee1/packages/jest-cli/bin/jest.js#L12 + if (!process.env.NODE_ENV) { + (process.env as any).NODE_ENV = 'test'; + } + + // This is to have a consistent timezone for when running tests that involve checking + // the formatting of date/times. + // https://stackoverflow.com/questions/56261381/how-do-i-set-a-timezone-in-my-jest-config + if (!process.env.TZ) { + process.env.TZ = 'UTC'; + } + + // This ensures that the process doesn't exit too early before stdout is flushed + if (args.includes('--help')) { + (process.stdout as any)._handle.setBlocking(true); + } + + await require('jest').run(args); +} diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index ec1bf8a7a0..b55919b2ce 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -78,6 +78,7 @@ export default async (_opts: OptionValues, cmd: Command) => { process.env.TZ = 'UTC'; } + // This ensures that the process doesn't exit too early before stdout is flushed if (args.includes('--help')) { (process.stdout as any)._handle.setBlocking(true); }