0
0
Fork 0
mirror of https://github.com/renovatebot/renovate.git synced 2025-01-10 21:37:41 +00:00

fix: better branch code coverage ()

This commit is contained in:
Michael Kriese 2023-09-06 13:26:22 +02:00 committed by GitHub
parent 4b7949acbc
commit a9dc0625cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 231 additions and 124 deletions
.github/workflows
jest.config.ts
lib
config/presets/internal
modules
datasource
bitbucket-tags
conan
conda
dart-version
endoflife-date
flutter-version
hermit
index.ts
jenkins-plugins
pod
sbt-package
terraform-module
manager
cake
custom/regex
docker-compose
helm-values/__fixtures__
jsonnet-bundler
mint
woodpecker
platform
versioning
util
workers/repository/model

View file

@ -404,7 +404,7 @@ jobs:
- name: Check coverage threshold
run: |
pnpm nyc check-coverage -t ./coverage/nyc \
--branches 98 \
--branches 98.99 \
--functions 100 \
--lines 100 \
--statements 100

View file

@ -213,7 +213,9 @@ const config: JestConfig = {
cacheDirectory: '.cache/jest',
clearMocks: true,
collectCoverage: true,
coverageReporters: ci ? ['lcovonly', 'json'] : ['html', 'text-summary'],
coverageReporters: ci
? ['lcovonly', 'json']
: ['html', 'text-summary', 'json'],
transform: {
'\\.ts$': [
'ts-jest',

View file

@ -1,3 +1,4 @@
import { coerceArray } from '../../../util/array';
import type { PackageRule } from '../../types';
import type { Preset } from '../types';
@ -45,7 +46,7 @@ export function addPresets(
presets: Record<string, Preset>,
...templates: PresetTemplate[]
): void {
const ext = presets.all?.extends ?? [];
const ext = coerceArray(presets.all?.extends);
for (const template of templates) {
const { title, description, packageRules } = template;
presets[title] = {

View file

@ -1,4 +1,4 @@
import is from '@sindresorhus/is';
import { toArray } from '../../../util/array';
import type { Preset } from '../types';
/* eslint sort-keys: ["error", "asc", {caseSensitive: false, natural: true}] */
@ -482,20 +482,20 @@ export const presets: Record<string, Preset> = {};
for (const [name, value] of Object.entries(repoGroups)) {
presets[name] = {
description: `${name} monorepo`,
matchSourceUrls: is.array(value) ? value : [value],
matchSourceUrls: toArray(value),
};
}
for (const [name, value] of Object.entries(orgGroups)) {
presets[name] = {
description: `${name} monorepo`,
matchSourceUrlPrefixes: is.array(value) ? value : [value],
matchSourceUrlPrefixes: toArray(value),
};
}
for (const [name, value] of Object.entries(patternGroups)) {
presets[name] = {
description: `${name} monorepo`,
matchPackagePatterns: is.array(value) ? value : [value],
matchPackagePatterns: toArray(value),
};
}

View file

@ -124,5 +124,23 @@ describe('modules/datasource/bitbucket-tags/index', () => {
expect(res).toBeString();
expect(res).toBe('123');
});
it('returns null for missing hash', async () => {
const body = {
name: 'v1.0.0',
};
httpMock
.scope('https://api.bitbucket.org')
.get('/2.0/repositories/some/dep2/refs/tags/v1.0.0')
.reply(200, body);
const res = await getDigest(
{
datasource,
packageName: 'some/dep2',
},
'v1.0.0'
);
expect(res).toBeNull();
});
});
});

View file

@ -51,6 +51,17 @@ describe('modules/datasource/conan/index', () => {
'3a9b47caee2e2c1d3fb7d97788339aa8'
);
});
it('returns null for missing revision', async () => {
const version = '1.8.1';
httpMock
.scope(nonDefaultRegistryUrl)
.get(`/v2/conans/poco/${version}/_/_/revisions`)
.reply(200, []);
digestConfig.packageName = `poco/${version}@_/_`;
digestConfig.currentDigest = '4fc13d60fd91ba44fefe808ad719a5af';
expect(await getDigest(digestConfig, version)).toBeNull();
});
});
describe('getReleases', () => {
@ -180,6 +191,22 @@ describe('modules/datasource/conan/index', () => {
});
});
it('works with empty releases', async () => {
httpMock
.scope('https://api.github.com')
.get(
'/repos/conan-io/conan-center-index/contents/recipes/poco/config.yml'
)
.reply(200, '');
expect(
await getPkgReleases({
...config,
registryUrls: [defaultRegistryUrl],
packageName: 'poco/1.2@_/_',
})
).toBeNull();
});
it('rejects userAndChannel for Conan Center', async () => {
expect(
await getPkgReleases({

View file

@ -31,7 +31,10 @@ describe('modules/datasource/conda/index', () => {
});
it('returns null for empty result', async () => {
httpMock.scope(defaultRegistryUrl).get(depUrl).reply(200, {});
httpMock
.scope(defaultRegistryUrl)
.get(depUrl)
.reply(200, { versions: [] });
expect(
await getPkgReleases({
datasource,

View file

@ -34,7 +34,14 @@ describe('modules/datasource/dart-version/index', () => {
});
it('returns null for empty 200 OK', async () => {
httpMock.scope(baseUrl).get(urlPath).reply(200, []);
const scope = httpMock.scope(baseUrl);
for (const channel of channels) {
scope
.get(
`/storage/v1/b/dart-archive/o?delimiter=%2F&prefix=channels%2F${channel}%2Frelease%2F&alt=json`
)
.reply(200, { prefixes: [] });
}
expect(
await getPkgReleases({
datasource,

View file

@ -100,7 +100,7 @@ describe('modules/datasource/endoflife-date/index', () => {
});
it('returns null for empty result', async () => {
httpMock.scope(registryUrl).get(eksMockPath).reply(200, {});
httpMock.scope(registryUrl).get(eksMockPath).reply(200, []);
expect(
await getPkgReleases({
datasource,

View file

@ -32,7 +32,7 @@ describe('modules/datasource/flutter-version/index', () => {
});
it('returns null for empty 200 OK', async () => {
httpMock.scope(baseUrl).get(urlPath).reply(200, []);
httpMock.scope(baseUrl).get(urlPath).reply(200, { releases: [] });
expect(
await getPkgReleases({
datasource,

View file

@ -54,10 +54,9 @@ export class FlutterVersionDatasource extends Datasource {
releaseTimestamp: release_date,
isStable: channel === 'stable',
}));
return result.releases.length ? result : null;
} catch (err) {
this.handleGenericErrors(err);
}
return result.releases.length ? result : null;
}
}

View file

@ -5,6 +5,7 @@ import { getApiBaseUrl } from '../../../util/github/url';
import { GithubHttp } from '../../../util/http/github';
import { regEx } from '../../../util/regex';
import { streamToString } from '../../../util/streams';
import { coerceString } from '../../../util/string';
import { parseUrl } from '../../../util/url';
import { id } from '../../versioning/hermit';
import { Datasource } from '../datasource';
@ -106,8 +107,8 @@ export class HermitDatasource extends Datasource {
})
async getHermitSearchManifest(u: URL): Promise<HermitSearchResult[] | null> {
const registryUrl = u.toString();
const host = u.host ?? '';
const groups = this.pathRegex.exec(u.pathname ?? '')?.groups;
const host = coerceString(u.host);
const groups = this.pathRegex.exec(coerceString(u.pathname))?.groups;
if (!groups) {
logger.warn(
{ registryUrl },

View file

@ -3,6 +3,7 @@ import { dequal } from 'dequal';
import { HOST_DISABLED } from '../../constants/error-messages';
import { logger } from '../../logger';
import { ExternalHostError } from '../../types/errors/external-host-error';
import { coerceArray } from '../../util/array';
import * as memCache from '../../util/cache/memory';
import * as packageCache from '../../util/cache/package';
import { clone } from '../../util/clone';
@ -149,10 +150,10 @@ async function mergeRegistries(
continue;
}
if (combinedRes) {
for (const existingRelease of combinedRes.releases || []) {
for (const existingRelease of coerceArray(combinedRes.releases)) {
existingRelease.registryUrl ??= combinedRes.registryUrl;
}
for (const additionalRelease of res.releases || []) {
for (const additionalRelease of coerceArray(res.releases)) {
additionalRelease.registryUrl = res.registryUrl;
}
combinedRes = { ...res, ...combinedRes };

View file

@ -22,7 +22,6 @@ const jenkinsPluginsVersions: JenkinsPluginsVersionsResponse = {
'1.0.0': {
version: '1.0.0',
url: 'https://download.example.com',
buildDate: 'Jan 01, 2020',
},
'2.0.0': {
version: '2.0.0',
@ -83,7 +82,6 @@ describe('modules/datasource/jenkins-plugins/index', () => {
releases: [
{
downloadUrl: 'https://download.example.com',
releaseTimestamp: '2020-01-01T00:00:00.000Z',
version: '1.0.0',
},
{

View file

@ -1,6 +1,7 @@
import { getPkgReleases } from '..';
import * as httpMock from '../../../../test/http-mock';
import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages';
import * as hostRules from '../../../util/host-rules';
import * as rubyVersioning from '../../versioning/ruby';
import { PodDatasource } from '.';
@ -20,6 +21,7 @@ describe('modules/datasource/pod/index', () => {
describe('getReleases', () => {
beforeEach(() => {
jest.resetAllMocks();
hostRules.clear();
});
it('returns null for invalid inputs', async () => {
@ -37,6 +39,16 @@ describe('modules/datasource/pod/index', () => {
).toBeNull();
});
it('returns null disabled host', async () => {
hostRules.add({ matchHost: cocoapodsHost, enabled: false });
expect(
await getPkgReleases({
datasource: PodDatasource.id,
packageName: 'foobar',
})
).toBeNull();
});
it('returns null for empty result', async () => {
// FIXME: why get request?
httpMock
@ -119,6 +131,14 @@ describe('modules/datasource/pod/index', () => {
await expect(getPkgReleases(config)).rejects.toThrow(EXTERNAL_HOST_ERROR);
});
it('throws for 500', async () => {
httpMock
.scope(cocoapodsHost)
.get('/all_pods_versions_a_c_b.txt')
.reply(500);
await expect(getPkgReleases(config)).rejects.toThrow(EXTERNAL_HOST_ERROR);
});
it('returns null for unknown error', async () => {
httpMock
.scope(cocoapodsHost)

View file

@ -68,7 +68,6 @@ function handleError(packageName: string, err: HttpError): void {
} else if (statusCode === 404) {
logger.debug(errorData, 'Package lookup error');
} else if (err.message === HOST_DISABLED) {
// istanbul ignore next
logger.trace(errorData, 'Host disabled');
} else {
logger.warn(errorData, 'CocoaPods lookup failure: Unknown error');
@ -77,8 +76,8 @@ function handleError(packageName: string, err: HttpError): void {
function isDefaultRepo(url: string): boolean {
const match = githubRegex.exec(url);
if (match) {
const { account, repo } = match.groups ?? {};
if (match?.groups) {
const { account, repo } = match.groups;
return (
account.toLowerCase() === 'cocoapods' && repo.toLowerCase() === 'specs'
); // https://github.com/CocoaPods/Specs.git
@ -228,9 +227,9 @@ export class PodDatasource extends Datasource {
let result: ReleaseResult | null = null;
const match = githubRegex.exec(baseUrl);
if (match) {
if (match?.groups) {
baseUrl = massageGithubUrl(baseUrl);
const { hostURL, account, repo } = match?.groups ?? {};
const { hostURL, account, repo } = match.groups;
const opts = { hostURL, account, repo };
result = await this.getReleasesFromGithub(podName, opts);
} else {

View file

@ -0,0 +1,7 @@
import { getLatestVersion } from './util';
describe('modules/datasource/sbt-package/util', () => {
it('gets latest version', () => {
expect(getLatestVersion(['1.0.0', '3.0.0', '2.0.0'])).toBe('3.0.0');
});
});

View file

@ -1,3 +1,4 @@
import { coerceArray } from '../../../util/array';
import { regEx } from '../../../util/regex';
import { compare } from '../../versioning/maven/compare';
@ -7,7 +8,7 @@ export function parseIndexDir(
content: string,
filterFn = (x: string): boolean => !regEx(/^\.+/).test(x)
): string[] {
const unfiltered = content.match(linkRegExp) ?? [];
const unfiltered = coerceArray(content.match(linkRegExp));
return unfiltered.filter(filterFn);
}

View file

@ -1,6 +1,7 @@
import { logger } from '../../../logger';
import { cache } from '../../../util/cache/package/decorator';
import { regEx } from '../../../util/regex';
import { coerceString } from '../../../util/string';
import * as hashicorpVersioning from '../../versioning/hashicorp';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import { TerraformDatasource } from './base';
@ -163,7 +164,7 @@ export class TerraformModuleDatasource extends TerraformDatasource {
private static getRegistryRepository(
packageName: string,
registryUrl = ''
registryUrl: string | undefined
): RegistryRepository {
let registry: string;
const split = packageName.split('/');
@ -171,7 +172,7 @@ export class TerraformModuleDatasource extends TerraformDatasource {
[registry] = split;
split.shift();
} else {
registry = registryUrl;
registry = coerceString(registryUrl);
}
if (!regEx(/^https?:\/\//).test(registry)) {
registry = `https://${registry}`;

View file

@ -1,5 +1,5 @@
foo
#addin nuget:?package=Foo.Foo&version=1.1.1
#addin nuget:?package=Foo.Foo
#addin "nuget:?package=Bim.Bim&version=6.6.6"
#tool nuget:https://example.com?package=Bar.Bar&version=2.2.2
#module nuget:file:///tmp/?package=Baz.Baz&version=3.3.3

View file

@ -1,42 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`modules/manager/cake/index extracts 1`] = `
{
"deps": [
{
"currentValue": "1.1.1",
"datasource": "nuget",
"depName": "Foo.Foo",
},
{
"currentValue": "6.6.6",
"datasource": "nuget",
"depName": "Bim.Bim",
},
{
"currentValue": "2.2.2",
"datasource": "nuget",
"depName": "Bar.Bar",
"registryUrls": [
"https://example.com",
],
},
{
"currentValue": "3.3.3",
"datasource": "nuget",
"depName": "Baz.Baz",
"skipReason": "unsupported-url",
},
{
"currentValue": "1.0.3",
"datasource": "nuget",
"depName": "Cake.7zip",
},
{
"currentValue": "1.0.0",
"datasource": "nuget",
"depName": "Cake.asciidoctorj",
},
],
}
`;

View file

@ -3,9 +3,9 @@ import { extractPackageFile } from '.';
describe('modules/manager/cake/index', () => {
it('extracts', () => {
expect(extractPackageFile(Fixtures.get('build.cake'))).toMatchSnapshot({
expect(extractPackageFile(Fixtures.get('build.cake'))).toMatchObject({
deps: [
{ depName: 'Foo.Foo', currentValue: '1.1.1' },
{ depName: 'Foo.Foo', currentValue: undefined },
{ depName: 'Bim.Bim', currentValue: '6.6.6' },
{ depName: 'Bar.Bar', registryUrls: ['https://example.com'] },
{ depName: 'Baz.Baz', skipReason: 'unsupported-url' },

View file

@ -12,7 +12,7 @@ import {
export function handleAny(
content: string,
packageFile: string,
_packageFile: string,
config: RegexManagerConfig
): PackageDependency[] {
return config.matchStrings
@ -20,7 +20,12 @@ export function handleAny(
.flatMap((regex) => regexMatchAll(regex, content)) // match all regex to content, get all matches, reduce to single array
.map((matchResult) =>
createDependency(
{ groups: matchResult.groups ?? {}, replaceString: matchResult[0] },
{
groups:
matchResult.groups ??
/* istanbul ignore next: can this happen? */ {},
replaceString: matchResult[0],
},
config
)
)
@ -30,7 +35,7 @@ export function handleAny(
export function handleCombination(
content: string,
packageFile: string,
_packageFile: string,
config: RegexManagerConfig
): PackageDependency[] {
const matches = config.matchStrings
@ -43,7 +48,7 @@ export function handleCombination(
const extraction = matches
.map((match) => ({
groups: match.groups ?? {},
groups: match.groups ?? /* istanbul ignore next: can this happen? */ {},
replaceString:
match?.groups?.currentValue ?? match?.groups?.currentDigest
? match[0]
@ -93,7 +98,7 @@ function processRecursive(parameters: RecursionParameter): PackageDependency[] {
},
config
);
return result ? [result] : [];
return result ? [result] : /* istanbul ignore next: can this happen? */ [];
}
return regexMatchAll(regexes[index], content).flatMap((match) => {
return processRecursive({

View file

@ -71,7 +71,9 @@ export function extractPackageFile(
// Image name/tags for services are only eligible for update if they don't
// use variables and if the image is not built locally
const deps = Object.values(services || {})
const deps = Object.values(
services || /* istanbul ignore next: can never happen */ {}
)
.filter((service) => is.string(service?.image) && !service?.build)
.map((service) => {
const dep = getDep(service.image, true, extractConfig.registryAliases);

View file

@ -28,4 +28,4 @@ empty_key:
coreImage:
registry: docker.io
repository: bitnami/harbor-core
tag: 2.1.3-debian-10-r38
version: 2.1.3-debian-10-r38

View file

@ -1,6 +1,7 @@
import { quote } from 'shlex';
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { exec } from '../../../util/exec';
import type { ExecOptions, ToolConstraint } from '../../../util/exec/types';
import { readLocalFile } from '../../../util/fs';
@ -66,7 +67,7 @@ export async function updateArtifacts(
const res: UpdateArtifactsResult[] = [];
for (const f of status.modified ?? []) {
for (const f of coerceArray(status.modified)) {
res.push({
file: {
type: 'addition',
@ -75,7 +76,7 @@ export async function updateArtifacts(
},
});
}
for (const f of status.not_added ?? []) {
for (const f of coerceArray(status.not_added)) {
res.push({
file: {
type: 'addition',
@ -84,7 +85,7 @@ export async function updateArtifacts(
},
});
}
for (const f of status.deleted ?? []) {
for (const f of coerceArray(status.deleted)) {
res.push({
file: {
type: 'deletion',

View file

@ -1,6 +1,7 @@
import { join } from 'upath';
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { coerceString } from '../../../util/string';
import { parseUrl } from '../../../util/url';
import type { PackageDependency, PackageFileContent } from '../types';
import type { Dependency, JsonnetFile } from './types';
@ -52,7 +53,7 @@ function extractDependency(dependency: Dependency): PackageDependency | null {
const depName = join(
gitRemote.host,
gitRemote.pathname.replace(/\.git$/, ''),
dependency.source.git.subdir ?? ''
coerceString(dependency.source.git.subdir)
);
return {

View file

@ -3,6 +3,10 @@ import { extractPackageFile } from '.';
describe('modules/manager/mint/extract', () => {
describe('extractPackageFile()', () => {
it('returns null for empty', () => {
expect(extractPackageFile('')).toBeNull();
});
it('Mintfile With Version Description', () => {
const res = extractPackageFile(codeBlock`
SwiftGen/SwiftGen@6.6.1

View file

@ -11,6 +11,7 @@ describe('modules/manager/woodpecker/extract', () => {
it('returns null for non-object YAML', () => {
expect(extractPackageFile('nothing here', '', {})).toBeNull();
expect(extractPackageFile('clone: null', '', {})).toBeNull();
});
it('returns null for malformed YAML', () => {

View file

@ -42,6 +42,9 @@ describe('modules/platform/codecommit/index', () => {
});
beforeEach(() => {
delete process.env.AWS_REGION;
delete process.env.AWS_ACCESS_KEY_ID;
delete process.env.AWS_SECRET_ACCESS_KEY;
codeCommitClient.reset();
config.prList = undefined;
config.repository = undefined;
@ -70,7 +73,6 @@ describe('modules/platform/codecommit/index', () => {
});
it('should init with env vars', async () => {
const temp = process.env.AWS_REGION;
process.env.AWS_REGION = 'REGION';
await expect(
codeCommit.initPlatform({
@ -80,7 +82,6 @@ describe('modules/platform/codecommit/index', () => {
).resolves.toEqual({
endpoint: 'https://git-codecommit.REGION.amazonaws.com/',
});
process.env.AWS_REGION = temp;
});
it('should ', async () => {
@ -588,6 +589,14 @@ describe('modules/platform/codecommit/index', () => {
const res = await codeCommit.getJsonFile('file.json');
expect(res).toEqual({ foo: 'bar' });
});
it('returns null', async () => {
codeCommitClient
.on(GetFileCommand)
.resolvesOnce({ fileContent: undefined });
const res = await codeCommit.getJsonFile('file.json');
expect(res).toBeNull();
});
});
describe('getRawFile()', () => {

View file

@ -13,6 +13,7 @@ import {
} from '../../../constants/error-messages';
import { logger } from '../../../logger';
import type { BranchStatus, PrState } from '../../../types';
import { coerceArray } from '../../../util/array';
import * as git from '../../../util/git';
import { regEx } from '../../../util/regex';
import { sanitize } from '../../../util/sanitize';
@ -163,7 +164,7 @@ export async function getPrList(): Promise<CodeCommitPr[]> {
return fetchedPrs;
}
const prIds = listPrsResponse.pullRequestIds ?? [];
const prIds = coerceArray(listPrsResponse.pullRequestIds);
for (const prId of prIds) {
const prRes = await client.getPr(prId);
@ -291,7 +292,7 @@ export async function getRepos(): Promise<string[]> {
const res: string[] = [];
const repoNames = reposRes?.repositories ?? [];
const repoNames = coerceArray(reposRes?.repositories);
for (const repo of repoNames) {
if (repo.repositoryName) {

View file

@ -11,14 +11,14 @@ export function smartTruncate(input: string, len: number): string {
}
const reMatch = re.exec(input);
if (!reMatch) {
if (!reMatch?.groups) {
return input.substring(0, len);
}
const divider = `\n\n</details>\n\n---\n\n### Configuration`;
const preNotes = reMatch.groups?.preNotes ?? '';
const releaseNotes = reMatch.groups?.releaseNotes ?? '';
const postNotes = reMatch.groups?.postNotes ?? '';
const preNotes = reMatch.groups.preNotes;
const releaseNotes = reMatch.groups.releaseNotes;
const postNotes = reMatch.groups.postNotes;
const availableLength =
len - (preNotes.length + postNotes.length + divider.length);

View file

@ -11,10 +11,6 @@ describe('modules/versioning/debian/index', () => {
Settings.now = () => dt.valueOf();
});
afterEach(() => {
jest.resetAllMocks();
});
it.each`
version | expected
${undefined} | ${false}

View file

@ -30,7 +30,7 @@ export class DebianVersioningApi extends GenericVersioningApi {
const schedule = this._distroInfo.getSchedule(
this._rollingReleases.getVersionByLts(version)
);
return (isValid && schedule && RELEASE_PROP in schedule) ?? false;
return isValid && schedule !== null && RELEASE_PROP in schedule;
}
override isStable(version: string): boolean {
@ -43,7 +43,6 @@ export class DebianVersioningApi extends GenericVersioningApi {
override getNewValue({
currentValue,
rangeStrategy,
currentVersion,
newVersion,
}: NewValueConfig): string {
if (rangeStrategy === 'pin') {
@ -83,7 +82,10 @@ export class DebianVersioningApi extends GenericVersioningApi {
// newVersion is [oldold|old|]stable
// current value is numeric
if (this._rollingReleases.has(newVersion)) {
return this._rollingReleases.schedule(newVersion)?.version ?? newVersion;
return (
this._rollingReleases.schedule(newVersion)?.version ??
/* istanbul ignore next: should never happen */ newVersion
);
}
return this._distroInfo.getVersionByCodename(newVersion);

View file

@ -2,6 +2,7 @@ import { gte, lt, lte, satisfies } from '@renovatebot/pep440';
import { parse as parseRange } from '@renovatebot/pep440/lib/specifier.js';
import { parse as parseVersion } from '@renovatebot/pep440/lib/version.js';
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { regEx } from '../../../util/regex';
import type { NewValueConfig } from '../types';
@ -28,8 +29,9 @@ type UserPolicy =
* @returns A {@link UserPolicy}
*/
function getRangePrecision(ranges: Range[]): UserPolicy {
const bound: number[] =
parseVersion((ranges[1] || ranges[0]).version)?.release ?? [];
const bound = coerceArray(
parseVersion((ranges[1] || ranges[0]).version)?.release
);
let rangePrecision = -1;
// range is defined by a single bound.
// ie. <1.2.2.3,
@ -39,7 +41,7 @@ function getRangePrecision(ranges: Range[]): UserPolicy {
}
// Range is defined by both upper and lower bounds.
if (ranges.length === 2) {
const lowerBound: number[] = parseVersion(ranges[0].version)?.release ?? [];
const lowerBound = coerceArray(parseVersion(ranges[0].version)?.release);
rangePrecision = bound.findIndex((el, index) => el > lowerBound[index]);
}
// Tune down Major precision if followed by a zero
@ -74,11 +76,12 @@ function getFutureVersion(
newVersion: string,
baseVersion?: string
): number[] {
const toRelease: number[] = parseVersion(newVersion)?.release ?? [];
const baseRelease: number[] =
parseVersion(baseVersion ?? newVersion)?.release ?? [];
const toRelease = coerceArray(parseVersion(newVersion)?.release);
const baseRelease = coerceArray(
parseVersion(baseVersion ?? newVersion)?.release
);
return baseRelease.map((_, index) => {
const toPart: number = toRelease[index] ?? 0;
const toPart = toRelease[index] ?? 0;
if (index < policy) {
return toPart;
}
@ -303,8 +306,8 @@ function updateRangeValue(
return range.operator + futureVersion + '.*';
}
if (range.operator === '~=') {
const baseVersion = parseVersion(range.version)?.release ?? [];
const futureVersion = parseVersion(newVersion)?.release ?? [];
const baseVersion = coerceArray(parseVersion(range.version)?.release);
const futureVersion = coerceArray(parseVersion(newVersion)?.release);
const baseLen = baseVersion.length;
const newVerLen = futureVersion.length;
// trim redundant trailing version specifiers
@ -410,7 +413,7 @@ function handleWidenStrategy(
return newRanges.map((range) => {
// newVersion is over the upper bound
if (range.operator === '<' && gte(newVersion, range.version)) {
const upperBound = parseVersion(range.version)?.release ?? [];
const upperBound = coerceArray(parseVersion(range.version)?.release);
const len = upperBound.length;
// Match the precision of the smallest specifier if other than 0
if (upperBound[len - 1] !== 0) {
@ -474,7 +477,7 @@ function handleReplaceStrategy(
return '>=' + newVersion;
}
// update the lower bound to reflect the accepted new version
const lowerBound = parseVersion(range.version)?.release ?? [];
const lowerBound = coerceArray(parseVersion(range.version)?.release);
const rangePrecision = lowerBound.length - 1;
let newBase = getFutureVersion(rangePrecision, newVersion);
if (trimZeros) {

View file

@ -20,7 +20,7 @@ class RedhatVersioningApi extends GenericVersioningApi {
const { major, minor, patch, releaseMajor, releaseMinor } = matches;
const release = [
typeof major === 'undefined' ? 0 : Number.parseInt(major, 10),
Number.parseInt(major, 10),
typeof minor === 'undefined' ? 0 : Number.parseInt(minor, 10),
typeof patch === 'undefined' ? 0 : Number.parseInt(patch, 10),
typeof releaseMajor === 'undefined'

View file

@ -1,5 +1,6 @@
import type { RangeStrategy } from '../../../types/versioning';
import { regEx } from '../../../util/regex';
import { coerceString } from '../../../util/string';
import { api as npm } from '../npm';
import { api as pep440 } from '../pep440';
import type { NewValueConfig, VersioningApi } from '../types';
@ -160,10 +161,12 @@ function getNewValue({
const lowerAscVersionCurrent = matchAscRange.groups.range_lower_asc_version;
const upperAscVersionCurrent = matchAscRange.groups.range_upper_asc_version;
const [lowerBoundAscPep440, upperBoundAscPep440] = pep440Value.split(', ');
const lowerAscVersionNew =
regEx(versionGroup).exec(lowerBoundAscPep440)?.[0] ?? '';
const upperAscVersionNew =
regEx(versionGroup).exec(upperBoundAscPep440)?.[0] ?? '';
const lowerAscVersionNew = coerceString(
regEx(versionGroup).exec(lowerBoundAscPep440)?.[0]
);
const upperAscVersionNew = coerceString(
regEx(versionGroup).exec(upperBoundAscPep440)?.[0]
);
const lowerBoundAscNew = lowerBoundAscCurrent.replace(
lowerAscVersionCurrent,
lowerAscVersionNew
@ -189,10 +192,12 @@ function getNewValue({
const [lowerBoundDescPep440, upperBoundDescPep440] =
pep440Value.split(', ');
const upperDescVersionNew =
regEx(versionGroup).exec(upperBoundDescPep440)?.[0] ?? '';
const lowerDescVersionNew =
regEx(versionGroup).exec(lowerBoundDescPep440)?.[0] ?? '';
const upperDescVersionNew = coerceString(
regEx(versionGroup).exec(upperBoundDescPep440)?.[0]
);
const lowerDescVersionNew = coerceString(
regEx(versionGroup).exec(lowerBoundDescPep440)?.[0]
);
const upperBoundDescNew = upperBoundDescCurrent.replace(
upperDescVersionCurrent,
upperDescVersionNew

View file

@ -61,6 +61,7 @@ describe('modules/versioning/swift/index', () => {
versions | range | expected
${['1.2.3', '1.2.4', '1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'}
${['v1.2.3', 'v1.2.4', 'v1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'}
${['v1.2.3', 'v1.2.4', 'v1.2.5']} | ${''} | ${null}
`(
'minSatisfyingVersion($versions, "$range") === "$expected"',
({ versions, range, expected }) => {
@ -73,6 +74,7 @@ describe('modules/versioning/swift/index', () => {
${['1.2.3', '1.2.4', '1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'}
${['v1.2.3', 'v1.2.4', 'v1.2.5']} | ${'..<"1.2.4"'} | ${'1.2.3'}
${['1.2.3', '1.2.4', '1.2.5']} | ${'..."1.2.4"'} | ${'1.2.4'}
${['1.2.3', '1.2.4', '1.2.5']} | ${''} | ${null}
`(
'getSatisfyingVersion($versions, "$range") === "$expected"',
({ versions, range, expected }) => {
@ -86,6 +88,7 @@ describe('modules/versioning/swift/index', () => {
${'v1.2.3'} | ${'..."1.2.4"'} | ${false}
${'1.2.3'} | ${'"1.2.4"...'} | ${true}
${'v1.2.3'} | ${'"1.2.4"...'} | ${true}
${'v1.2.3'} | ${''} | ${false}
`(
'isLessThanRange("$version", "$range") === "$expected"',
({ version, range, expected }) => {
@ -99,6 +102,7 @@ describe('modules/versioning/swift/index', () => {
${'v1.2.4'} | ${'..."1.2.4"'} | ${true}
${'1.2.4'} | ${'..."1.2.3"'} | ${false}
${'v1.2.4'} | ${'..."1.2.3"'} | ${false}
${'v1.2.4'} | ${''} | ${false}
`(
'matches("$version", "$range") === "$expected"',
({ version, range, expected }) => {

View file

@ -1,4 +1,4 @@
import { isNotNullOrUndefined } from './array';
import { isNotNullOrUndefined, toArray } from './array';
describe('util/array', () => {
it.each`
@ -9,4 +9,13 @@ describe('util/array', () => {
`('.isNotNullOrUndefined', ({ a, exp }) => {
expect(isNotNullOrUndefined(a)).toEqual(exp);
});
it.each`
a | exp
${null} | ${[null]}
${undefined} | ${[undefined]}
${[]} | ${[]}
`('.toArray', ({ a, exp }) => {
expect(toArray(a)).toEqual(exp);
});
});

View file

@ -19,3 +19,12 @@ export function isNotNullOrUndefined<T>(
): value is T {
return !is.nullOrUndefined(value);
}
/**
* Converts a single value or an array of values to an array of values.
* @param value a single value or an array of values
* @returns array of values
*/
export function toArray<T>(value: T | T[]): T[] {
return is.array(value) ? value : [value];
}

View file

@ -1,4 +1,4 @@
import { looseEquals, replaceAt } from './string';
import { coerceString, looseEquals, replaceAt } from './string';
describe('util/string', () => {
describe('replaceAt', () => {
@ -32,4 +32,11 @@ describe('util/string', () => {
expect(looseEquals(null, '')).toBeFalse();
});
});
it('coerceString', () => {
expect(coerceString('foo')).toBe('foo');
expect(coerceString('')).toBe('');
expect(coerceString(undefined)).toBe('');
expect(coerceString(null)).toBe('');
});
});

View file

@ -82,3 +82,7 @@ export function copystr(x: string): string {
buf.write(x, 'utf8');
return buf.toString('utf8');
}
export function coerceString(val: string | null | undefined): string {
return val ?? '';
}

View file

@ -1,4 +1,5 @@
import type { RenovateSharedConfig } from '../../../config/types';
import { coerceString } from '../../../util/string';
import type { CommitMessage } from './commit-message';
import { CustomCommitMessage } from './custom-commit-message';
import { SemanticCommitMessage } from './semantic-commit-message';
@ -29,8 +30,8 @@ export class CommitMessageFactory {
private createSemanticCommitMessage(): SemanticCommitMessage {
const message = new SemanticCommitMessage();
message.type = this._config.semanticCommitType ?? '';
message.scope = this._config.semanticCommitScope ?? '';
message.type = coerceString(this._config.semanticCommitType);
message.scope = coerceString(this._config.semanticCommitScope);
return message;
}

View file

@ -27,11 +27,11 @@ export class SemanticCommitMessage extends CommitMessage {
static fromString(value: string): SemanticCommitMessage | undefined {
const match = value.match(SemanticCommitMessage.REGEXP);
if (!match) {
if (!match?.groups) {
return undefined;
}
const { groups = {} } = match;
const { groups } = match;
const message = new SemanticCommitMessage();
message.type = groups.type;
message.scope = groups.scope;