diff --git a/.github/label-actions.yml b/.github/label-actions.yml index 42c351f76b200c429f7a01cbb437ac035488d11f..6f5e6646899c86029047eb218d1a8821adbb7d11 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 ca4482cfa356cc401b0e48459932ed152e31b339..73efcabbdab3c8b91ee8d430352401b36a5f399e 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 d5277ee170b17adf437595359d6521cbc71b51be..3bae833dde9c5f1c5af8bcf193d124faf5f62832 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 0b15c230ef8b99431d0e5df38fa36a97f8cdf74b..59eaa8f731b59457ac9cfc73481ea715c15467e1 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 210b78d81f7f12ee4a47665ea989c9be71df0ab5..bb8bfb21761a8fb784d3702fd5b7205bf8d4c631 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 6cb34bb1fa68d9a35bf75b9db7018217014ebc0b..63622820d1be107324594865dd540abc3147a273 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 b88f515aef47ba9c499421be9efaaa1b830e382b..c220bb3ca926ea04d3c9b00c00fdb17f82fc8be1 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 2ba19032334fe4a4d5cd0e5923cebc8912d0e1e7..16818bb055b32703cc590e2233a2395854b109aa 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 1ac07046d69f5e50e16605c3231841d2d3a805fc..86cf232a98dc0dfa15f506bfd839cd2d0b8f19ad 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 0000000000000000000000000000000000000000..ce5ba6498e838a87c4d53cf54fc1e550f1d4782b --- /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 45bbb8e651d434947c3346764bb9c39a519f47c1..991a1eb2144434ba2b953ac451097d7f972c8787 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 888afb7b11194d03474c1cf59c6005fb22ae7626..27b717a02b01c152f4248e107697bffcd852bcf1 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 bbe1dae14f95db6ea1ca1b5ccedd8cbcbe27c5cf..203de5bb1602d32a7558a397f9e14fd22736ba30 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 623841f3c2accfc189478f381623a79ada2ff714..1829799e34e4ae28895fd8042e61714a1033a7a3 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 a9063233107ec3d137171b01cb6fdabed043bdeb..643cfbc976b625d440440fe7ea8ce3b79b7e47e9 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 315619b8742be5ea269aceebe5b704e85a2fa62a..3fafe4799c0b9e16b128d38e75b826498339272f 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 646b39a462e9deb50d284a7c3e8deb0e32799d09..90bd220f14e7384bc16b8543edb2b876fe18d511 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 fc9e14a60ba9842061bb16a59b2b7aa285cf9677..708a93cb92d639b9adbefbc63a13c08c1015a164 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 b482521ff41d591b252037eb2ee18f35dcbcffa9..68b756241ee5d87300016271d20f4d44e9f56fc1 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 e3dfbd4154dc3ba5fea8d80eadc1ce11b8dd0495..a85b009b5141e30e76b02330b2f01d275a630591 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 d9ec8c4bdff8ba54619780a61146a91eb10b70e0..b550eca4a89ee179ec07e5d76e381443158bf929 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 c89f07f4ee8e59bbd166290300a03e31d3658c1e..27962e25b3e83938cf1b2fe9d18f03ada2b4f17c 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 94c22015c9a5cd3d0cec107ba1b89d2d31b183e4..e0a11c9a75ac0f72c3b27c5d0f43a7803dcda62b 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 71e66aab5ea038311a054a340eb241f584c3dfe6..23d7e630b931289b3aa05d07a893df09a0e73bf1 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 bd6053b5b24a97ea557410e20f77737187e103c5..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..ab877f9c5fcacedcf5f1e970eb2da09fe8304e1c --- /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 6b3492d3ba0e44296a4b568b3d025cf0ae7e9732..7dd7c78f3746268e534da263e18a7eda5b36a0a7 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 8a2dfd95ef7537203667431be40a1b5406745ca0..64847e096a407d897a777719d2f1c181d38ed9af 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 4e484b92ab6869a93666f8af1a02f5a0de7e1d71..b8a4c9e1bdfa2ae1ab43c1c025af6ff7f3f23150 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 fce2edf9be5a3329f33578bb14de81ba95d95441..92a61baeff1cfdcafb3ae41df136f5d1d22926ab 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 301fdb323da990e64bece8772a71ac25edb63897..0f670affe2a2ee81b836c41168d2ce66ae4569cc 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 41aa9b55d17d3f5d045606912aa0b0ec85d4c038..17e8328811a3d23b97efb6c861ed8dfd22f03377 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 f358730a3ddae9652340c663eaef9a589e8716fb..dea632c4bbaf5924eb785b244feb1867bc5e70fc 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 3e7608971e27c748d1441f81458486d60499a057..fe0fce5d369ff85a0d6488dce527590ed8679e68 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 096b763ec42d4d29e9c3c99cf7b8a7d122bd8085..0d964a332fe6efe72bc14de7f006ce7473e4d58e 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 df0fb726fed246a2aa632869dc41cbc3ede0ee67..c9c94cb007b86d5884dc8caab0369d2e3bf98ec2 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 d734057792e1f9f63ef41c568a4beb0959a30183..723f8e4e00ee18f51d0fcba02810650318839d31 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 6cc1550ce08444983eb208173981252acfc19a5d..25c2b82c584847627ee3a07709065a3eb19d8798 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 d60ae5b2d7dec13a62f97dbf667983d046ded14a..a9c1ef2c7f81e37c2cf28fdebe70c6619864bcb1 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 24c36a8d4ab6c4c21184179c2eb615754d5b9e54..5e3a640c9e84076c69cd9d97b39937a32ec6bf63 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 3582c251d167da208360f66291b71df552b11beb..318a17106cbed483212e2e5d842d8179003966d9 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 55dc4beac8ea68949bbe2befc56d723bcc54b14f..30d92fd33faaf34e581493349feee3cb464964ba 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 0000000000000000000000000000000000000000..ba3c997cadde3a25d5e7073aec4fb32d5b028ba2 --- /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 d42fd4df7d613589a2f85ef00051be0ff0f1eddb..ae7716726cba7e5d961a6ac480e033cb241aefb9 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',