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'); }); }); });