import { codeBlock } from 'common-tags'; import upath from 'upath'; import { mockDeep } from 'vitest-mock-extended'; import { envMock, mockExecAll, mockExecSequence } from '~test/exec-util.ts'; import { Fixtures } from '~test/fixtures.ts'; import { env, fs, git, partial } from '~test/util.ts'; import { GlobalConfig } from '../../../config/global.ts'; import type { InternalGlobalConfigOptions, RepoGlobalConfig, } from '../../../config/types.ts'; import { logger } from '../../../logger/index.ts'; import * as docker from '../../../util/exec/docker/index.ts'; import type { StatusResult } from '../../../util/git/types.ts'; import * as _datasource from '../../datasource/index.ts'; import type { UpdateArtifactsConfig, Upgrade } from '../types.ts'; import { constructPipCompileCmd } from './artifacts.ts'; import { extractHeaderCommand } from './common.ts'; import { updateArtifacts } from './index.ts'; const datasource = vi.mocked(_datasource); vi.mock('../../../util/exec/env.ts'); vi.mock('../../../util/fs/index.ts'); vi.mock('../../../util/http/index.ts'); vi.mock('../../datasource/index.ts', () => mockDeep()); const requirementsWithUv = `# This file was autogenerated by uv via the following command: # uv pip compile --generate-hashes --output-file=requirements.txt --python-version=3.11 --no-emit-package=cffi --universal requirements.in attrs==21.2.0 \ --hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \ --hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb # via -r requirements.in setuptools==59.2.0 \ --hash=sha256:157d21de9d055ab9e8ea3186d91e7f4f865e11f42deafa952d90842671fc2576 \ --hash=sha256:4adde3d1e1c89bde1c643c64d89cdd94cbfd8c75252ee459d4500bccb9c7d05d # via -r requirements.in `; function getCommandInHeader(command: string) { return `# # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # ${command} # `; } function getCommandInUvHeader(command: string) { return `# This file was autogenerated by uv via the following command: # ${command} `; } const simpleHeader = getCommandInHeader('pip-compile requirements.in'); const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = { // `join` fixes Windows CI localDir: upath.join('/tmp/github/some/repo'), cacheDir: upath.join('/tmp/renovate/cache'), containerbaseDir: upath.join('/tmp/renovate/cache/containerbase'), binarySource: 'global', }; const dockerAdminConfig = { ...adminConfig, binarySource: 'docker', dockerSidecarImage: 'ghcr.io/renovatebot/base-image', } satisfies RepoGlobalConfig & InternalGlobalConfigOptions; process.env.CONTAINERBASE = 'true'; const config: UpdateArtifactsConfig = {}; const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true }; describe('modules/manager/pip-compile/artifacts', () => { beforeEach(() => { env.getChildProcessEnv.mockReturnValue({ ...envMock.basic, LANG: 'en_US.UTF-8', LC_ALL: 'en_US', }); GlobalConfig.set(adminConfig); docker.resetPrefetchedImages(); }); it('returns if no requirements.txt found', async () => { const execSnapshots = mockExecAll(); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: '', config: { ...config, lockFiles: ['requirements.txt'], }, }), ).resolves.toBeNull(); expect(execSnapshots).toEqual([]); }); it('returns null if all unchanged', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); const execSnapshots = mockExecAll(); fs.readLocalFile.mockResolvedValueOnce('new lock'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, lockFiles: ['requirements.txt'], }, }), ).resolves.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pip-compile requirements.in' }, ]); }); it('returns null if no config.lockFiles', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); fs.readLocalFile.mockResolvedValueOnce('new lock'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, }, }), ).resolves.toBeNull(); expect(logger.warn).toHaveBeenCalledWith( { packageFileName: 'requirements.in' }, 'pip-compile: No lock files associated with a package file', ); }); it('returns updated requirements.txt', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce('new lock'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.7' }, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pip-compile requirements.in' }, ]); }); it('supports docker mode', async () => { GlobalConfig.set(dockerAdminConfig); // pip-tools datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '6.13.0' }], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.10.2' }, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'docker pull ghcr.io/renovatebot/base-image' }, { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, { cmd: 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' + '-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' + '-e CI ' + '-e PIP_CACHE_DIR ' + '-e PIP_NO_INPUT ' + '-e PIP_KEYRING_PROVIDER ' + '-e PYTHON_KEYRING_BACKEND ' + '-e CONTAINERBASE_CACHE_DIR ' + '-w "/tmp/github/some/repo" ' + 'ghcr.io/renovatebot/base-image ' + "bash -l -c '" + 'install-tool python 3.10.2 ' + '&& ' + 'install-tool pip-tools 6.13.0 ' + '&& ' + 'pip-compile requirements.in' + "'", }, ]); }); it('supports install mode', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); // pip-tools datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '6.13.0' }], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.10.2' }, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.10.2' }, { cmd: 'install-tool pip-tools 6.13.0' }, { cmd: 'pip-compile requirements.in', options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('installs Python version according to the lock file', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '3.11.0' }, { version: '3.11.1' }, { version: '3.12.0' }, ], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce(simpleHeader); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { pipTools: '6.13.0' }, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.11.1' }, { cmd: 'install-tool pip-tools 6.13.0' }, { cmd: 'pip-compile requirements.in', options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('installs Python version according to the uv option', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '3.11.0' }, { version: '3.11.1' }, { version: '3.12.0' }, ], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce( getCommandInUvHeader( 'uv pip compile --python-version=3.11 requirements.in', ), ); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, lockFiles: ['requirements.txt'], constraints: { uv: '0.5.27' }, }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.11.1' }, { cmd: 'install-tool uv 0.5.27' }, { cmd: 'uv pip compile --python-version=3.11 requirements.in', options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('install uv tools without constraints', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); // python datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '3.12.0' }], }); // uv datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '0.5.27' }], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce( getCommandInUvHeader('uv pip compile requirements.in'), ); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.12.0' }, { cmd: 'install-tool uv 0.5.27' }, { cmd: 'uv pip compile requirements.in', options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('installs latest Python version if no constraints and not in header', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '3.11.0' }, { version: '3.11.1' }, { version: '3.12.0' }, ], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); // Before 6.2.0, pip-compile didn't include Python version in header const noPythonVersionHeader = codeBlock` # # This file is autogenerated by pip-compile # To update, run: # # pip-compile requirements.in # `; fs.readLocalFile.mockResolvedValueOnce(noPythonVersionHeader); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { pipTools: '6.13.0' }, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.12.0' }, { cmd: 'install-tool pip-tools 6.13.0' }, { cmd: 'pip-compile requirements.in', options: { cwd: '/tmp/github/some/repo' }, }, ]); }); it('catches errors', async () => { const execSnapshots = mockExecAll(); fs.readLocalFile.mockResolvedValueOnce('Current requirements.txt'); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); fs.writeLocalFile.mockImplementationOnce(() => { throw new Error('not found'); }); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: '{}', config: { ...config, lockFiles: ['requirements.txt'] }, }), ).resolves.toEqual([ { artifactError: { fileName: 'requirements.txt', stderr: 'not found' }, }, ]); expect(execSnapshots).toEqual([]); }); it('returns updated requirements.txt when doing lockfile maintenance', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce('New requirements.txt'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: '{}', config: { ...lockMaintenanceConfig, lockFiles: ['requirements.txt'] }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pip-compile requirements.in' }, ]); }); it('uses --upgrade-package only for isLockfileUpdate', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce('New requirements.txt'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [ { depName: 'foo', newVersion: '1.0.2', isLockfileUpdate: true }, { depName: 'bar', newVersion: '2.0.0' }, ] satisfies Upgrade[], newPackageFileContent: '{}', config: { ...lockMaintenanceConfig, lockFiles: ['requirements.txt'] }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pip-compile requirements.in --upgrade-package=foo==1.0.2', }, ]); }); it('uses pip-compile version from config', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce('dependency==1.2.3'); GlobalConfig.set(dockerAdminConfig); // pip-tools datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '6.13.0' }], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce('new lock'); fs.ensureCacheDir.mockResolvedValueOnce('/tmp/renovate/cache/others/pip'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '3.10.2', pipTools: '6.13.0' }, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'docker pull ghcr.io/renovatebot/base-image' }, { cmd: 'docker ps --filter name=renovate_sidecar -aq' }, { cmd: 'docker run --rm --name=renovate_sidecar --label=renovate_child ' + '-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' + '-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' + '-e CI ' + '-e PIP_CACHE_DIR ' + '-e PIP_NO_INPUT ' + '-e PIP_KEYRING_PROVIDER ' + '-e PYTHON_KEYRING_BACKEND ' + '-e CONTAINERBASE_CACHE_DIR ' + '-w "/tmp/github/some/repo" ' + 'ghcr.io/renovatebot/base-image ' + "bash -l -c '" + 'install-tool python 3.10.2 ' + '&& ' + 'install-tool pip-tools 6.13.0 ' + '&& ' + 'pip-compile requirements.in' + "'", }, ]); }); describe('constructPipCompileCmd()', () => { it('throws for garbage', () => { expect(() => constructPipCompileCmd( extractHeaderCommand( Fixtures.get('requirementsNoHeaders.txt'), 'subdir/requirements.txt', ), ), ).toThrow(/extract/); }); it('returns extracted common arguments (like those featured in the README)', () => { expect( constructPipCompileCmd( extractHeaderCommand( Fixtures.get('requirementsWithHashes.txt'), 'subdir/requirements.txt', ), ), ).toBe( 'pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --strip-extras --resolver=backtracking --output-file=requirements.txt requirements.in', ); }); it('returns extracted arguments for uv', () => { expect( constructPipCompileCmd( extractHeaderCommand(requirementsWithUv, 'subdir/requirements.txt'), ), ).toBe( 'uv pip compile --generate-hashes --output-file=requirements.txt --python-version=3.11 --no-emit-package=cffi --universal requirements.in', ); }); it('does not add --no-emit-index-url when PIP_INDEX_URL has no credentials', () => { vi.stubEnv('PIP_INDEX_URL', 'https://example.com/pypi/simple'); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile requirements.in'); }); it('returns --no-emit-index-url when credentials are found in PIP_INDEX_URL', () => { vi.stubEnv('PIP_INDEX_URL', 'https://user:pass@example.com/pypi/simple'); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url when credentials are found in PIP_EXTRA_INDEX_URL', () => { vi.stubEnv( 'PIP_EXTRA_INDEX_URL', 'https://user:pass@example.com/pypi/simple', ); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url when only a username is found in PIP_INDEX_URL', () => { vi.stubEnv('PIP_INDEX_URL', 'https://user@example.com/pypi/simple'); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url when only a username is found in PIP_EXTRA_INDEX_URL', () => { vi.stubEnv('PIP_EXTRA_INDEX_URL', 'https://user@example.com/pypi/simple'); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url when only a password is found in PIP_INDEX_URL', () => { vi.stubEnv('PIP_INDEX_URL', 'https://:pass@example.com/pypi/simple'); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url when only a password is found in PIP_EXTRA_INDEX_URL', () => { vi.stubEnv( 'PIP_EXTRA_INDEX_URL', 'https://:pass@example.com/pypi/simple', ); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url when PIP_INDEX_URL is invalid', () => { vi.stubEnv('PIP_INDEX_URL', 'invalid-url'); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url PIP_EXTRA_INDEX_URL is invalid', () => { vi.stubEnv('PIP_EXTRA_INDEX_URL', 'invalid-url'); expect( constructPipCompileCmd( extractHeaderCommand(simpleHeader, 'subdir/requirements.txt'), ), ).toBe('pip-compile --no-emit-index-url requirements.in'); }); it('returns --no-emit-index-url only once when its in the header and credentials are present in the environment', () => { vi.stubEnv( 'PIP_EXTRA_INDEX_URL', 'https://user:pass@example.com/pypi/simple', ); expect( constructPipCompileCmd( extractHeaderCommand( Fixtures.get('requirementsWithHashes.txt'), 'subdir/requirements.txt', ), ), ).toBe( 'pip-compile --allow-unsafe --generate-hashes --no-emit-index-url --strip-extras --resolver=backtracking --output-file=requirements.txt requirements.in', ); }); it('allow explicit --emit-index-url', () => { vi.stubEnv('PIP_INDEX_URL', 'https://user:pass@example.com/pypi/simple'); expect( constructPipCompileCmd( extractHeaderCommand( getCommandInHeader('pip-compile --emit-index-url requirements.in'), 'subdir/requirements.txt', ), ), ).toBe('pip-compile --emit-index-url requirements.in'); }); it('throws on unknown arguments', () => { expect(() => constructPipCompileCmd( extractHeaderCommand( Fixtures.get('requirementsWithUnknownArguments.txt'), 'subdir/requirements.txt', ), ), ).toThrow(/supported/); }); it('throws on custom command', () => { expect(() => constructPipCompileCmd( extractHeaderCommand( Fixtures.get('requirementsCustomCommand.txt'), 'subdir/requirements.txt', ), ), ).toThrow(/custom/); }); it('add --upgrade-package to command if Upgrade[] passed', () => { expect( constructPipCompileCmd( extractHeaderCommand( getCommandInHeader( 'pip-compile --output-file=requirements.txt requirements.in', ), 'subdir/requirements.txt', ), [ { depName: 'foo', newVersion: '1.0.2' }, { depName: 'bar', newVersion: '2.0.0' }, ] satisfies Upgrade[], ), ).toBe( 'pip-compile --output-file=requirements.txt requirements.in --upgrade-package=foo==1.0.2 --upgrade-package=bar==2.0.0', ); }); it('skips source file package registry extraction when source file is not pip_requirements', async () => { const header = getCommandInHeader( 'pip-compile --output-file=requirements.txt setup.cfg', ); fs.readLocalFile.mockResolvedValueOnce(header); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce('new lock'); await expect( updateArtifacts({ packageFileName: 'setup.cfg', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pip-compile --output-file=requirements.txt setup.cfg' }, ]); }); it('skips source file when readLocalFile returns null', async () => { fs.readLocalFile.mockResolvedValueOnce(simpleHeader); fs.readLocalFile.mockResolvedValueOnce(null); // source file read returns null const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['requirements.txt'], }), ); fs.readLocalFile.mockResolvedValueOnce('new lock'); await expect( updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, lockFiles: ['requirements.txt'], }, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pip-compile requirements.in' }, ]); }); it('reports errors when a lock file is unchanged', async () => { fs.readLocalFile.mockResolvedValue(simpleHeader); mockExecSequence([ new Error('Oh noes!'), { stdout: 'This one worked', stderr: '' }, ]); git.getRepoStatus.mockResolvedValue( partial({ modified: [], }), ); const results = await updateArtifacts({ packageFileName: 'requirements.in', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, lockFiles: ['requirements1.txt', 'requirements2.txt'], }, }); expect(results).toMatchObject([ { artifactError: { fileName: 'requirements1.txt', stderr: 'Oh noes!' }, }, ]); }); }); });