diff --git a/e2e/ci-e2e/mocks/setup.ts b/e2e/ci-e2e/mocks/setup.ts index 70d47a192..c1598d99e 100644 --- a/e2e/ci-e2e/mocks/setup.ts +++ b/e2e/ci-e2e/mocks/setup.ts @@ -3,12 +3,12 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { simpleGit } from 'simple-git'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, initGitRepo, simulateGitFetch, + teardownTestFolder, } from '@code-pushup/test-utils'; export type TestRepo = Awaited>; diff --git a/e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap b/e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap index 670d2e5ec..dcea5545a 100644 --- a/e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap +++ b/e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap @@ -21,7 +21,8 @@ Global Options: --progress Show progress bar in stdout. [boolean] [default: true] --verbose When true creates more verbose output. This is helpful w - hen debugging. [boolean] [default: false] + hen debugging. You may also set CP_VERBOSE env variable + instead. [boolean] [default: false] --config Path to config file. By default it loads code-pushup.con fig.(ts|mjs|js). [string] --tsconfig Path to a TypeScript config, to be used when loading con @@ -65,9 +66,9 @@ Examples: code-pushup collect --onlyPlugins=covera Run collect with only coverage plugi ge n, other plugins from config file wi ll be skipped. - code-pushup collect --skipPlugins=covera Run collect skiping the coverage plu - ge gin, other plugins from config file - will be included. + code-pushup collect --skipPlugins=covera Run collect skipping the coverage pl + ge ugin, other plugins from config file + will be included. code-pushup upload --persist.outputDir=d Upload dist/report.json to portal us ist --upload.apiKey=$CP_API_KEY ing API key from environment variabl e diff --git a/e2e/cli-e2e/tests/collect.e2e.test.ts b/e2e/cli-e2e/tests/collect.e2e.test.ts index efa768b3d..b29f26471 100644 --- a/e2e/cli-e2e/tests/collect.e2e.test.ts +++ b/e2e/cli-e2e/tests/collect.e2e.test.ts @@ -2,8 +2,11 @@ import { cp } from 'node:fs/promises'; import path from 'node:path'; import { afterEach, beforeAll, describe, expect, it } from 'vitest'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; -import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR } from '@code-pushup/test-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + teardownTestFolder, +} from '@code-pushup/test-utils'; import { executeProcess, readTextFile } from '@code-pushup/utils'; describe('CLI collect', () => { diff --git a/e2e/cli-e2e/tests/compare.e2e.test.ts b/e2e/cli-e2e/tests/compare.e2e.test.ts index 0d1140ebc..d7510d7f9 100644 --- a/e2e/cli-e2e/tests/compare.e2e.test.ts +++ b/e2e/cli-e2e/tests/compare.e2e.test.ts @@ -3,8 +3,11 @@ import path from 'node:path'; import { beforeAll } from 'vitest'; import type { ReportsDiff } from '@code-pushup/models'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; -import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR } from '@code-pushup/test-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + teardownTestFolder, +} from '@code-pushup/test-utils'; import { executeProcess, readJsonFile, readTextFile } from '@code-pushup/utils'; describe('CLI compare', () => { diff --git a/e2e/cli-e2e/tests/help.e2e.test.ts b/e2e/cli-e2e/tests/help.e2e.test.ts index 6382952be..182cc9671 100644 --- a/e2e/cli-e2e/tests/help.e2e.test.ts +++ b/e2e/cli-e2e/tests/help.e2e.test.ts @@ -23,6 +23,7 @@ describe('CLI help', () => { const helpArgResult = await executeProcess({ command: 'npx', args: ['@code-pushup/cli', 'help'], + cwd: envRoot, }); const helpCommandResult = await executeProcess({ command: 'npx', diff --git a/e2e/cli-e2e/tests/print-config.e2e.test.ts b/e2e/cli-e2e/tests/print-config.e2e.test.ts index e29230721..ad021d9a7 100644 --- a/e2e/cli-e2e/tests/print-config.e2e.test.ts +++ b/e2e/cli-e2e/tests/print-config.e2e.test.ts @@ -2,8 +2,11 @@ import { cp } from 'node:fs/promises'; import path from 'node:path'; import { beforeAll, expect } from 'vitest'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; -import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR } from '@code-pushup/test-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + teardownTestFolder, +} from '@code-pushup/test-utils'; import { executeProcess } from '@code-pushup/utils'; describe('CLI print-config', () => { diff --git a/e2e/create-cli-e2e/tests/init.e2e.test.ts b/e2e/create-cli-e2e/tests/init.e2e.test.ts index d4b7742ac..a9752ef9c 100644 --- a/e2e/create-cli-e2e/tests/init.e2e.test.ts +++ b/e2e/create-cli-e2e/tests/init.e2e.test.ts @@ -1,12 +1,12 @@ import path from 'node:path'; import { afterEach, expect } from 'vitest'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, createNpmWorkspace, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readJsonFile, readTextFile } from '@code-pushup/utils'; diff --git a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts index 3c1ff2c8e..2a3797492 100644 --- a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts @@ -11,11 +11,11 @@ import { materializeTree, nxTargetProject, } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readJsonFile } from '@code-pushup/utils'; import { INLINE_PLUGIN } from './inline-plugin.js'; diff --git a/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts b/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts index 6237a9212..27b978771 100644 --- a/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts @@ -8,11 +8,11 @@ import { materializeTree, nxTargetProject, } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess } from '@code-pushup/utils'; diff --git a/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts b/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts index c3ea4c895..3ad4b7645 100644 --- a/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts @@ -7,11 +7,11 @@ import { materializeTree, nxTargetProject, } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess } from '@code-pushup/utils'; diff --git a/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts index 9ded9aab0..11db54c22 100644 --- a/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts @@ -10,11 +10,11 @@ import { nxTargetProject, registerPluginInWorkspace, } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readTextFile } from '@code-pushup/utils'; import { INLINE_PLUGIN } from './inline-plugin.js'; diff --git a/e2e/plugin-coverage-e2e/tests/collect.e2e.test.ts b/e2e/plugin-coverage-e2e/tests/collect.e2e.test.ts index 09bc75e96..0067bc2a9 100644 --- a/e2e/plugin-coverage-e2e/tests/collect.e2e.test.ts +++ b/e2e/plugin-coverage-e2e/tests/collect.e2e.test.ts @@ -3,11 +3,11 @@ import path from 'node:path'; import { afterAll, afterEach, beforeAll } from 'vitest'; import { type Report, reportSchema } from '@code-pushup/models'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, omitVariableReportData, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readJsonFile } from '@code-pushup/utils'; diff --git a/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts b/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts index 36e6dcd5e..98966c6b8 100644 --- a/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts +++ b/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts @@ -3,11 +3,11 @@ import path from 'node:path'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { type Report, reportSchema } from '@code-pushup/models'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, omitVariableReportData, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readJsonFile } from '@code-pushup/utils'; diff --git a/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts b/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts index d7ed8594a..47981967f 100644 --- a/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts +++ b/e2e/plugin-jsdocs-e2e/tests/collect.e2e.test.ts @@ -3,12 +3,12 @@ import path from 'node:path'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { type Report, reportSchema } from '@code-pushup/models'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, omitVariableReportData, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readJsonFile } from '@code-pushup/utils'; diff --git a/e2e/plugin-lighthouse-e2e/tests/collect.e2e.test.ts b/e2e/plugin-lighthouse-e2e/tests/collect.e2e.test.ts index 0614fcf22..e1c255e70 100644 --- a/e2e/plugin-lighthouse-e2e/tests/collect.e2e.test.ts +++ b/e2e/plugin-lighthouse-e2e/tests/collect.e2e.test.ts @@ -3,12 +3,12 @@ import path from 'node:path'; import { afterAll, beforeAll, expect } from 'vitest'; import { type Report, reportSchema } from '@code-pushup/models'; import { nxTargetProject } from '@code-pushup/test-nx-utils'; -import { teardownTestFolder } from '@code-pushup/test-setup'; import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, omitVariableReportData, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readJsonFile } from '@code-pushup/utils'; diff --git a/packages/ci/src/lib/run.integration.test.ts b/packages/ci/src/lib/run.integration.test.ts index 068c93e86..4a0d2a2f7 100644 --- a/packages/ci/src/lib/run.integration.test.ts +++ b/packages/ci/src/lib/run.integration.test.ts @@ -11,8 +11,12 @@ import { fileURLToPath } from 'node:url'; import { type SimpleGit, simpleGit } from 'simple-git'; import type { MockInstance } from 'vitest'; import type { CoreConfig } from '@code-pushup/models'; -import { cleanTestFolder, teardownTestFolder } from '@code-pushup/test-setup'; -import { initGitRepo, simulateGitFetch } from '@code-pushup/test-utils'; +import { + cleanTestFolder, + initGitRepo, + simulateGitFetch, + teardownTestFolder, +} from '@code-pushup/test-utils'; import * as utils from '@code-pushup/utils'; import type { Comment, diff --git a/packages/cli/README.md b/packages/cli/README.md index d68c65d75..a1e0ecfe6 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -195,12 +195,12 @@ Each example is fully tested to demonstrate best practices for plugin testing as ### Global Options -| Option | Type | Default | Description | -| ---------------- | --------- | -------------------------------------------- | ---------------------------------------------------------------------- | -| **`--progress`** | `boolean` | `true` | Show progress bar in stdout. | -| **`--verbose`** | `boolean` | `false` | When true creates more verbose output. This is helpful when debugging. | -| **`--config`** | `string` | looks for `code-pushup.config.{ts\|mjs\|js}` | Path to config file. | -| **`--tsconfig`** | `string` | n/a | Path to a TypeScript config, used to load config file. | +| Option | Type | Default | Description | +| ---------------- | --------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| **`--progress`** | `boolean` | `true` | Show progress bar in stdout. | +| **`--verbose`** | `boolean` | `false` | When true creates more verbose output. This is helpful when debugging. You may also set `CP_VERBOSE` env variable instead. | +| **`--config`** | `string` | looks for `code-pushup.config.{ts\|mjs\|js}` | Path to config file. | +| **`--tsconfig`** | `string` | n/a | Path to a TypeScript config, used to load config file. | > [!NOTE] > By default, the CLI loads `code-pushup.config.(ts|mjs|js)` if no config path is provided with `--config`. diff --git a/packages/cli/src/lib/cli.ts b/packages/cli/src/lib/cli.ts index 50eef57be..0faa8b2d2 100644 --- a/packages/cli/src/lib/cli.ts +++ b/packages/cli/src/lib/cli.ts @@ -25,7 +25,7 @@ export const cli = (args: string[]) => ], [ 'code-pushup collect --skipPlugins=coverage', - 'Run collect skiping the coverage plugin, other plugins from config file will be included.', + 'Run collect skipping the coverage plugin, other plugins from config file will be included.', ], [ 'code-pushup upload --persist.outputDir=dist --upload.apiKey=$CP_API_KEY', diff --git a/packages/cli/src/lib/implementation/core-config.middleware.integration.test.ts b/packages/cli/src/lib/implementation/core-config.middleware.integration.test.ts index 8ea4bb283..99288a834 100644 --- a/packages/cli/src/lib/implementation/core-config.middleware.integration.test.ts +++ b/packages/cli/src/lib/implementation/core-config.middleware.integration.test.ts @@ -21,7 +21,7 @@ const configDirPath = path.join( describe('coreConfigMiddleware', () => { const CLI_DEFAULTS = { progress: true, - verbose: false, + plugins: [], onlyPlugins: [], skipPlugins: [], }; diff --git a/packages/cli/src/lib/implementation/core-config.model.ts b/packages/cli/src/lib/implementation/core-config.model.ts index 612c071cc..ed011da56 100644 --- a/packages/cli/src/lib/implementation/core-config.model.ts +++ b/packages/cli/src/lib/implementation/core-config.model.ts @@ -16,6 +16,7 @@ export type UploadConfigCliOptions = { export type ConfigCliOptions = { config?: string; tsconfig?: string; + verbose?: string; }; export type CoreConfigCliOptions = Pick & { diff --git a/packages/cli/src/lib/implementation/filter.middleware.ts b/packages/cli/src/lib/implementation/filter.middleware.ts index b0c1349b5..1559de69b 100644 --- a/packages/cli/src/lib/implementation/filter.middleware.ts +++ b/packages/cli/src/lib/implementation/filter.middleware.ts @@ -30,7 +30,6 @@ export function filterMiddleware( onlyCategories = [], skipPlugins = [], onlyPlugins = [], - verbose = false, } = originalProcessArgs; const plugins = filterSkippedInPlugins(rcPlugins); @@ -43,7 +42,7 @@ export function filterMiddleware( onlyPlugins.length === 0 ) { if (rcCategories && categories) { - validateSkippedCategories(rcCategories, categories, verbose); + validateSkippedCategories(rcCategories, categories); } return { ...originalProcessArgs, @@ -61,12 +60,12 @@ export function filterMiddleware( const filteredCategories = applyCategoryFilters( { categories, plugins }, skippedCategories, - { skipCategories, onlyCategories, verbose }, + { skipCategories, onlyCategories }, ); const filteredPlugins = applyPluginFilters( { categories: filteredCategories, plugins }, skippedPlugins, - { skipPlugins, onlyPlugins, verbose }, + { skipPlugins, onlyPlugins }, ); const finalCategories = filteredCategories ? filterItemRefsBy(filteredCategories, ref => @@ -89,9 +88,9 @@ export function filterMiddleware( function applyCategoryFilters( { categories, plugins }: Filterables, skippedCategories: string[], - options: Pick, + options: Pick, ): CoreConfig['categories'] { - const { skipCategories = [], onlyCategories = [], verbose = false } = options; + const { skipCategories = [], onlyCategories = [] } = options; if ( (skipCategories.length === 0 && onlyCategories.length === 0) || ((!categories || categories.length === 0) && skippedCategories.length === 0) @@ -101,12 +100,12 @@ function applyCategoryFilters( validateFilterOption( 'skipCategories', { plugins, categories }, - { itemsToFilter: skipCategories, skippedItems: skippedCategories, verbose }, + { itemsToFilter: skipCategories, skippedItems: skippedCategories }, ); validateFilterOption( 'onlyCategories', { plugins, categories }, - { itemsToFilter: onlyCategories, skippedItems: skippedCategories, verbose }, + { itemsToFilter: onlyCategories, skippedItems: skippedCategories }, ); return applyFilters(categories ?? [], skipCategories, onlyCategories, 'slug'); } @@ -114,9 +113,9 @@ function applyCategoryFilters( function applyPluginFilters( { categories, plugins }: Filterables, skippedPlugins: string[], - options: Pick, + options: Pick, ): CoreConfig['plugins'] { - const { skipPlugins = [], onlyPlugins = [], verbose = false } = options; + const { skipPlugins = [], onlyPlugins = [] } = options; const filteredPlugins = filterPluginsFromCategories({ categories, plugins, @@ -127,12 +126,12 @@ function applyPluginFilters( validateFilterOption( 'skipPlugins', { plugins: filteredPlugins, categories }, - { itemsToFilter: skipPlugins, skippedItems: skippedPlugins, verbose }, + { itemsToFilter: skipPlugins, skippedItems: skippedPlugins }, ); validateFilterOption( 'onlyPlugins', { plugins: filteredPlugins, categories }, - { itemsToFilter: onlyPlugins, skippedItems: skippedPlugins, verbose }, + { itemsToFilter: onlyPlugins, skippedItems: skippedPlugins }, ); return applyFilters(filteredPlugins, skipPlugins, onlyPlugins, 'slug'); } diff --git a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts index d3088f52d..4babe4dc3 100644 --- a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts +++ b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts @@ -291,6 +291,8 @@ describe('filterMiddleware', () => { ); it('should trigger verbose logging when skipPlugins or onlyPlugins removes categories', () => { + vi.stubEnv('CP_VERBOSE', 'true'); + filterMiddleware({ onlyPlugins: ['p1'], skipPlugins: ['p2'], @@ -311,7 +313,6 @@ describe('filterMiddleware', () => { refs: [{ type: 'audit', plugin: 'p2', slug: 'a1-p2', weight: 1 }], }, ] as CategoryConfig[], - verbose: true, }); expect(ui()).toHaveNthLogged( diff --git a/packages/cli/src/lib/implementation/global.options.ts b/packages/cli/src/lib/implementation/global.options.ts index 9cbf4c8df..d541bdc7a 100644 --- a/packages/cli/src/lib/implementation/global.options.ts +++ b/packages/cli/src/lib/implementation/global.options.ts @@ -13,7 +13,7 @@ export function yargsGlobalOptionsDefinition(): Record< }, verbose: { describe: - 'When true creates more verbose output. This is helpful when debugging.', + 'When true creates more verbose output. This is helpful when debugging. You may also set CP_VERBOSE env variable instead.', type: 'boolean', default: false, }, diff --git a/packages/cli/src/lib/implementation/set-verbose.middleware.integration.test.ts b/packages/cli/src/lib/implementation/set-verbose.middleware.integration.test.ts new file mode 100644 index 000000000..4918f6cc4 --- /dev/null +++ b/packages/cli/src/lib/implementation/set-verbose.middleware.integration.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from 'vitest'; +import { isVerbose } from '@code-pushup/utils'; +import { setVerboseMiddleware } from './set-verbose.middleware.js'; + +describe('setVerboseMiddleware', () => { + it.each([ + [true, undefined, true], + [false, undefined, false], + [undefined, undefined, false], + [undefined, true, true], + [true, true, true], + [false, true, true], + [true, false, false], + [false, false, false], + ['True', undefined, true], + ['TRUE', undefined, true], + [42, undefined, false], + [undefined, 'true', true], + [true, 'False', false], + ])( + 'should set CP_VERBOSE based on env variable `%j` and cli argument `%j` and return `%j` from isVerbose() function', + (envValue, cliFlag, expected) => { + vi.stubEnv('CP_VERBOSE', `${envValue}`); + + setVerboseMiddleware({ verbose: cliFlag } as any); + expect(process.env['CP_VERBOSE']).toBe(`${expected}`); + expect(isVerbose()).toBe(expected); + }, + ); +}); diff --git a/packages/cli/src/lib/implementation/set-verbose.middleware.ts b/packages/cli/src/lib/implementation/set-verbose.middleware.ts new file mode 100644 index 000000000..95260d255 --- /dev/null +++ b/packages/cli/src/lib/implementation/set-verbose.middleware.ts @@ -0,0 +1,52 @@ +import type { GlobalOptions } from '@code-pushup/core'; +import type { CoreConfig } from '@code-pushup/models'; +import type { FilterOptions } from './filter.model.js'; +import type { GeneralCliOptions } from './global.model'; + +/** + * + * | CP_VERBOSE value | CLI `--verbose` flag | Effect | + * |------------------|-----------------------------|------------| + * | true | Not provided | enabled | + * | false | Not provided | disabled | + * | Not provided | Not provided | disabled | + * | Not provided | Explicitly set (true) | enabled | + * | true | Explicitly set (true) | enabled | + * | false | Explicitly set (true) | enabled | + * | true | Explicitly negated (false) | disabled | + * | false | Explicitly negated (false) | disabled | + * + * @param originalProcessArgs + */ +export function setVerboseMiddleware< + T extends GeneralCliOptions & CoreConfig & FilterOptions & GlobalOptions, +>(originalProcessArgs: T): T { + const envVerbose = getNormalizedOptionValue(process.env['CP_VERBOSE']); + const cliVerbose = getNormalizedOptionValue(originalProcessArgs.verbose); + const verboseEffect = cliVerbose ?? envVerbose ?? false; + + // eslint-disable-next-line functional/immutable-data + process.env['CP_VERBOSE'] = `${verboseEffect}`; + + return { + ...originalProcessArgs, + verbose: verboseEffect, + }; +} + +export function getNormalizedOptionValue(value: unknown): boolean | undefined { + if (typeof value === 'boolean') { + return value; + } + if (typeof value === 'string') { + const lowerCaseValue = value.toLowerCase(); + if (lowerCaseValue === 'true') { + return true; + } + if (lowerCaseValue === 'false') { + return false; + } + } + + return undefined; +} diff --git a/packages/cli/src/lib/implementation/set-verbose.middleware.unit.test.ts b/packages/cli/src/lib/implementation/set-verbose.middleware.unit.test.ts new file mode 100644 index 000000000..440a7c508 --- /dev/null +++ b/packages/cli/src/lib/implementation/set-verbose.middleware.unit.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + getNormalizedOptionValue, + setVerboseMiddleware, +} from './set-verbose.middleware.js'; + +describe('setVerboseMiddleware', () => { + it.each([ + [true, true], + ['true', true], + ['True', true], + ['TRUE', true], + [false, false], + ['false', false], + ['False', false], + ['FALSE', false], + [undefined, undefined], + ['undefined', undefined], + ['Whatever else', undefined], + [0, undefined], + [1, undefined], + [null, undefined], + ])( + 'should return normalize value of `%j` option set as `%j`', + (value, expected) => { + expect(getNormalizedOptionValue(value)).toBe(expected); + }, + ); + + it.each([ + [true, undefined, true], + [false, undefined, false], + [undefined, undefined, false], + [undefined, true, true], + [true, true, true], + [false, true, true], + [true, false, false], + [false, false, false], + ])( + 'should set verbosity based on env variable `%j` and cli argument `%j` to perform verbose effect as `%j`', + (envValue, cliFlag, expected) => { + vi.stubEnv('CP_VERBOSE', `${envValue}`); + + expect(setVerboseMiddleware({ verbose: cliFlag } as any).verbose).toBe( + expected, + ); + }, + ); +}); diff --git a/packages/cli/src/lib/implementation/validate-filter-options.utils.ts b/packages/cli/src/lib/implementation/validate-filter-options.utils.ts index 60048f0a6..cf3ef9e44 100644 --- a/packages/cli/src/lib/implementation/validate-filter-options.utils.ts +++ b/packages/cli/src/lib/implementation/validate-filter-options.utils.ts @@ -2,6 +2,7 @@ import type { PluginConfig } from '@code-pushup/models'; import { capitalize, filterItemRefsBy, + isVerbose, pluralize, ui, } from '@code-pushup/utils'; @@ -16,8 +17,7 @@ export function validateFilterOption( { itemsToFilter, skippedItems, - verbose, - }: { itemsToFilter: string[]; skippedItems: string[]; verbose: boolean }, + }: { itemsToFilter: string[]; skippedItems: string[] }, ): void { const validItems = isCategoryOption(option) ? categories.map(({ slug }) => slug) @@ -51,14 +51,14 @@ export function validateFilterOption( } ui().logger.warning(message); } - if (skippedValidItems.length > 0 && verbose) { + if (skippedValidItems.length > 0 && isVerbose()) { const item = getItemType(option, skippedValidItems.length); const prefix = skippedValidItems.length === 1 ? `a skipped` : `skipped`; ui().logger.warning( `The --${option} argument references ${prefix} ${item}: ${skippedValidItems.join(', ')}.`, ); } - if (isPluginOption(option) && categories.length > 0 && verbose) { + if (isPluginOption(option) && categories.length > 0 && isVerbose()) { const removedCategories = filterItemRefsBy(categories, ({ plugin }) => isOnlyOption(option) ? !itemsToFilterSet.has(plugin) @@ -78,12 +78,11 @@ export function validateFilterOption( export function validateSkippedCategories( originalCategories: NonNullable, filteredCategories: NonNullable, - verbose: boolean, ): void { const skippedCategories = originalCategories.filter( original => !filteredCategories.some(({ slug }) => slug === original.slug), ); - if (skippedCategories.length > 0 && verbose) { + if (skippedCategories.length > 0 && isVerbose()) { skippedCategories.forEach(category => { ui().logger.info( `Category ${category.slug} was removed because all its refs were skipped. Affected refs: ${category.refs diff --git a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts index 58a72247e..ffeaa539b 100644 --- a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts +++ b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts @@ -48,7 +48,7 @@ describe('validateFilterOption', () => { { slug: 'c1', refs: [{ plugin: 'p1', slug: 'a1-p1' }] }, ] as CategoryConfig[], }, - { itemsToFilter, skippedItems: [], verbose: false }, + { itemsToFilter, skippedItems: [] }, ); expect(ui()).toHaveLogged('warn', expected); }, @@ -91,7 +91,7 @@ describe('validateFilterOption', () => { }, ] as CategoryConfig[], }, - { itemsToFilter, skippedItems: [], verbose: false }, + { itemsToFilter, skippedItems: [] }, ); expect(ui()).toHaveLogged('warn', expected); }, @@ -106,12 +106,14 @@ describe('validateFilterOption', () => { { slug: 'p2', audits: [{ slug: 'a1-p2' }] }, ] as PluginConfig[], }, - { itemsToFilter: ['p1'], skippedItems: [], verbose: false }, + { itemsToFilter: ['p1'], skippedItems: [] }, ); expect(ui()).not.toHaveLogs(); }); it('should log a category ignored as a result of plugin filtering', () => { + vi.stubEnv('CP_VERBOSE', 'true'); + validateFilterOption( 'onlyPlugins', { @@ -125,7 +127,7 @@ describe('validateFilterOption', () => { { slug: 'c3', refs: [{ plugin: 'p2' }] }, ] as CategoryConfig[], }, - { itemsToFilter: ['p1'], skippedItems: [], verbose: true }, + { itemsToFilter: ['p1'], skippedItems: [] }, ); expect(ui()).toHaveLoggedTimes(1); expect(ui()).toHaveLogged( @@ -145,7 +147,7 @@ describe('validateFilterOption', () => { { slug: 'p3', audits: [{ slug: 'a1-p3' }] }, ] as PluginConfig[], }, - { itemsToFilter: ['p4', 'p5'], skippedItems: [], verbose: false }, + { itemsToFilter: ['p4', 'p5'], skippedItems: [] }, ); }).toThrow( new OptionValidationError( @@ -164,12 +166,12 @@ describe('validateFilterOption', () => { validateFilterOption( 'skipPlugins', { plugins: allPlugins }, - { itemsToFilter: ['plugin1'], skippedItems: [], verbose: false }, + { itemsToFilter: ['plugin1'], skippedItems: [] }, ); validateFilterOption( 'onlyPlugins', { plugins: allPlugins }, - { itemsToFilter: ['plugin3'], skippedItems: [], verbose: false }, + { itemsToFilter: ['plugin3'], skippedItems: [] }, ); }).toThrow( new OptionValidationError( @@ -178,7 +180,7 @@ describe('validateFilterOption', () => { ); }); - it('should throw OptionValidationError when none of the onlyCatigories are valid', () => { + it('should throw OptionValidationError when none of the onlyCategories are valid', () => { expect(() => { validateFilterOption( 'onlyCategories', @@ -197,7 +199,7 @@ describe('validateFilterOption', () => { }, ] as CategoryConfig[], }, - { itemsToFilter: ['c2', 'c3'], skippedItems: [], verbose: false }, + { itemsToFilter: ['c2', 'c3'], skippedItems: [] }, ); }).toThrow( new OptionValidationError( @@ -207,6 +209,8 @@ describe('validateFilterOption', () => { }); it('should log skipped items if verbose mode is enabled', () => { + vi.stubEnv('CP_VERBOSE', 'true'); + const plugins = [ { slug: 'p1', audits: [{ slug: 'a1-p1' }] }, ] as PluginConfig[]; @@ -217,7 +221,7 @@ describe('validateFilterOption', () => { validateFilterOption( 'skipPlugins', { plugins, categories }, - { itemsToFilter: ['p1'], skippedItems: ['p1'], verbose: true }, + { itemsToFilter: ['p1'], skippedItems: ['p1'] }, ); expect(ui()).toHaveNthLogged( 1, @@ -450,16 +454,14 @@ describe('validateSkippedCategories', () => { ] as NonNullable; it('should log info when categories are removed', () => { - validateSkippedCategories( - categories, - [ - { - slug: 'c2', - refs: [{ type: 'audit', plugin: 'p2', slug: 'a1', weight: 1 }], - }, - ] as NonNullable, - true, - ); + vi.stubEnv('CP_VERBOSE', 'true'); + + validateSkippedCategories(categories, [ + { + slug: 'c2', + refs: [{ type: 'audit', plugin: 'p2', slug: 'a1', weight: 1 }], + }, + ] as NonNullable); expect(ui()).toHaveLogged( 'info', 'Category c1 was removed because all its refs were skipped. Affected refs: g1 (group)', @@ -468,12 +470,12 @@ describe('validateSkippedCategories', () => { it('should not log anything when categories are not removed', () => { const loggerSpy = vi.spyOn(ui().logger, 'info'); - validateSkippedCategories(categories, categories, true); + validateSkippedCategories(categories, categories); expect(loggerSpy).not.toHaveBeenCalled(); }); it('should throw an error when no categories remain after filtering', () => { - expect(() => validateSkippedCategories(categories, [], false)).toThrow( + expect(() => validateSkippedCategories(categories, [])).toThrow( new OptionValidationError( 'No categories remain after filtering. Removed categories: c1, c2', ), diff --git a/packages/cli/src/lib/middlewares.ts b/packages/cli/src/lib/middlewares.ts index b163a8085..d40df4325 100644 --- a/packages/cli/src/lib/middlewares.ts +++ b/packages/cli/src/lib/middlewares.ts @@ -1,8 +1,13 @@ import type { MiddlewareFunction } from 'yargs'; import { coreConfigMiddleware } from './implementation/core-config.middleware.js'; import { filterMiddleware } from './implementation/filter.middleware.js'; +import { setVerboseMiddleware } from './implementation/set-verbose.middleware.js'; export const middlewares = [ + { + middlewareFunction: setVerboseMiddleware as unknown as MiddlewareFunction, + applyBeforeValidation: false, + }, { middlewareFunction: coreConfigMiddleware as unknown as MiddlewareFunction, applyBeforeValidation: false, diff --git a/packages/core/src/lib/collect-and-persist.ts b/packages/core/src/lib/collect-and-persist.ts index f7aaeda49..dfa48ba76 100644 --- a/packages/core/src/lib/collect-and-persist.ts +++ b/packages/core/src/lib/collect-and-persist.ts @@ -4,10 +4,10 @@ import { pluginReportSchema, } from '@code-pushup/models'; import { + isVerbose, logStdoutSummary, scoreReport, sortReport, - verboseUtils, } from '@code-pushup/utils'; import { collect } from './implementation/collect.js'; import { @@ -24,8 +24,6 @@ export type CollectAndPersistReportsOptions = Pick< export async function collectAndPersistReports( options: CollectAndPersistReportsOptions, ): Promise { - const { exec } = verboseUtils(options.verbose); - const report = await collect(options); const sortedScoredReport = sortReport(scoreReport(report)); @@ -36,11 +34,11 @@ export async function collectAndPersistReports( ); // terminal output - logStdoutSummary(sortedScoredReport, options.verbose); + logStdoutSummary(sortedScoredReport); - exec(() => { + if (isVerbose()) { logPersistedResults(persistResults); - }); + } // validate report and throw if invalid report.plugins.forEach(plugin => { diff --git a/packages/core/src/lib/collect-and-persist.unit.test.ts b/packages/core/src/lib/collect-and-persist.unit.test.ts index 1256b9e41..1ecb5c5fe 100644 --- a/packages/core/src/lib/collect-and-persist.unit.test.ts +++ b/packages/core/src/lib/collect-and-persist.unit.test.ts @@ -6,6 +6,7 @@ import { } from '@code-pushup/test-utils'; import { type ScoredReport, + isVerbose, logStdoutSummary, scoreReport, sortReport, @@ -47,6 +48,9 @@ describe('collectAndPersistReports', () => { it('should call collect and persistReport with correct parameters in non-verbose mode', async () => { const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); + + expect(isVerbose()).toBe(false); + const nonVerboseConfig: CollectAndPersistReportsOptions = { ...MINIMAL_CONFIG_MOCK, persist: { @@ -54,7 +58,6 @@ describe('collectAndPersistReports', () => { filename: 'report', format: ['md'], }, - verbose: false, progress: false, }; await collectAndPersistReports(nonVerboseConfig); @@ -80,12 +83,15 @@ describe('collectAndPersistReports', () => { }, ); - expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport, false); + expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport); expect(logPersistedResults).not.toHaveBeenCalled(); }); it('should call collect and persistReport with correct parameters in verbose mode', async () => { const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); + + vi.stubEnv('CP_VERBOSE', 'true'); + const verboseConfig: CollectAndPersistReportsOptions = { ...MINIMAL_CONFIG_MOCK, persist: { @@ -93,7 +99,6 @@ describe('collectAndPersistReports', () => { filename: 'report', format: ['md'], }, - verbose: true, progress: false, }; await collectAndPersistReports(verboseConfig); @@ -106,7 +111,7 @@ describe('collectAndPersistReports', () => { verboseConfig.persist, ); - expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport, true); + expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport); expect(logPersistedResults).toHaveBeenCalled(); }); diff --git a/packages/core/src/lib/implementation/execute-plugin.ts b/packages/core/src/lib/implementation/execute-plugin.ts index 2d558bb71..c283aa144 100644 --- a/packages/core/src/lib/implementation/execute-plugin.ts +++ b/packages/core/src/lib/implementation/execute-plugin.ts @@ -4,7 +4,6 @@ import { type AuditOutput, type AuditOutputs, type AuditReport, - type OnProgress, type PluginConfig, type PluginReport, auditOutputsSchema, @@ -37,7 +36,6 @@ export class PluginOutputMissingAuditError extends Error { * * @public * @param pluginConfig - {@link ProcessConfig} object with runner and meta - * @param onProgress - progress handler {@link OnProgress} * @returns {Promise} - audit outputs from plugin runner * @throws {PluginOutputMissingAuditError} - if plugin runner output is invalid * @@ -56,7 +54,6 @@ export class PluginOutputMissingAuditError extends Error { */ export async function executePlugin( pluginConfig: PluginConfig, - onProgress?: OnProgress, ): Promise { const { runner, @@ -70,8 +67,8 @@ export async function executePlugin( // execute plugin runner const runnerResult = typeof runner === 'object' - ? await executeRunnerConfig(runner, onProgress) - : await executeRunnerFunction(runner, onProgress); + ? await executeRunnerConfig(runner) + : await executeRunnerFunction(runner); const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult; // validate auditOutputs diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index facb4903b..04ba10dc2 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -1,14 +1,12 @@ import path from 'node:path'; -import type { - OnProgress, - RunnerConfig, - RunnerFunction, -} from '@code-pushup/models'; +import type { RunnerConfig, RunnerFunction } from '@code-pushup/models'; import { calcDuration, executeProcess, + isVerbose, readJsonFile, removeDirectoryIfExists, + ui, } from '@code-pushup/utils'; export type RunnerResult = { @@ -19,7 +17,6 @@ export type RunnerResult = { export async function executeRunnerConfig( cfg: RunnerConfig, - onProgress?: OnProgress, ): Promise { const { args, command, outputFile, outputTransform } = cfg; @@ -27,7 +24,14 @@ export async function executeRunnerConfig( const { duration, date } = await executeProcess({ command, args, - observer: { onStdout: onProgress }, + observer: { + onStdout: stdout => { + if (isVerbose()) { + ui().logger.log(stdout); + } + }, + onStderr: stderr => ui().logger.error(stderr), + }, }); // read process output from file system and parse it @@ -48,13 +52,12 @@ export async function executeRunnerConfig( export async function executeRunnerFunction( runner: RunnerFunction, - onProgress?: OnProgress, ): Promise { const date = new Date().toISOString(); const start = performance.now(); // execute plugin runner - const audits = await runner(onProgress); + const audits = await runner(); // create runner result return { diff --git a/packages/core/src/lib/types.ts b/packages/core/src/lib/types.ts index 2bbe44368..98f1b7f52 100644 --- a/packages/core/src/lib/types.ts +++ b/packages/core/src/lib/types.ts @@ -1,6 +1,4 @@ export type GlobalOptions = { // Show progress bar in stdout progress: boolean; - // Outputs additional information for a run - verbose: boolean; }; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index b5ce207ce..8d55ca6ee 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -96,11 +96,9 @@ export { type ReportsDiff, } from './lib/reports-diff.js'; export { - onProgressSchema, runnerConfigSchema, runnerFunctionSchema, runnerFilesPathsSchema, - type OnProgress, type RunnerConfig, type RunnerFunction, type RunnerFilesPaths, diff --git a/packages/models/src/lib/runner-config.ts b/packages/models/src/lib/runner-config.ts index 7e9af7bb3..799da8063 100644 --- a/packages/models/src/lib/runner-config.ts +++ b/packages/models/src/lib/runner-config.ts @@ -26,15 +26,8 @@ export const runnerConfigSchema = z.object( export type RunnerConfig = z.infer; -export const onProgressSchema = z - .function() - .args(z.unknown()) - .returns(z.void()); -export type OnProgress = z.infer; - export const runnerFunctionSchema = z .function() - .args(onProgressSchema.optional()) .returns(z.union([auditOutputsSchema, z.promise(auditOutputsSchema)])); export type RunnerFunction = z.infer; diff --git a/packages/nx-plugin/package.json b/packages/nx-plugin/package.json index e1abb1bc4..64ff19a0d 100644 --- a/packages/nx-plugin/package.json +++ b/packages/nx-plugin/package.json @@ -35,6 +35,7 @@ "@code-pushup/models": "0.61.0", "@code-pushup/utils": "0.61.0", "@nx/devkit": "^17.0.0 || ^18.0.0 || ^19.0.0", + "ansis": "^3.3.0", "nx": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.22.4" } diff --git a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts index 416ff91ab..77a81a6f0 100644 --- a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts @@ -1,6 +1,6 @@ import { logger } from '@nx/devkit'; import { execSync } from 'node:child_process'; -import { afterEach, beforeEach, expect, vi } from 'vitest'; +import { afterEach, expect, vi } from 'vitest'; import { executorContext } from '@code-pushup/test-nx-utils'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import runAutorunExecutor from './executor.js'; @@ -19,18 +19,12 @@ vi.mock('node:child_process', async () => { }); describe('runAutorunExecutor', () => { - const envSpy = vi.spyOn(process, 'env', 'get'); const loggerInfoSpy = vi.spyOn(logger, 'info'); const loggerWarnSpy = vi.spyOn(logger, 'warn'); - beforeEach(() => { - envSpy.mockReturnValue({}); - }); - afterEach(() => { loggerWarnSpy.mockReset(); loggerInfoSpy.mockReset(); - envSpy.mockReset().mockReturnValue({}); }); it('should call execSync with return result', async () => { @@ -70,7 +64,7 @@ describe('runAutorunExecutor', () => { }); it('should create command from context, options and arguments', async () => { - envSpy.mockReturnValue({ CP_PROJECT: 'CLI' }); + vi.stubEnv('CP_PROJECT', 'CLI'); const output = await runAutorunExecutor( { persist: { filename: 'REPORT', format: ['md', 'json'] } }, executorContext('core'), diff --git a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts index 5626375bd..19da42b17 100644 --- a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts @@ -84,7 +84,7 @@ describe('parseAutorunExecutorOptions', () => { }), ); - expect(processEnvSpy).toHaveBeenCalledTimes(1); + expect(processEnvSpy.mock.calls.length).toBeGreaterThanOrEqual(1); expect(executorOptions.persist).toEqual( expect.objectContaining({ diff --git a/packages/nx-plugin/src/internal/execute-process.ts b/packages/nx-plugin/src/internal/execute-process.ts index cedc3fdbc..ee5813397 100644 --- a/packages/nx-plugin/src/internal/execute-process.ts +++ b/packages/nx-plugin/src/internal/execute-process.ts @@ -1,4 +1,6 @@ +import { gray } from 'ansis'; import { spawn } from 'node:child_process'; +import { ui } from '@code-pushup/utils'; export function calcDuration(start: number, stop?: number): number { return Math.round((stop ?? performance.now()) - start); @@ -141,6 +143,13 @@ export function executeProcess(cfg: ProcessConfig): Promise { const date = new Date().toISOString(); const start = performance.now(); + const logCommand = [command, ...(args || [])].join(' '); + ui().logger.log( + gray( + `Executing command:\n${logCommand}\nIn working directory:\n${cfg.cwd ?? process.cwd()}`, + ), + ); + return new Promise((resolve, reject) => { // shell:true tells Windows to use shell command for spawning a child process const process = spawn(command, args, { cwd, shell: true }); diff --git a/packages/plugin-lighthouse/CONTRIBUTING.md b/packages/plugin-lighthouse/CONTRIBUTING.md index 63a124d8d..e0c0254bb 100644 --- a/packages/plugin-lighthouse/CONTRIBUTING.md +++ b/packages/plugin-lighthouse/CONTRIBUTING.md @@ -15,7 +15,7 @@ To test lighthouse properly we work with a predefined testing setup. On some OS there could be a problem finding the path to Chrome. -We try to detect it automatically in the [`chrome-path.setup.ts` script](../../testing/test-setup/src/lib/chrome-path.setup.ts). +We try to detect it automatically in the [`chrome-path.mock.ts` script](../../testing/test-setup/src/lib/chrome-path.mock.ts). There we use `getChromePath` and have `chromium` installed as NPM package, so detecting the path should not cause any problem. However, if no chrome path is detected automatically the error looks like this: @@ -46,7 +46,7 @@ In the CI you can set a static path if needed over the env variable like this: # ... ``` -We consider this path in our `beforeAll` hook in a [`chrome-path.setup.ts` script](../../testing/test-setup/src/lib/chrome-path.setup.ts). +We consider this path in our `beforeAll` hook in a [`chrome-path.mock.ts` script](../../testing/test-setup/src/lib/chrome-path.mock.ts). ### Testing chrome flags diff --git a/packages/plugin-lighthouse/vite.config.integration.ts b/packages/plugin-lighthouse/vite.config.integration.ts index 87a7c4ede..1636c3fca 100644 --- a/packages/plugin-lighthouse/vite.config.integration.ts +++ b/packages/plugin-lighthouse/vite.config.integration.ts @@ -24,7 +24,7 @@ export default defineConfig({ setupFiles: [ '../../testing/test-setup/src/lib/cliui.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', - '../../testing/test-setup/src/lib/chrome-path.setup.ts', + '../../testing/test-setup/src/lib/chrome-path.mock.ts', ], }, }); diff --git a/packages/plugin-typescript/vite.config.integration.ts b/packages/plugin-typescript/vite.config.integration.ts index 9089de7bf..557752a81 100644 --- a/packages/plugin-typescript/vite.config.integration.ts +++ b/packages/plugin-typescript/vite.config.integration.ts @@ -24,7 +24,7 @@ export default defineConfig({ setupFiles: [ '../../testing/test-setup/src/lib/cliui.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', - '../../testing/test-setup/src/lib/chrome-path.setup.ts', + '../../testing/test-setup/src/lib/chrome-path.mock.ts', ], }, }); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 5e01bf1e7..678b762fb 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -61,7 +61,7 @@ export { hasNoNullableProps, } from './lib/guards.js'; export { logMultipleResults } from './lib/log-results.js'; -export { link, ui, type CliUi, type Column } from './lib/logging.js'; +export { link, ui, type CliUi, type Column, isVerbose } from './lib/logging.js'; export { mergeConfigs } from './lib/merge-configs.js'; export { getProgressBar, type ProgressBar } from './lib/progress.js'; export { @@ -129,6 +129,5 @@ export type { WithRequired, CamelCaseToKebabCase, } from './lib/types.js'; -export { verboseUtils } from './lib/verbose-utils.js'; export { parseSchema, SchemaValidationError } from './lib/zod-validation.js'; export { createRunnerFiles } from './lib/create-runner-files.js'; diff --git a/packages/utils/src/lib/execute-process.ts b/packages/utils/src/lib/execute-process.ts index 8c506c15e..e82f6ba31 100644 --- a/packages/utils/src/lib/execute-process.ts +++ b/packages/utils/src/lib/execute-process.ts @@ -153,7 +153,7 @@ export function executeProcess(cfg: ProcessConfig): Promise { const logCommand = [command, ...(args || [])].join(' '); ui().logger.log( gray( - `Executing command:\n${logCommand}\nIn working directory:\n${cfg.cwd}`, + `Executing command:\n${logCommand}\nIn working directory:\n${cfg.cwd ?? process.cwd()}`, ), ); diff --git a/packages/utils/src/lib/git/git.integration.test.ts b/packages/utils/src/lib/git/git.integration.test.ts index 0851b01b7..47c2f749f 100644 --- a/packages/utils/src/lib/git/git.integration.test.ts +++ b/packages/utils/src/lib/git/git.integration.test.ts @@ -2,6 +2,7 @@ import { mkdir, rm, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { type SimpleGit, simpleGit } from 'simple-git'; import { afterAll, beforeAll, beforeEach, describe, expect } from 'vitest'; +import { initGitRepo, teardownTestFolder } from '@code-pushup/test-utils'; import { toUnixPath } from '../transform.js'; import { getGitRoot, @@ -16,14 +17,11 @@ describe('git utils in a git repo', () => { beforeAll(async () => { await mkdir(baseDir, { recursive: true }); - emptyGit = simpleGit(baseDir); - await emptyGit.init(); - await emptyGit.addConfig('user.name', 'John Doe'); - await emptyGit.addConfig('user.email', 'john.doe@example.com'); + emptyGit = await initGitRepo(simpleGit, { baseDir, baseBranch: 'master' }); }); afterAll(async () => { - await rm(baseDir, { recursive: true, force: true }); + await teardownTestFolder(baseDir); }); describe('without a branch and commits', () => { diff --git a/packages/utils/src/lib/logging.ts b/packages/utils/src/lib/logging.ts index 24d290632..f9f88ab7e 100644 --- a/packages/utils/src/lib/logging.ts +++ b/packages/utils/src/lib/logging.ts @@ -19,19 +19,27 @@ export type Column = { }; export type CliUi = CliUiBase & CliExtension; -// eslint-disable-next-line import/no-mutable-exports,functional/no-let -export let singletonUiInstance: CliUiBase | undefined; +export const isVerbose = () => process.env['CP_VERBOSE'] === 'true'; + +// eslint-disable-next-line functional/no-let +let cliUISingleton: CliUiBase | undefined; +// eslint-disable-next-line functional/no-let +let cliUIExtendedSingleton: CliUi | undefined; export function ui(): CliUi { - if (singletonUiInstance === undefined) { - singletonUiInstance = cliui(); + if (cliUISingleton === undefined) { + cliUISingleton = cliui(); } - return { - ...singletonUiInstance, - row: args => { - logListItem(args); - }, - }; + if (!cliUIExtendedSingleton) { + cliUIExtendedSingleton = { + ...cliUISingleton, + row: args => { + logListItem(args); + }, + }; + } + + return cliUIExtendedSingleton; } // eslint-disable-next-line functional/no-let @@ -44,7 +52,7 @@ export function logListItem(args: ArgumentsType) { const content = singletonisaacUi.toString(); // eslint-disable-next-line functional/immutable-data singletonisaacUi.rows = []; - singletonUiInstance?.logger.log(content); + cliUIExtendedSingleton?.logger.log(content); } export function link(text: string) { diff --git a/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts index 7035c4ed9..b4d7c5fc4 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.integration.test.ts @@ -50,7 +50,9 @@ describe('logStdoutSummary', () => { }); it('should include all audits when verbose is true', async () => { - logStdoutSummary(sortReport(scoreReport(reportMock())), true); + vi.stubEnv('CP_VERBOSE', 'true'); + + logStdoutSummary(sortReport(scoreReport(reportMock()))); const output = logs.join('\n'); diff --git a/packages/utils/src/lib/reports/log-stdout-summary.ts b/packages/utils/src/lib/reports/log-stdout-summary.ts index 93d71f149..0b4282ab6 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.ts @@ -1,6 +1,6 @@ import { bold, cyan, cyanBright, green, red } from 'ansis'; import type { AuditReport } from '@code-pushup/models'; -import { ui } from '../logging.js'; +import { isVerbose, ui } from '../logging.js'; import { CODE_PUSHUP_DOMAIN, FOOTER_PREFIX, @@ -19,11 +19,11 @@ function log(msg = ''): void { ui().logger.log(msg); } -export function logStdoutSummary(report: ScoredReport, verbose = false): void { +export function logStdoutSummary(report: ScoredReport): void { const { plugins, categories, packageName, version } = report; log(reportToHeaderSection({ packageName, version })); log(); - logPlugins(plugins, verbose); + logPlugins(plugins); if (categories && categories.length > 0) { logCategories({ plugins, categories }); } @@ -38,14 +38,11 @@ function reportToHeaderSection({ return `${bold(REPORT_HEADLINE_TEXT)} - ${packageName}@${version}`; } -export function logPlugins( - plugins: ScoredReport['plugins'], - verbose: boolean, -): void { +export function logPlugins(plugins: ScoredReport['plugins']): void { plugins.forEach(plugin => { const { title, audits } = plugin; const filteredAudits = - verbose || audits.length === 1 + isVerbose() || audits.length === 1 ? audits : audits.filter(({ score }) => score !== 1); const diff = audits.length - filteredAudits.length; diff --git a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts index 1697bea04..51b1e2777 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts @@ -181,19 +181,16 @@ describe('logPlugins', () => { }); it('should log only audits with scores other than 1 when verbose is false', () => { - logPlugins( - [ - { - title: 'Best Practices', - slug: 'best-practices', - audits: [ - { title: 'Audit 1', score: 0.75, value: 75 }, - { title: 'Audit 2', score: 1, value: 100 }, - ], - }, - ] as ScoredReport['plugins'], - false, - ); + logPlugins([ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 0.75, value: 75 }, + { title: 'Audit 2', score: 1, value: 100 }, + ], + }, + ] as ScoredReport['plugins']); const output = logs.join('\n'); expect(output).toContain('Audit 1'); expect(output).not.toContain('Audit 2'); @@ -201,72 +198,62 @@ describe('logPlugins', () => { }); it('should log all audits when verbose is true', () => { - logPlugins( - [ - { - title: 'Best Practices', - slug: 'best-practices', - audits: [ - { title: 'Audit 1', score: 0.5, value: 50 }, - { title: 'Audit 2', score: 1, value: 100 }, - ], - }, - ] as ScoredReport['plugins'], - true, - ); + vi.stubEnv('CP_VERBOSE', 'true'); + + logPlugins([ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 0.5, value: 50 }, + { title: 'Audit 2', score: 1, value: 100 }, + ], + }, + ] as ScoredReport['plugins']); const output = logs.join('\n'); expect(output).toContain('Audit 1'); expect(output).toContain('Audit 2'); }); it('should indicate all audits have perfect scores', () => { - logPlugins( - [ - { - title: 'Best Practices', - slug: 'best-practices', - audits: [ - { title: 'Audit 1', score: 1, value: 100 }, - { title: 'Audit 2', score: 1, value: 100 }, - ], - }, - ] as ScoredReport['plugins'], - false, - ); + logPlugins([ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 1, value: 100 }, + { title: 'Audit 2', score: 1, value: 100 }, + ], + }, + ] as ScoredReport['plugins']); const output = logs.join('\n'); expect(output).toContain('All 2 audits have perfect scores'); }); it('should log original audits when verbose is false and no audits have perfect scores', () => { - logPlugins( - [ - { - title: 'Best Practices', - slug: 'best-practices', - audits: [ - { title: 'Audit 1', score: 0.5, value: 100 }, - { title: 'Audit 2', score: 0.5, value: 100 }, - ], - }, - ] as ScoredReport['plugins'], - false, - ); + logPlugins([ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [ + { title: 'Audit 1', score: 0.5, value: 100 }, + { title: 'Audit 2', score: 0.5, value: 100 }, + ], + }, + ] as ScoredReport['plugins']); const output = logs.join('\n'); expect(output).toContain('Audit 1'); expect(output).toContain('Audit 2'); }); it('should not truncate a perfect audit in non-verbose mode when it is the only audit available', () => { - logPlugins( - [ - { - title: 'Best Practices', - slug: 'best-practices', - audits: [{ title: 'Audit 1', score: 1, value: 100 }], - }, - ] as ScoredReport['plugins'], - false, - ); + logPlugins([ + { + title: 'Best Practices', + slug: 'best-practices', + audits: [{ title: 'Audit 1', score: 1, value: 100 }], + }, + ] as ScoredReport['plugins']); const output = logs.join('\n'); expect(output).toContain('Audit 1'); }); diff --git a/packages/utils/src/lib/verbose-utils.ts b/packages/utils/src/lib/verbose-utils.ts deleted file mode 100644 index 9daf7d8e2..000000000 --- a/packages/utils/src/lib/verbose-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ui } from './logging.js'; - -function getLogVerbose(verbose = false) { - return (msg: string) => { - if (verbose) { - ui().logger.info(msg); - } - }; -} - -function getExecVerbose(verbose = false) { - return (fn: () => unknown) => { - if (verbose) { - fn(); - } - }; -} - -export const verboseUtils = (verbose = false) => ({ - log: getLogVerbose(verbose), - exec: getExecVerbose(verbose), -}); diff --git a/packages/utils/src/lib/verbose-utils.unit.test.ts b/packages/utils/src/lib/verbose-utils.unit.test.ts deleted file mode 100644 index 1403d44f7..000000000 --- a/packages/utils/src/lib/verbose-utils.unit.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { ui } from './logging.js'; -import { verboseUtils } from './verbose-utils.js'; - -describe('verbose-utils', () => { - it('exec should be off by default', () => { - const spy = vi.fn(); - verboseUtils().exec(spy); - expect(spy).not.toHaveBeenCalled(); - }); - - it('exec should work no-verbose', () => { - const spy = vi.fn(); - verboseUtils(false).exec(spy); - expect(spy).not.toHaveBeenCalled(); - }); - - it('exec should work verbose', () => { - const spy = vi.fn(); - verboseUtils(true).exec(spy); - expect(spy).toHaveBeenCalled(); - }); - - it('logs should be off by default', () => { - verboseUtils(false).log('42'); - expect(ui()).not.toHaveLogs(); - }); - - it('should not print any logs when verbose is off', () => { - verboseUtils(false).log('42'); - expect(ui()).not.toHaveLogs(); - }); - - it('should log when verbose is on', () => { - verboseUtils(true).log('42'); - expect(ui()).toHaveLogged('info', '42'); - }); -}); diff --git a/testing/test-setup/src/index.ts b/testing/test-setup/src/index.ts deleted file mode 100644 index e15bd396e..000000000 --- a/testing/test-setup/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/test-folder.setup.js'; diff --git a/testing/test-setup/src/lib/chrome-path.setup.ts b/testing/test-setup/src/lib/chrome-path.mock.ts similarity index 100% rename from testing/test-setup/src/lib/chrome-path.setup.ts rename to testing/test-setup/src/lib/chrome-path.mock.ts diff --git a/testing/test-setup/src/lib/reset.mocks.ts b/testing/test-setup/src/lib/reset.mocks.ts index b69446142..2b5f7144c 100644 --- a/testing/test-setup/src/lib/reset.mocks.ts +++ b/testing/test-setup/src/lib/reset.mocks.ts @@ -3,6 +3,10 @@ import { beforeEach, vi } from 'vitest'; beforeEach(() => { vi.clearAllMocks(); + vi.unstubAllEnvs(); + // set the verbose to false as default for all tests as local env could be set to true + vi.stubEnv('CP_VERBOSE', 'false'); + vol.reset(); }); diff --git a/testing/test-utils/src/index.ts b/testing/test-utils/src/index.ts index 25a36b96c..3c8a3626b 100644 --- a/testing/test-utils/src/index.ts +++ b/testing/test-utils/src/index.ts @@ -8,6 +8,7 @@ export * from './lib/utils/file-system.js'; export * from './lib/utils/create-npm-workshpace.js'; export * from './lib/utils/omit-report-data.js'; export * from './lib/utils/project-graph.js'; +export * from './lib/utils/test-folder-setup.js'; // static mocks export * from './lib/utils/commit.mock.js'; diff --git a/testing/test-utils/src/lib/utils/git.ts b/testing/test-utils/src/lib/utils/git.ts index 90e2a9265..1b3b00dbd 100644 --- a/testing/test-utils/src/lib/utils/git.ts +++ b/testing/test-utils/src/lib/utils/git.ts @@ -9,12 +9,15 @@ import type { import { vi } from 'vitest'; export type GitConfig = { name: string; email: string }; - export async function initGitRepo( simpleGit: SimpleGitFactory, - opt: { baseDir: string; config?: GitConfig }, + opt: { + baseDir: string; + config?: GitConfig; + baseBranch?: string; + }, ): Promise { - const { baseDir, config } = opt; + const { baseDir, config, baseBranch } = opt; const { email = 'john.doe@example.com', name = 'John Doe' } = config ?? {}; await mkdir(baseDir, { recursive: true }); const git = simpleGit(baseDir); @@ -23,7 +26,7 @@ export async function initGitRepo( await git.addConfig('user.email', email); await git.addConfig('commit.gpgSign', 'false'); await git.addConfig('tag.gpgSign', 'false'); - await git.branch(['-M', 'main']); + await git.branch(['-M', baseBranch ?? 'main']); return git; } diff --git a/testing/test-setup/src/lib/test-folder.setup.ts b/testing/test-utils/src/lib/utils/test-folder-setup.ts similarity index 57% rename from testing/test-setup/src/lib/test-folder.setup.ts rename to testing/test-utils/src/lib/utils/test-folder-setup.ts index 317b8f4c1..7b4750b35 100644 --- a/testing/test-setup/src/lib/test-folder.setup.ts +++ b/testing/test-utils/src/lib/utils/test-folder-setup.ts @@ -1,17 +1,27 @@ import { logger } from '@nx/devkit'; import { bold } from 'ansis'; -import { mkdir, rm } from 'node:fs/promises'; - -export async function setupTestFolder(dirName: string) { - await mkdir(dirName, { recursive: true }); -} +import { mkdir, rm, stat } from 'node:fs/promises'; export async function cleanTestFolder(dirName: string) { - await rm(dirName, { recursive: true, force: true }); + await teardownTestFolder(dirName); await mkdir(dirName, { recursive: true }); } export async function teardownTestFolder(dirName: string) { + try { + const stats = await stat(dirName); + if (!stats.isDirectory()) { + logger.warn( + `⚠️ You are trying to delete a file instead of a directory - ${bold( + dirName, + )}.`, + ); + } + } catch { + // continue safely without deleting as folder does not exist in the filesystem + return; + } + try { await rm(dirName, { recursive: true,