Files
renovate/lib/modules/manager/pip-compile/common.spec.ts
Sebastian Poxhofer 5ad36a2387 test(linters): add linting rule to enforce usage of host rules test util (#44692)
* ci(linter): add renovate/no-host-rules-mock rule

Flags vi.mock()/vi.doMock() calls whose module specifier contains
"host-rules" in spec files. Tests should configure real rules via
hostRules.add() instead of mocking the host-rules module.

Enabled at "warn" severity instead of "error": the repo currently has
35 pre-existing violations across 34 spec files, which exceeds the
agreed fix threshold of 12 files. No violation fixes are included;
migrating each spec to real hostRules.add()/clear() setup is a
semantic test rewrite that should be done incrementally, after which
the rule can be promoted to "error".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(linter): enforce renovate/no-host-rules-mock at error

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(datasource): use real host-rules instead of mocking

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(manager): use real host-rules instead of mocking

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(platform): use real host-rules instead of mocking

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(util,workers): use real host-rules instead of mocking

Also narrow renovate/no-host-rules-mock to the host-rules module itself so that mocking e.g. host-rules-from-env is not flagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(linter): mention '~test/host-rules.ts' util in no-host-rules-mock message

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:36:12 +00:00

396 lines
11 KiB
TypeScript

import { hostRules } from '~test/host-rules.ts';
import { logger } from '../../../logger/index.ts';
import {
allowedOptions,
extractHeaderCommand,
extractPythonVersion,
getRegistryCredVarsFromPackageFiles,
matchManager,
} from './common.ts';
import { inferCommandExecDir } from './utils.ts';
function getCommandInHeader(command: string) {
return `#
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# ${command}
#
`;
}
describe('modules/manager/pip-compile/common', () => {
describe('extractHeaderCommand()', () => {
it.each([
'-v',
'--all-extras',
`--allow-unsafe`,
'--generate-hashes',
`--no-emit-index-url`,
`--strip-extras`,
'--resolver=backtracking',
'--resolver=legacy',
'--output-file=reqs.txt',
'--extra-index-url=https://pypi.org/simple',
])('returns object on correct options', (argument: string) => {
expect(
extractHeaderCommand(
getCommandInHeader(`pip-compile ${argument} reqs.in`),
'reqs.txt',
),
).toBeObject();
expect(logger.warn).toHaveBeenCalledTimes(0);
});
it.each([
'-v',
'--all-extras',
'--generate-hashes',
'--output-file=reqs.txt',
'--extra-index-url=https://pypi.org/simple',
// uv-specific options
`--no-strip-extras`,
'--universal',
'--constraints=constraints.txt',
'--constraint=constraints.txt',
'--python-version=3.13',
'--no-emit-package=cffi',
'--prerelease=if-necessary',
'--format=pylock.toml',
'--resolution=lowest',
'--fork-strategy=fewest',
'--exclude-newer=2025-11-01',
'--exclude-newer-package="tqdm=2022-04-04T00:00:00Z"',
'--group=docs',
'--override=overrides.txt',
'--overrides=overrides.txt',
])('returns object on correct uv options', (argument: string) => {
expect(
extractHeaderCommand(
getCommandInHeader(`uv pip compile ${argument} reqs.in`),
'reqs.txt',
),
).toBeObject();
expect(logger.warn).toHaveBeenCalledTimes(0);
});
it.each(['--resolver', '--output-file reqs.txt', '--extra = jupyter'])(
'errors on malformed options with argument',
(argument: string) => {
expect(() =>
extractHeaderCommand(
getCommandInHeader(`pip-compile ${argument} reqs.in`),
'reqs.txt',
),
).toThrow(/equal sign/);
},
);
it.each(['--foo', '-x', '--$(curl this)', '--bar=sus', '--extra-large'])(
'errors on unknown options',
(argument: string) => {
expect(() =>
extractHeaderCommand(
getCommandInHeader(`pip-compile ${argument} reqs.in`),
'reqs.txt',
),
).toThrow(/not supported/);
},
);
it.each(['--no-header'])(
'always errors on not allowed options',
(argument: string) => {
expect(() =>
extractHeaderCommand(
getCommandInHeader(`pip-compile ${argument} reqs.in`),
'reqs.txt',
),
).toThrow(/not allowed/);
},
);
it.each(['--output-file', '--index-url'])(
'throws on duplicate options',
(argument: string) => {
expect(() =>
extractHeaderCommand(
getCommandInHeader(
`pip-compile ${argument}=xxx ${argument}=xxx reqs.in`,
),
'reqs.txt',
),
).toThrow(/multiple/);
},
);
it('throws when no source files passed as arguments', () => {
expect(() =>
extractHeaderCommand(
getCommandInHeader(`pip-compile --extra=color`),
'reqs.txt',
),
).toThrow(/source/);
});
it('throws on malformed header', () => {
expect(() => extractHeaderCommand('Dd', 'reqs.txt')).toThrow(/extract/);
});
it('throws on mutually exclusive options', () => {
expect(() =>
extractHeaderCommand(
getCommandInHeader(
`pip-compile --no-emit-index-url --emit-index-url reqs.in`,
),
'reqs.txt',
),
).toThrow('Cannot use both --no-emit-index-url and --emit-index-url');
});
it('returned sourceFiles returns all source files', () => {
const exampleSourceFiles = [
'requirements.in',
'reqs/testing.in',
'base.txt',
'./lib/setup.py',
'pyproject.toml',
];
expect(
extractHeaderCommand(
getCommandInHeader(
`pip-compile --extra=color ${exampleSourceFiles.join(' ')}`,
),
'reqs.txt',
).sourceFiles,
).toEqual(exampleSourceFiles);
});
it.each(allowedOptions['pip-compile'])(
'returned sourceFiles must not contain options (pip-compile)',
(argument: string) => {
const sourceFiles = extractHeaderCommand(
getCommandInHeader(`pip-compile ${argument}=reqs.txt reqs.in`),
'reqs.txt',
).sourceFiles;
expect(sourceFiles).not.toContainEqual(argument);
expect(sourceFiles).toEqual(['reqs.in']);
},
);
it.each(allowedOptions.uv)(
'returned sourceFiles must not contain options (uv)',
(argument: string) => {
const sourceFiles = extractHeaderCommand(
getCommandInHeader(`uv pip compile ${argument}=reqs.txt reqs.in`),
'reqs.txt',
).sourceFiles;
expect(sourceFiles).not.toContainEqual(argument);
expect(sourceFiles).toEqual(['reqs.in']);
},
);
it('detects custom command', () => {
expect(
extractHeaderCommand(
getCommandInHeader(`./pip-compile-wrapper reqs.in`),
'reqs.txt',
),
).toHaveProperty('commandType', 'custom');
});
it.each([
{ path: 'reqs.txt', arg: 'reqs.txt', result: '.' },
{ path: 'subdir/reqs.txt', arg: 'subdir/reqs.txt', result: '.' },
{ path: 'subdir/reqs.txt', arg: './subdir/reqs.txt', result: '.' },
{ path: 'subdir/reqs.txt', arg: 'reqs.txt', result: 'subdir' },
{ path: 'subdir/reqs.txt', arg: './reqs.txt', result: 'subdir' },
])(
'infer exec directory (cwd) from output file path and header command',
({ path, arg, result }) => {
expect(inferCommandExecDir(path, arg)).toEqual(result);
},
);
});
describe('extractPythonVersion()', () => {
it('extracts Python version from valid header', () => {
expect(
extractPythonVersion(
getCommandInHeader('pip-compile reqs.in'),
'reqs.txt',
),
).toBe('3.11');
});
it('returns undefined if version cannot be extracted', () => {
expect(extractPythonVersion('', 'reqs.txt')).toBeUndefined();
});
});
describe('getRegistryCredVarsFromPackageFiles()', () => {
it('handles both registryUrls and additionalRegistryUrls', () => {
hostRules.add({
matchHost: 'example.com',
username: 'user1',
password: 'password1',
});
hostRules.add({
matchHost: 'example2.com',
username: 'user2',
password: 'password2',
});
expect(
getRegistryCredVarsFromPackageFiles([
{
deps: [],
registryUrls: ['https://example.com/pypi/simple'],
additionalRegistryUrls: ['https://example2.com/pypi/simple'],
},
]),
).toEqual({
KEYRING_SERVICE_NAME_0: 'example.com',
KEYRING_SERVICE_USERNAME_0: 'user1',
KEYRING_SERVICE_PASSWORD_0: 'password1',
KEYRING_SERVICE_NAME_1: 'example2.com',
KEYRING_SERVICE_USERNAME_1: 'user2',
KEYRING_SERVICE_PASSWORD_1: 'password2',
});
});
it('handles multiple additionalRegistryUrls', () => {
hostRules.add({
matchHost: 'example.com',
username: 'user1',
password: 'password1',
});
hostRules.add({
matchHost: 'example2.com',
username: 'user2',
password: 'password2',
});
expect(
getRegistryCredVarsFromPackageFiles([
{
deps: [],
additionalRegistryUrls: [
'https://example.com/pypi/simple',
'https://example2.com/pypi/simple',
],
},
]),
).toEqual({
KEYRING_SERVICE_NAME_0: 'example.com',
KEYRING_SERVICE_USERNAME_0: 'user1',
KEYRING_SERVICE_PASSWORD_0: 'password1',
KEYRING_SERVICE_NAME_1: 'example2.com',
KEYRING_SERVICE_USERNAME_1: 'user2',
KEYRING_SERVICE_PASSWORD_1: 'password2',
});
});
it('handles hosts with only a username', () => {
hostRules.add({
username: 'user',
});
expect(
getRegistryCredVarsFromPackageFiles([
{
deps: [],
additionalRegistryUrls: ['https://example.com/pypi/simple'],
},
]),
).toEqual({
KEYRING_SERVICE_NAME_0: 'example.com',
KEYRING_SERVICE_USERNAME_0: 'user',
KEYRING_SERVICE_PASSWORD_0: '',
});
});
it('handles hosts with only a password', () => {
hostRules.add({
password: 'password',
});
expect(
getRegistryCredVarsFromPackageFiles([
{
deps: [],
additionalRegistryUrls: ['https://example.com/pypi/simple'],
},
]),
).toEqual({
KEYRING_SERVICE_NAME_0: 'example.com',
KEYRING_SERVICE_USERNAME_0: '',
KEYRING_SERVICE_PASSWORD_0: 'password',
});
});
it('handles invalid URLs', () => {
expect(
getRegistryCredVarsFromPackageFiles([
{
deps: [],
additionalRegistryUrls: ['invalid-url'],
},
]),
).toEqual({});
});
});
it('handles multiple package files', () => {
hostRules.add({
matchHost: 'example.com',
username: 'user1',
password: 'password1',
});
hostRules.add({
matchHost: 'example2.com',
username: 'user2',
password: 'password2',
});
expect(
getRegistryCredVarsFromPackageFiles([
{
deps: [],
registryUrls: ['https://example.com/pypi/simple'],
},
{
deps: [],
additionalRegistryUrls: ['https://example2.com/pypi/simple'],
},
]),
).toEqual({
KEYRING_SERVICE_NAME_0: 'example.com',
KEYRING_SERVICE_USERNAME_0: 'user1',
KEYRING_SERVICE_PASSWORD_0: 'password1',
KEYRING_SERVICE_NAME_1: 'example2.com',
KEYRING_SERVICE_USERNAME_1: 'user2',
KEYRING_SERVICE_PASSWORD_1: 'password2',
});
});
describe('matchManager()', () => {
it('matches pip_setup setup.py', () => {
expect(matchManager('setup.py')).toBe('pip_setup');
});
it('matches setup-cfg setup.cfg', () => {
expect(matchManager('setup.cfg')).toBe('setup-cfg');
});
it('matches pep621 pyproject.toml', () => {
expect(matchManager('pyproject.toml')).toBe('pep621');
});
it('matches pip_requirements any .in file', () => {
expect(matchManager('file.in')).toBe('pip_requirements');
expect(matchManager('another_file.in')).toBe('pip_requirements');
});
it('matches pip_requirements any .txt file', () => {
expect(matchManager('file.txt')).toBe('pip_requirements');
expect(matchManager('another_file.txt')).toBe('pip_requirements');
});
});
});