From bb5377b3d8f7d453a20beff59c8ccbc337ba97c7 Mon Sep 17 00:00:00 2001 From: Michael Kriese <michael.kriese@visualon.de> Date: Wed, 26 Feb 2025 13:40:30 +0100 Subject: [PATCH] test: fix code coverage (#34495) --- .github/label-actions.yml | 2 +- docs/development/best-practices.md | 2 +- docs/development/issue-labeling.md | 2 +- docs/development/local-development.md | 2 +- lib/config-validator.ts | 1 - lib/config/decrypt.ts | 4 +- lib/config/decrypt/legacy.ts | 1 - lib/config/migrate-validate.ts | 5 +- lib/config/migration.ts | 3 +- lib/config/parse.spec.ts | 59 +++++++++++++++++++ lib/config/parse.ts | 4 +- lib/config/presets/gitea/index.spec.ts | 29 ++++++--- lib/config/presets/gitea/index.ts | 1 - lib/config/presets/github/index.spec.ts | 29 ++++++--- lib/config/presets/github/index.ts | 1 - lib/config/presets/http/index.spec.ts | 9 +++ lib/config/presets/http/index.ts | 1 - lib/config/presets/index.spec.ts | 43 +++++++++++++- lib/config/presets/index.ts | 7 --- lib/config/presets/internal/index.spec.ts | 4 ++ lib/config/presets/internal/index.ts | 4 +- lib/config/validation.ts | 6 +- lib/constants/category.ts | 1 - lib/instrumentation/index.ts | 13 ++-- .../config-serializer.spec.ts.snap | 14 ----- lib/logger/cmd-serializer.spec.ts | 13 ++++ lib/logger/cmd-serializer.ts | 1 - lib/logger/config-serializer.spec.ts | 14 ++++- lib/logger/config-serializer.ts | 1 - lib/logger/index.spec.ts | 14 +++++ lib/logger/index.ts | 7 +-- lib/logger/once.ts | 7 ++- lib/logger/pretty-stdout.ts | 1 - lib/logger/remap.spec.ts | 2 +- lib/logger/renovate-logger.ts | 2 +- lib/logger/utils.spec.ts | 20 +++++++ lib/logger/utils.ts | 12 ++-- lib/proxy.ts | 3 +- lib/renovate.ts | 4 +- lib/util/cache/package/redis.ts | 1 - lib/util/http/legacy.ts | 1 - lib/workers/repository/cache.ts | 2 - test/host-rules.ts | 7 +++ vitest.config.ts | 1 + 44 files changed, 258 insertions(+), 102 deletions(-) create mode 100644 lib/config/parse.spec.ts delete mode 100644 lib/logger/__snapshots__/config-serializer.spec.ts.snap create mode 100644 lib/logger/cmd-serializer.spec.ts create mode 100644 test/host-rules.ts diff --git a/.github/label-actions.yml b/.github/label-actions.yml index 42c351f76b..6f5e664689 100644 --- a/.github/label-actions.yml +++ b/.github/label-actions.yml @@ -260,7 +260,7 @@ ```ts - // v8 ignore next: typescript strict null check + /* v8 ignore next: typescript strict null check */ if (!url) { return null; } diff --git a/docs/development/best-practices.md b/docs/development/best-practices.md index ca4482cfa3..73efcabbda 100644 --- a/docs/development/best-practices.md +++ b/docs/development/best-practices.md @@ -36,7 +36,7 @@ Read the [GitHub Docs, renaming a branch](https://docs.github.com/en/repositorie - Avoid `Boolean` instead use `is` functions from `@sindresorhus/is` package, for example: `is.string` ```ts -// v8 ignore next: can never happen +/* v8 ignore next: can never happen */ ``` ### Functions diff --git a/docs/development/issue-labeling.md b/docs/development/issue-labeling.md index d5277ee170..3bae833dde 100644 --- a/docs/development/issue-labeling.md +++ b/docs/development/issue-labeling.md @@ -203,7 +203,7 @@ Add a label `auto:logs` to indicate that there's a problem with the logs, and th Add a label `auto:needs-details` to discussions which need more details to move forward. -Add a label `auto:no-coverage-ignore` if PR authors avoid needed unit tests by v8 ignoring code with the `// v8 ignore` comment. +Add a label `auto:no-coverage-ignore` if PR authors avoid needed unit tests by v8 ignoring code with the `/* v8 ignore ... */` comment. Add a label `auto:no-done-comments` if PR authors unnecessary "Done" comments, or type comments to ask for a review instead of requesting a new review through GitHub's UI. diff --git a/docs/development/local-development.md b/docs/development/local-development.md index 0b15c230ef..59eaa8f731 100644 --- a/docs/development/local-development.md +++ b/docs/development/local-development.md @@ -158,7 +158,7 @@ e.g. `pnpm vitest composer -u` would update the saved snapshots for _all_ tests ### Coverage The Renovate project maintains 100% test coverage, so any Pull Request will fail if it does not have full coverage for code. -Using `// v8 ignore` is not ideal, but can be a pragmatic solution if adding more tests wouldn't really prove anything. +Using `/* v8 ignore ... */` is not ideal, but can be a pragmatic solution if adding more tests wouldn't really prove anything. To view the current test coverage locally, open up `coverage/index.html` in your browser. diff --git a/lib/config-validator.ts b/lib/config-validator.ts index 210b78d81f..bb8bfb2176 100644 --- a/lib/config-validator.ts +++ b/lib/config-validator.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node -// istanbul ignore file import 'source-map-support/register'; import './punycode.cjs'; import { dequal } from 'dequal'; diff --git a/lib/config/decrypt.ts b/lib/config/decrypt.ts index 6cb34bb1fa..63622820d1 100644 --- a/lib/config/decrypt.ts +++ b/lib/config/decrypt.ts @@ -38,13 +38,14 @@ export async function tryDecrypt( ); } else { decryptedStr = tryDecryptPublicKeyPKCS1(privateKey, encryptedStr); - // istanbul ignore if + /* v8 ignore start: not testable */ if (is.string(decryptedStr)) { logger.warn( { keyName }, 'Encrypted value is using deprecated PKCS1 padding, please change to using PGP encryption.', ); } + /* v8 ignore stop */ } } return decryptedStr; @@ -56,7 +57,6 @@ function validateDecryptedValue( ): string | null { try { const decryptedObj = DecryptedObject.safeParse(decryptedObjStr); - // istanbul ignore if if (!decryptedObj.success) { const error = new Error('config-validation'); error.validationError = `Could not parse decrypted config.`; diff --git a/lib/config/decrypt/legacy.ts b/lib/config/decrypt/legacy.ts index b88f515aef..c220bb3ca9 100644 --- a/lib/config/decrypt/legacy.ts +++ b/lib/config/decrypt/legacy.ts @@ -1,4 +1,3 @@ -/** istanbul ignore file */ import crypto from 'node:crypto'; import { logger } from '../../logger'; diff --git a/lib/config/migrate-validate.ts b/lib/config/migrate-validate.ts index 2ba1903233..16818bb055 100644 --- a/lib/config/migrate-validate.ts +++ b/lib/config/migrate-validate.ts @@ -33,7 +33,7 @@ export async function migrateAndValidate( warnings: ValidationMessage[]; errors: ValidationMessage[]; } = await configValidation.validateConfig('repo', massagedConfig); - // istanbul ignore if + /* v8 ignore start: hard to test */ if (is.nonEmptyArray(warnings)) { logger.warn({ warnings }, 'Found renovate config warnings'); } @@ -45,7 +45,8 @@ export async function migrateAndValidate( massagedConfig.warnings = (config.warnings ?? []).concat(warnings); } return massagedConfig; - } catch (err) /* istanbul ignore next */ { + /* v8 ignore next 3: TODO: test me */ + } catch (err) { logger.debug({ config: input }, 'migrateAndValidate error'); throw err; } diff --git a/lib/config/migration.ts b/lib/config/migration.ts index 1ac07046d6..86cf232a98 100644 --- a/lib/config/migration.ts +++ b/lib/config/migration.ts @@ -196,7 +196,8 @@ export function migrateConfig( }; } return { isMigrated, migratedConfig }; - } catch (err) /* istanbul ignore next */ { + /* v8 ignore next 4: TODO: test me */ + } catch (err) { logger.debug({ config, err }, 'migrateConfig() error'); throw err; } diff --git a/lib/config/parse.spec.ts b/lib/config/parse.spec.ts new file mode 100644 index 0000000000..ce5ba6498e --- /dev/null +++ b/lib/config/parse.spec.ts @@ -0,0 +1,59 @@ +import jsonValidator from 'json-dup-key-validator'; +import { parseFileConfig } from './parse'; + +vi.mock('json-dup-key-validator', { spy: true }); + +describe('config/parse', () => { + describe('json', () => { + it('parses', () => { + expect(parseFileConfig('config.json', '{}')).toEqual({ + success: true, + parsedContents: {}, + }); + }); + + it('returns error', () => { + // syntax validation + expect(parseFileConfig('config.json', '{')).toEqual({ + success: false, + validationError: 'Invalid JSON (parsing failed)', + validationMessage: 'Syntax error: unclosed statement near {', + }); + + // duplicate keys + vi.mocked(jsonValidator).validate.mockReturnValueOnce(undefined); + expect(parseFileConfig('config.json', '{')).toEqual({ + success: false, + validationError: 'Duplicate keys in JSON', + validationMessage: '"Syntax error: unclosed statement near {"', + }); + + // JSON.parse + vi.mocked(jsonValidator).validate.mockReturnValue(undefined); + expect(parseFileConfig('config.json', '{')).toEqual({ + success: false, + validationError: 'Invalid JSON (parsing failed)', + validationMessage: + 'JSON.parse error: `JSON5: invalid end of input at 1:2`', + }); + }); + }); + + describe('json5', () => { + it('parses', () => { + expect(parseFileConfig('config.json5', '{}')).toEqual({ + success: true, + parsedContents: {}, + }); + }); + + it('returns error', () => { + expect(parseFileConfig('config.json5', '{')).toEqual({ + success: false, + validationError: 'Invalid JSON5 (parsing failed)', + validationMessage: + 'JSON5.parse error: `JSON5: invalid end of input at 1:2`', + }); + }); + }); +}); diff --git a/lib/config/parse.ts b/lib/config/parse.ts index 45bbb8e651..991a1eb214 100644 --- a/lib/config/parse.ts +++ b/lib/config/parse.ts @@ -15,7 +15,7 @@ export function parseFileConfig( if (fileType === '.json5') { try { return { success: true, parsedContents: JSON5.parse(fileContents) }; - } catch (err) /* istanbul ignore next */ { + } catch (err) { logger.debug({ fileName, fileContents }, 'Error parsing JSON5 file'); const validationError = 'Invalid JSON5 (parsing failed)'; const validationMessage = `JSON5.parse error: \`${err.message.replaceAll( @@ -62,7 +62,7 @@ export function parseFileConfig( success: true, parsedContents: parseJson(fileContents, fileName), }; - } catch (err) /* istanbul ignore next */ { + } catch (err) { logger.debug({ fileContents }, 'Error parsing renovate config'); const validationError = 'Invalid JSON (parsing failed)'; const validationMessage = `JSON.parse error: \`${err.message.replaceAll( diff --git a/lib/config/presets/gitea/index.spec.ts b/lib/config/presets/gitea/index.spec.ts index 888afb7b11..27b717a02b 100644 --- a/lib/config/presets/gitea/index.spec.ts +++ b/lib/config/presets/gitea/index.spec.ts @@ -1,22 +1,17 @@ -import { mockDeep } from 'jest-mock-extended'; import * as httpMock from '../../../../test/http-mock'; -import { mocked } from '../../../../test/util'; -import * as _hostRules from '../../../util/host-rules'; +import { ExternalHostError } from '../../../types/errors/external-host-error'; import { setBaseUrl } from '../../../util/http/gitea'; import { toBase64 } from '../../../util/string'; import { PRESET_INVALID_JSON, PRESET_NOT_FOUND } from '../util'; import * as gitea from '.'; - -vi.mock('../../../util/host-rules', () => mockDeep()); - -const hostRules = mocked(_hostRules); +import { hostRules } from '~test/host-rules'; const giteaApiHost = gitea.Endpoint; const basePath = '/api/v1/repos/some/repo/contents'; describe('config/presets/gitea/index', () => { beforeEach(() => { - hostRules.find.mockReturnValue({ token: 'abc' }); + hostRules.add({ token: 'abc' }); setBaseUrl(giteaApiHost); }); @@ -54,6 +49,24 @@ describe('config/presets/gitea/index', () => { ); expect(res).toEqual({ from: 'api' }); }); + + it('throws external host error', async () => { + httpMock + .scope(giteaApiHost) + .get(`${basePath}/some-filename.json`) + .reply(404, {}); + + hostRules.add({ abortOnError: true }); + + await expect( + gitea.fetchJSONFile( + 'some/repo', + 'some-filename.json', + giteaApiHost, + null, + ), + ).rejects.toThrow(ExternalHostError); + }); }); describe('getPreset()', () => { diff --git a/lib/config/presets/gitea/index.ts b/lib/config/presets/gitea/index.ts index bbe1dae14f..203de5bb16 100644 --- a/lib/config/presets/gitea/index.ts +++ b/lib/config/presets/gitea/index.ts @@ -20,7 +20,6 @@ export async function fetchJSONFile( baseUrl: endpoint, }); } catch (err) { - // istanbul ignore if: not testable with nock if (err instanceof ExternalHostError) { throw err; } diff --git a/lib/config/presets/github/index.spec.ts b/lib/config/presets/github/index.spec.ts index 623841f3c2..1829799e34 100644 --- a/lib/config/presets/github/index.spec.ts +++ b/lib/config/presets/github/index.spec.ts @@ -1,21 +1,16 @@ -import { mockDeep } from 'jest-mock-extended'; import * as httpMock from '../../../../test/http-mock'; -import { mocked } from '../../../../test/util'; -import * as _hostRules from '../../../util/host-rules'; +import { ExternalHostError } from '../../../types/errors/external-host-error'; import { toBase64 } from '../../../util/string'; import { PRESET_INVALID_JSON, PRESET_NOT_FOUND } from '../util'; import * as github from '.'; - -vi.mock('../../../util/host-rules', () => mockDeep()); - -const hostRules = mocked(_hostRules); +import { hostRules } from '~test/host-rules'; const githubApiHost = github.Endpoint; const basePath = '/repos/some/repo/contents'; describe('config/presets/github/index', () => { beforeEach(() => { - hostRules.find.mockReturnValue({ token: 'abc' }); + hostRules.add({ token: 'abc' }); }); describe('fetchJSONFile()', () => { @@ -35,6 +30,24 @@ describe('config/presets/github/index', () => { ); expect(res).toEqual({ from: 'api' }); }); + + it('throws external host error', async () => { + httpMock + .scope(githubApiHost) + .get(`${basePath}/some-filename.json`) + .reply(404, {}); + + hostRules.add({ abortOnError: true }); + + await expect( + github.fetchJSONFile( + 'some/repo', + 'some-filename.json', + githubApiHost, + undefined, + ), + ).rejects.toThrow(ExternalHostError); + }); }); describe('getPreset()', () => { diff --git a/lib/config/presets/github/index.ts b/lib/config/presets/github/index.ts index a906323310..643cfbc976 100644 --- a/lib/config/presets/github/index.ts +++ b/lib/config/presets/github/index.ts @@ -26,7 +26,6 @@ export async function fetchJSONFile( try { res = await http.getJsonUnchecked(url); } catch (err) { - // istanbul ignore if: not testable with nock if (err instanceof ExternalHostError) { throw err; } diff --git a/lib/config/presets/http/index.spec.ts b/lib/config/presets/http/index.spec.ts index 315619b874..3fafe4799c 100644 --- a/lib/config/presets/http/index.spec.ts +++ b/lib/config/presets/http/index.spec.ts @@ -1,6 +1,8 @@ import * as httpMock from '../../../../test/http-mock'; +import { ExternalHostError } from '../../../types/errors/external-host-error'; import { PRESET_DEP_NOT_FOUND, PRESET_INVALID_JSON } from '../util'; import * as http from '.'; +import { hostRules } from '~test/host-rules'; const host = 'https://my.server/'; const filePath = '/test-preset.json'; @@ -46,5 +48,12 @@ describe('config/presets/http/index', () => { PRESET_DEP_NOT_FOUND, ); }); + it('throws external host error', async () => { + httpMock.scope(host).get(filePath).reply(404, {}); + + hostRules.add({ abortOnError: true }); + + await expect(http.getPreset({ repo })).rejects.toThrow(ExternalHostError); + }); }); }); diff --git a/lib/config/presets/http/index.ts b/lib/config/presets/http/index.ts index 646b39a462..90bd220f14 100644 --- a/lib/config/presets/http/index.ts +++ b/lib/config/presets/http/index.ts @@ -22,7 +22,6 @@ export async function getPreset({ try { response = await http.get(url); } catch (err) { - // istanbul ignore if: not testable with nock if (err instanceof ExternalHostError) { throw err; } diff --git a/lib/config/presets/index.spec.ts b/lib/config/presets/index.spec.ts index fc9e14a60b..708a93cb92 100644 --- a/lib/config/presets/index.spec.ts +++ b/lib/config/presets/index.spec.ts @@ -1,6 +1,7 @@ import { mockDeep } from 'jest-mock-extended'; import { Fixtures } from '../../../test/fixtures'; -import { mocked } from '../../../test/util'; +import { PLATFORM_RATE_LIMIT_EXCEEDED } from '../../constants/error-messages'; +import { ExternalHostError } from '../../types/errors/external-host-error'; import * as memCache from '../../util/cache/memory'; import * as _packageCache from '../../util/cache/package'; import { GlobalConfig } from '../global'; @@ -15,6 +16,7 @@ import { PRESET_RENOVATE_CONFIG_NOT_FOUND, } from './util'; import * as presets from '.'; +import { logger, mocked } from '~test/util'; vi.mock('./npm'); vi.mock('./github'); @@ -83,9 +85,25 @@ describe('config/presets/index', () => { expect(res).toEqual({ foo: 1 }); }); + it('skips duplicate resolves', async () => { + config.extends = ['local>some/repo:a', 'local>some/repo:b']; + local.getPreset.mockResolvedValueOnce({ extends: ['local>some/repo:c'] }); + local.getPreset.mockResolvedValueOnce({ extends: ['local>some/repo:c'] }); + local.getPreset.mockResolvedValueOnce({ foo: 1 }); + expect(await presets.resolveConfigPresets(config)).toEqual({ + foo: 1, + }); + expect(local.getPreset).toHaveBeenCalledTimes(3); + expect(logger.logger.debug).toHaveBeenCalledWith( + 'Already seen preset local>some/repo:c in [local>some/repo:a, local>some/repo:c]', + ); + }); + it('throws if invalid preset file', async () => { config.foo = 1; - config.extends = ['notfound']; + config.extends = ['local>some/repo']; + + local.getPreset.mockResolvedValueOnce({ extends: ['notfound'] }); let e: Error | undefined; try { await presets.resolveConfigPresets(config); @@ -95,7 +113,8 @@ describe('config/presets/index', () => { expect(e).toBeDefined(); expect(e!.validationSource).toBeUndefined(); expect(e!.validationError).toBe( - "Cannot find preset's package (notfound)", + "Cannot find preset's package (notfound)." + + ' Note: this is a *nested* preset so please contact the preset author if you are unable to fix it yourself.', ); expect(e!.validationMessage).toBeUndefined(); }); @@ -464,6 +483,24 @@ describe('config/presets/index', () => { expect(packageCache.set.mock.calls[0][3]).toBe(60); }); + + it('throws', async () => { + config.extends = ['local>username/preset-repo']; + + local.getPreset.mockRejectedValueOnce( + new ExternalHostError(new Error('whoops')), + ); + await expect(presets.resolveConfigPresets(config)).rejects.toThrow( + ExternalHostError, + ); + + local.getPreset.mockRejectedValueOnce( + new Error(PLATFORM_RATE_LIMIT_EXCEEDED), + ); + await expect(presets.resolveConfigPresets(config)).rejects.toThrow( + PLATFORM_RATE_LIMIT_EXCEEDED, + ); + }); }); describe('replaceArgs', () => { diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts index b482521ff4..68b756241e 100644 --- a/lib/config/presets/index.ts +++ b/lib/config/presets/index.ts @@ -159,7 +159,6 @@ export async function getPreset( } logger.trace({ presetConfig }, `Applied params to preset ${preset}`); const presetKeys = Object.keys(presetConfig); - // istanbul ignore if if ( presetKeys.length === 2 && presetKeys.includes('description') && @@ -212,7 +211,6 @@ export async function resolveConfigPresets( ignorePresets, existingPresets.concat([preset]), ); - // istanbul ignore if if (inputConfig?.ignoreDeps?.length === 0) { delete presetConfig.description; } @@ -271,11 +269,9 @@ async function fetchPreset( return await getPreset(preset, baseConfig ?? inputConfig); } catch (err) { logger.debug({ preset, err }, 'Preset fetch error'); - // istanbul ignore if if (err instanceof ExternalHostError) { throw err; } - // istanbul ignore if if (err.message === PLATFORM_RATE_LIMIT_EXCEEDED) { throw err; } @@ -295,7 +291,6 @@ async function fetchPreset( } else { error.validationError = `Preset caused unexpected error (${preset})`; } - // istanbul ignore if if (existingPresets.length) { error.validationError += '. Note: this is a *nested* preset so please contact the preset author if you are unable to fix it yourself.'; @@ -313,7 +308,6 @@ function shouldResolvePreset( existingPresets: string[], ignorePresets: string[], ): boolean { - // istanbul ignore if if (existingPresets.includes(preset)) { logger.debug( `Already seen preset ${preset} in [${existingPresets.join(', ')}]`, @@ -321,7 +315,6 @@ function shouldResolvePreset( return false; } if (ignorePresets.includes(preset)) { - // istanbul ignore next logger.debug( `Ignoring preset ${preset} in [${existingPresets.join(', ')}]`, ); diff --git a/lib/config/presets/internal/index.spec.ts b/lib/config/presets/internal/index.spec.ts index e3dfbd4154..a85b009b51 100644 --- a/lib/config/presets/internal/index.spec.ts +++ b/lib/config/presets/internal/index.spec.ts @@ -55,4 +55,8 @@ describe('config/presets/internal/index', () => { .flat() .forEach((preset) => expect(preset).not.toMatch(/{{.*}}/)); }); + + it('returns undefined for unknown preset', () => { + expect(internal.getPreset({ repo: 'some/repo' })).toBeUndefined(); + }); }); diff --git a/lib/config/presets/internal/index.ts b/lib/config/presets/internal/index.ts index d9ec8c4bdf..b550eca4a8 100644 --- a/lib/config/presets/internal/index.ts +++ b/lib/config/presets/internal/index.ts @@ -41,7 +41,5 @@ export function getPreset({ repo, presetName, }: PresetConfig): Preset | undefined { - return groups[repo] && presetName - ? groups[repo][presetName] - : /* istanbul ignore next */ undefined; + return groups[repo] && presetName ? groups[repo][presetName] : undefined; } diff --git a/lib/config/validation.ts b/lib/config/validation.ts index c89f07f4ee..27962e25b3 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -166,7 +166,7 @@ export async function validateConfig( for (const [key, val] of Object.entries(config)) { const currentPath = parentPath ? `${parentPath}.${key}` : key; - // istanbul ignore if + /* v8 ignore next 7: TODO: test me */ if (key === '__proto__') { errors.push({ topic: 'Config security error', @@ -818,11 +818,9 @@ export async function validateConfig( } function sortAll(a: ValidationMessage, b: ValidationMessage): number { - // istanbul ignore else: currently never happen if (a.topic === b.topic) { return a.message > b.message ? 1 : -1; } - // istanbul ignore next: currently never happen return a.topic > b.topic ? 1 : -1; } @@ -843,7 +841,7 @@ async function validateGlobalConfig( currentPath: string | undefined, config: RenovateConfig, ): Promise<void> { - // istanbul ignore if + /* v8 ignore next 5: not testable yet */ if (getDeprecationMessage(key)) { warnings.push({ topic: 'Deprecation Warning', diff --git a/lib/constants/category.ts b/lib/constants/category.ts index 94c22015c9..e0a11c9a75 100644 --- a/lib/constants/category.ts +++ b/lib/constants/category.ts @@ -1,4 +1,3 @@ -// istanbul ignore next export const Categories = [ 'ansible', 'batect', diff --git a/lib/instrumentation/index.ts b/lib/instrumentation/index.ts index 71e66aab5e..23d7e630b9 100644 --- a/lib/instrumentation/index.ts +++ b/lib/instrumentation/index.ts @@ -74,11 +74,8 @@ export function init(): void { instrumentations = [ new HttpInstrumentation({ - applyCustomAttributesOnSpan: /* istanbul ignore next */ ( - span, - request, - response, - ) => { + /* v8 ignore start: not easily testable */ + applyCustomAttributesOnSpan: (span, request, response) => { // ignore 404 errors when the branch protection of Github could not be found. This is expected if no rules are configured if ( request instanceof ClientRequest && @@ -89,6 +86,7 @@ export function init(): void { span.setStatus({ code: SpanStatusCode.OK }); } }, + /* v8 ignore stop */ }), new BunyanInstrumentation(), ]; @@ -97,8 +95,7 @@ export function init(): void { }); } -/* istanbul ignore next */ - +/* v8 ignore start: not easily testable */ // https://github.com/open-telemetry/opentelemetry-js-api/issues/34 export async function shutdown(): Promise<void> { const traceProvider = getTracerProvider(); @@ -111,8 +108,8 @@ export async function shutdown(): Promise<void> { } } } +/* v8 ignore stop */ -/* istanbul ignore next */ export function disableInstrumentations(): void { for (const instrumentation of instrumentations) { instrumentation.disable(); diff --git a/lib/logger/__snapshots__/config-serializer.spec.ts.snap b/lib/logger/__snapshots__/config-serializer.spec.ts.snap deleted file mode 100644 index bd6053b5b2..0000000000 --- a/lib/logger/__snapshots__/config-serializer.spec.ts.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`logger/config-serializer > squashes templates 1`] = ` -{ - "nottoken": "b", - "prBody": "[Template]", -} -`; - -exports[`logger/config-serializer > suppresses content 1`] = ` -{ - "content": "[content]", -} -`; diff --git a/lib/logger/cmd-serializer.spec.ts b/lib/logger/cmd-serializer.spec.ts new file mode 100644 index 0000000000..ab877f9c5f --- /dev/null +++ b/lib/logger/cmd-serializer.spec.ts @@ -0,0 +1,13 @@ +import cmdSerializer from './cmd-serializer'; + +describe('logger/cmd-serializer', () => { + it('returns array', () => { + expect(cmdSerializer([''])).toEqual(['']); + }); + + it('redacts', () => { + expect(cmdSerializer(' https://token@domain.com')).toEqual( + ' https://**redacted**@domain.com', + ); + }); +}); diff --git a/lib/logger/cmd-serializer.ts b/lib/logger/cmd-serializer.ts index 6b3492d3ba..7dd7c78f37 100644 --- a/lib/logger/cmd-serializer.ts +++ b/lib/logger/cmd-serializer.ts @@ -1,4 +1,3 @@ -// istanbul ignore next export default function cmdSerializer( cmd: string | string[], ): string | string[] { diff --git a/lib/logger/config-serializer.spec.ts b/lib/logger/config-serializer.spec.ts index 8a2dfd95ef..64847e096a 100644 --- a/lib/logger/config-serializer.spec.ts +++ b/lib/logger/config-serializer.spec.ts @@ -6,7 +6,8 @@ describe('logger/config-serializer', () => { nottoken: 'b', prBody: 'foo', }; - expect(configSerializer(config)).toMatchSnapshot({ + expect(configSerializer(config)).toEqual({ + nottoken: 'b', prBody: '[Template]', }); }); @@ -15,8 +16,17 @@ describe('logger/config-serializer', () => { const config = { content: {}, }; - expect(configSerializer(config)).toMatchSnapshot({ + expect(configSerializer(config)).toEqual({ content: '[content]', }); }); + + it('suppresses packageFiles', () => { + const config = { + packageFiles: [], + }; + expect(configSerializer(config)).toEqual({ + packageFiles: '[Array]', + }); + }); }); diff --git a/lib/logger/config-serializer.ts b/lib/logger/config-serializer.ts index 4e484b92ab..b8a4c9e1bd 100644 --- a/lib/logger/config-serializer.ts +++ b/lib/logger/config-serializer.ts @@ -22,7 +22,6 @@ export default function configSerializer( if (contentFields.includes(key)) { this.update('[content]'); } - // istanbul ignore if if (arrayFields.includes(key)) { this.update('[Array]'); } diff --git a/lib/logger/index.spec.ts b/lib/logger/index.spec.ts index fce2edf9be..92a61baeff 100644 --- a/lib/logger/index.spec.ts +++ b/lib/logger/index.spec.ts @@ -5,10 +5,12 @@ import { partial } from '../../test/util'; import { add } from '../util/host-rules'; import { addSecretForSanitizing as addSecret } from '../util/sanitize'; import type { RenovateLogger } from './renovate-logger'; +import { ProblemStream } from './utils'; import { addMeta, addStream, clearProblems, + createDefaultStreams, getContext, getProblems, levels, @@ -161,6 +163,18 @@ describe('logger/index', () => { }); }); + describe('createDefaultStreams', () => { + it('creates log file stream', () => { + expect( + createDefaultStreams('info', new ProblemStream(), 'file.log'), + ).toMatchObject([ + { name: 'stdout', type: 'raw' }, + { name: 'problems', type: 'raw' }, + { name: 'logfile' }, + ]); + }); + }); + it('sets level', () => { expect(logLevel()).toBeDefined(); // depends on passed env expect(() => levels('stdout', 'debug')).not.toThrow(); diff --git a/lib/logger/index.ts b/lib/logger/index.ts index 301fdb323d..0f670affe2 100644 --- a/lib/logger/index.ts +++ b/lib/logger/index.ts @@ -34,7 +34,6 @@ export function createDefaultStreams( stream: process.stdout, }; - // istanbul ignore if: not testable if (getEnv('LOG_FORMAT') !== 'json') { // TODO: typings (#9615) const prettyStdOut = new RenovateStream() as any; @@ -50,7 +49,6 @@ export function createDefaultStreams( type: 'raw', }; - // istanbul ignore next: not easily testable const logFileStream: bunyan.Stream | undefined = is.string(logFile) ? createLogFileStream(logFile) : undefined; @@ -60,7 +58,6 @@ export function createDefaultStreams( ) as bunyan.Stream[]; } -// istanbul ignore next: not easily testable function createLogFileStream(logFile: string): bunyan.Stream { // Ensure log file directory exists const directoryName = upath.dirname(logFile); @@ -135,9 +132,7 @@ export function withMeta<T>(obj: Record<string, unknown>, cb: () => T): T { } } -export /* istanbul ignore next */ function addStream( - stream: bunyan.Stream, -): void { +export function addStream(stream: bunyan.Stream): void { loggerInternal.addStream(stream); } diff --git a/lib/logger/once.ts b/lib/logger/once.ts index 41aa9b55d1..17e8328811 100644 --- a/lib/logger/once.ts +++ b/lib/logger/once.ts @@ -1,5 +1,7 @@ type OmitFn = (...args: any[]) => any; +// TODO: use `callsite` package instead? + /** * Get the single frame of this function's callers stack. * @@ -24,7 +26,8 @@ function getCallSite(omitFn: OmitFn): string | null { if (callsite) { result = callsite.toString(); } - } catch /* istanbul ignore next */ { + /* v8 ignore next 2: should not happen */ + } catch { // no-op } finally { Error.stackTraceLimit = stackTraceLimitOrig; @@ -39,7 +42,7 @@ const keys = new Set<string>(); export function once(callback: () => void, omitFn: OmitFn = once): void { const key = getCallSite(omitFn); - // istanbul ignore if + /* v8 ignore next 3: should not happen */ if (!key) { return; } diff --git a/lib/logger/pretty-stdout.ts b/lib/logger/pretty-stdout.ts index f358730a3d..dea632c4bb 100644 --- a/lib/logger/pretty-stdout.ts +++ b/lib/logger/pretty-stdout.ts @@ -100,7 +100,6 @@ export class RenovateStream extends Stream { this.writable = true; } - // istanbul ignore next write(data: BunyanRecord): boolean { this.emit('data', formatRecord(data)); return true; diff --git a/lib/logger/remap.spec.ts b/lib/logger/remap.spec.ts index 3e7608971e..fe0fce5d36 100644 --- a/lib/logger/remap.spec.ts +++ b/lib/logger/remap.spec.ts @@ -23,7 +23,7 @@ describe('logger/remap', () => { it('performs global remaps', () => { setGlobalLogLevelRemaps([{ matchMessage: '*foo*', newLogLevel: 'error' }]); - setRepositoryLogLevelRemaps(undefined); + setRepositoryLogLevelRemaps([]); const res = getRemappedLevel('foo'); diff --git a/lib/logger/renovate-logger.ts b/lib/logger/renovate-logger.ts index 096b763ec4..0d964a332f 100644 --- a/lib/logger/renovate-logger.ts +++ b/lib/logger/renovate-logger.ts @@ -115,7 +115,7 @@ export class RenovateLogger implements Logger { if (is.string(msg)) { const remappedLevel = getRemappedLevel(msg); - // istanbul ignore if: not easily testable + /* v8 ignore next 4: not easily testable */ if (remappedLevel) { meta.oldLevel = level; level = remappedLevel; diff --git a/lib/logger/utils.spec.ts b/lib/logger/utils.spec.ts index df0fb726fe..c9c94cb007 100644 --- a/lib/logger/utils.spec.ts +++ b/lib/logger/utils.spec.ts @@ -1,3 +1,4 @@ +import { TimeoutError } from 'got'; import { z } from 'zod'; import prepareError, { prepareZodIssues, @@ -106,6 +107,9 @@ describe('logger/utils', () => { } it('prepareZodIssues', () => { + expect(prepareZodIssues(null)).toBe(null); + expect(prepareZodIssues({ _errors: ['a', 'b'] })).toEqual(['a', 'b']); + expect(prepareIssues(z.string(), 42)).toBe( 'Expected string, received number', ); @@ -214,5 +218,21 @@ describe('logger/utils', () => { stack: expect.stringMatching(/^ZodError: Schema error/), }); }); + + it('handles HTTP timout error', () => { + const err = new TimeoutError( + // @ts-expect-error some types are private + new Error('timeout'), + {}, + { context: { hostType: 'foo' } }, + ); + Object.assign(err, { + response: {}, + }); + expect(prepareError(err)).toMatchObject({ + message: 'timeout', + name: 'TimeoutError', + }); + }); }); }); diff --git a/lib/logger/utils.ts b/lib/logger/utils.ts index d734057792..723f8e4e00 100644 --- a/lib/logger/utils.ts +++ b/lib/logger/utils.ts @@ -57,14 +57,12 @@ type ZodShortenedIssue = }; export function prepareZodIssues(input: unknown): ZodShortenedIssue { - // istanbul ignore if if (!is.plainObject(input)) { return null; } let err: null | string | string[] = null; if (is.array(input._errors, is.string)) { - // istanbul ignore else if (input._errors.length === 1) { err = input._errors[0]; } else if (input._errors.length > 1) { @@ -96,11 +94,11 @@ export function prepareZodIssues(input: unknown): ZodShortenedIssue { } export function prepareZodError(err: ZodError): Record<string, unknown> { - // istanbul ignore next Object.defineProperty(err, 'message', { get: () => 'Schema error', + /* v8 ignore next 3: TODO: drop set? */ set: () => { - // intentionally empty + /* intentionally empty */ }, }); @@ -144,13 +142,11 @@ export default function prepareError(err: Error): Record<string, unknown> { options.method = err.options.method; options.http2 = err.options.http2; - // istanbul ignore else if (err.response) { response.response = { - statusCode: err.response?.statusCode, - statusMessage: err.response?.statusMessage, + statusCode: err.response.statusCode, + statusMessage: err.response.statusMessage, body: - // istanbul ignore next: not easily testable err.name === 'TimeoutError' ? undefined : structuredClone(err.response.body), diff --git a/lib/proxy.ts b/lib/proxy.ts index 6cc1550ce0..25c2b82c58 100644 --- a/lib/proxy.ts +++ b/lib/proxy.ts @@ -8,13 +8,14 @@ let agent = false; export function bootstrap(): void { envVars.forEach((envVar) => { - /* istanbul ignore if: env is case-insensitive on windows */ + /* v8 ignore start: env is case-insensitive on windows */ if ( typeof process.env[envVar] === 'undefined' && typeof process.env[envVar.toLowerCase()] !== 'undefined' ) { process.env[envVar] = process.env[envVar.toLowerCase()]; } + /* v8 ignore stop */ if (process.env[envVar]) { logger.debug(`Detected ${envVar} value in env`); diff --git a/lib/renovate.ts b/lib/renovate.ts index d60ae5b2d7..a9c1ef2c7f 100644 --- a/lib/renovate.ts +++ b/lib/renovate.ts @@ -7,7 +7,7 @@ import { logger } from './logger'; import { bootstrap } from './proxy'; import { start } from './workers/global'; -// istanbul ignore next +/* v8 ignore next 3: not easily testable */ process.on('unhandledRejection', (err) => { logger.error({ err }, 'unhandledRejection'); }); @@ -19,7 +19,7 @@ bootstrap(); process.exitCode = await instrument('run', () => start()); await telemetryShutdown(); //gracefully shutdown OpenTelemetry - // istanbul ignore if + /* v8 ignore next 3: no test required */ if (process.env.RENOVATE_X_HARD_EXIT) { process.exit(process.exitCode); } diff --git a/lib/util/cache/package/redis.ts b/lib/util/cache/package/redis.ts index 24c36a8d4a..5e3a640c9e 100644 --- a/lib/util/cache/package/redis.ts +++ b/lib/util/cache/package/redis.ts @@ -1,4 +1,3 @@ -/* istanbul ignore file */ import { DateTime } from 'luxon'; import { createClient, createCluster } from 'redis'; import { logger } from '../../../logger'; diff --git a/lib/util/http/legacy.ts b/lib/util/http/legacy.ts index 3582c251d1..318a17106c 100644 --- a/lib/util/http/legacy.ts +++ b/lib/util/http/legacy.ts @@ -1,4 +1,3 @@ -// istanbul ignore file import { RequestError as HttpError } from 'got'; import { parseUrl } from '../url'; diff --git a/lib/workers/repository/cache.ts b/lib/workers/repository/cache.ts index 55dc4beac8..30d92fd33f 100644 --- a/lib/workers/repository/cache.ts +++ b/lib/workers/repository/cache.ts @@ -1,5 +1,3 @@ -/* istanbul ignore file */ - import { REPOSITORY_CHANGED } from '../../constants/error-messages'; import { logger } from '../../logger'; import { platform } from '../../modules/platform'; diff --git a/test/host-rules.ts b/test/host-rules.ts new file mode 100644 index 0000000000..ba3c997cad --- /dev/null +++ b/test/host-rules.ts @@ -0,0 +1,7 @@ +import { hostRules } from './util'; + +export * as hostRules from '../lib/util/host-rules'; + +beforeEach(() => { + hostRules.clear(); +}); diff --git a/vitest.config.ts b/vitest.config.ts index d42fd4df7d..ae7716726c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -105,6 +105,7 @@ export default defineConfig(() => '__mocks__/**', // fully ignored files 'lib/config-validator.ts', + 'lib/constants/category.ts', 'lib/modules/datasource/hex/v2/package.ts', 'lib/modules/datasource/hex/v2/signed.ts', 'lib/util/cache/package/redis.ts', -- GitLab