import type { Stats } from 'node:fs'; import * as _fsExtra from 'fs-extra'; import upath from 'upath'; import type { MockInstance } from 'vitest'; import { mockDeep } from 'vitest-mock-extended'; import { envMock, mockExecAll } from '~test/exec-util.ts'; import { Fixtures } from '~test/fixtures.ts'; import { hostRules } from '~test/host-rules.ts'; import { env, 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 { ExtraEnv, Opt } from '../../../util/exec/types.ts'; import type { StatusResult } from '../../../util/git/types.ts'; import * as _datasource from '../../datasource/index.ts'; import type { UpdateArtifactsConfig } from '../types.ts'; import { addExtraEnvVariable, extractEnvironmentVariableName, getMatchingHostRule, } from './artifacts.ts'; import { updateArtifacts } from './index.ts'; import type { PipfileLock } from './types.ts'; // mock for cjs require for `@renovatebot/detect-tools` // https://github.com/vitest-dev/vitest/discussions/3134 vi.hoisted(() => { const fsExtraModule: Partial = { exports: fixtures.fsExtra(), }; require.cache[require.resolve('fs-extra')] = fsExtraModule as NodeJS.Module; }); vi.mock('fs-extra', () => fixtures.fsExtra()); vi.mock('../../../util/exec/env.ts'); vi.mock('../../../util/git/index.ts', () => mockDeep()); vi.mock('../../../util/http/index.ts', () => mockDeep()); vi.mock('../../datasource/index.ts', () => mockDeep()); const datasource = vi.mocked(_datasource); const fsExtra = vi.mocked(_fsExtra); // vi.mocked() resolves stat() to its callback overload, so // mockResolvedValueOnce() would expect void; retype via the promise overload const statMock = fsExtra.stat as unknown as MockInstance< (path: string) => Promise >; process.env.CONTAINERBASE = 'true'; const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = { // `join` fixes Windows CI localDir: upath.join(upath.join('/tmp/github/some/repo')), cacheDir: upath.join(upath.join('/tmp/renovate/cache')), containerbaseDir: upath.join(upath.join('/tmp/renovate/cache/containerbase')), binarySource: 'global', }; const dockerAdminConfig = { ...adminConfig, binarySource: 'docker', dockerSidecarImage: 'ghcr.io/renovatebot/base-image', } satisfies RepoGlobalConfig & InternalGlobalConfigOptions; const config: UpdateArtifactsConfig = {}; const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true }; const pipenvCacheDir = upath.join('/tmp/renovate/cache/others/pipenv'); const pipCacheDir = upath.join('/tmp/renovate/cache/others/pip'); const virtualenvsCacheDir = upath.join( '/tmp/renovate/cache/others/virtualenvs', ); type MockFiles = Record; function mockFiles(mockFiles: MockFiles): void { fsExtra.readFile.mockImplementation(((name: string) => { for (const [key, value] of Object.entries(mockFiles)) { if (name.endsWith(key)) { if (!Array.isArray(value)) { return value; } if (value.length > 1) { return value.shift(); } return value[0]; } } throw new Error('File not found'); }) as never); } describe('modules/manager/pipenv/artifacts', () => { beforeEach(() => { Fixtures.reset(); env.getChildProcessEnv.mockReturnValue({ ...envMock.basic, LANG: 'en_US.UTF-8', LC_ALL: 'en_US', }); GlobalConfig.set(adminConfig); docker.resetPrefetchedImages(); // python datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '3.6.2' }, { version: '3.6.5' }, { version: '3.7.6' }, { version: '3.8.5' }, { version: '3.9.1' }, { version: '3.10.2' }, ], }); // pipenv datasource.getPkgReleases.mockResolvedValueOnce({ releases: [ { version: '2013.5.19' }, { version: '2013.6.11' }, { version: '2013.6.12' }, ], }); }); it('returns if no Pipfile.lock found', async () => { await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: '', config, }), ).resolves.toBeNull(); }); it('returns null if unchanged', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile.lock': JSON.stringify({ _meta: { requires: { python_full_version: '3.7.6' }, }, } satisfies PipfileLock), }); const execSnapshots = mockExecAll(); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config, }), ).resolves.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('gets python full version from Pipfile', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile': Fixtures.get('Pipfile1'), '/Pipfile.lock': JSON.stringify({ _meta: { requires: { python_full_version: '3.7.6' }, }, } satisfies PipfileLock), }); fsExtra.ensureDir.mockResolvedValue(undefined); const execSnapshots = mockExecAll(); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: Fixtures.get('Pipfile1'), config, }), ).resolves.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.6.2' }, {}, { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], ]); }); it('gets python version from Pipfile', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile': Fixtures.get('Pipfile2'), '/Pipfile.lock': JSON.stringify({ _meta: { requires: { python_full_version: '3.7.6' }, }, } satisfies PipfileLock), }); fsExtra.ensureDir.mockResolvedValue(undefined); const execSnapshots = mockExecAll(); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: Fixtures.get('Pipfile2'), config, }), ).resolves.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.6.5' }, {}, { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], ]); }); it('gets full python version from .python-version', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile.lock': '{}', '/.python-version': '3.7.6', }); fsExtra.ensureDir.mockResolvedValue(undefined); const execSnapshots = mockExecAll(); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some toml', config, }), ).resolves.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.7.6' }, {}, { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/.python-version')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('gets python stream, from .python-version', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); statMock.mockResolvedValueOnce(partial()); fsExtra.ensureDir.mockResolvedValue(undefined); mockFiles({ '/Pipfile.lock': '{}', '/.python-version': '3.8', }); const execSnapshots = mockExecAll(); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some toml', config, }), ).resolves.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.8.5' }, {}, { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/.python-version')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('handles no constraint', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile.lock': 'unparseable pipfile lock', }); const execSnapshots = mockExecAll(); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config, }), ).resolves.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, PIP_CACHE_DIR: pipCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/.python-version')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('returns updated Pipfile.lock', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile.lock': ['current pipfile.lock', 'new pipfile.lock'], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { python: '== 3.8.*' } }, }), ).resolves.toEqual([ { file: { contents: 'new pipfile.lock', path: 'Pipfile.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, PIP_CACHE_DIR: pipCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('supports docker mode', async () => { GlobalConfig.set(dockerAdminConfig); statMock.mockResolvedValueOnce(partial()); const pipFileLock = JSON.stringify({ _meta: { requires: { python_version: '3.7' } }, } satisfies PipfileLock); mockFiles({ '/Pipfile.lock': [pipFileLock, pipFileLock, 'new lock'], }); fsExtra.ensureDir.mockResolvedValue(undefined); // pipenv datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2023.1.2' }], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config, }), ).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 PIPENV_CACHE_DIR ' + '-e PIP_CACHE_DIR ' + '-e WORKON_HOME ' + '-e CONTAINERBASE_CACHE_DIR ' + '-w "/tmp/github/some/repo" ' + 'ghcr.io/renovatebot/base-image' + " bash -l -c '" + 'install-tool python 3.7.6' + ' && ' + 'install-tool pipenv 2013.6.12' + ' && ' + 'pipenv lock' + "'", options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, PIP_CACHE_DIR: pipCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('supports install mode', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); statMock.mockResolvedValueOnce(partial()); const pipFileLock = JSON.stringify({ _meta: { requires: { python_version: '3.6' } }, } satisfies PipfileLock); mockFiles({ '/Pipfile.lock': [pipFileLock, 'new lock'], }); fsExtra.ensureDir.mockResolvedValue(undefined); // pipenv datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2023.1.2' }], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.6.5' }, { cmd: 'install-tool pipenv 2013.6.12' }, { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, PIP_CACHE_DIR: pipCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('defaults to latest if no lock constraints', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'install' }); statMock.mockResolvedValueOnce(partial()); fsExtra.ensureDir.mockResolvedValue(undefined); mockFiles({ '/Pipfile.lock': ['{}', 'new lock'], }); // pipenv datasource.getPkgReleases.mockResolvedValueOnce({ releases: [{ version: '2023.1.2' }], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config, }), ).resolves.not.toBeNull(); expect(execSnapshots).toMatchObject([ { cmd: 'install-tool python 3.10.2' }, { cmd: 'install-tool pipenv 2013.6.12' }, { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, PIP_CACHE_DIR: pipCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/.python-version')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('catches errors', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile.lock': 'Current Pipfile.lock', }); fsExtra.outputFile.mockImplementationOnce(() => { throw new Error('not found'); }); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: '{}', config, }), ).resolves.toEqual([ { artifactError: { fileName: 'Pipfile.lock', stderr: 'not found' } }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([]); expect(fsExtra.readFile.mock.calls).toEqual([]); }); it('returns updated Pipenv.lock when doing lockfile maintenance', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); fsExtra.remove.mockResolvedValue(); mockFiles({ '/Pipfile.lock': ['Current Pipfile.lock', 'New Pipfile.lock'], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: '{}', config: lockMaintenanceConfig, }), ).resolves.toEqual([ { file: { contents: 'New Pipfile.lock', path: 'Pipfile.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, PIP_CACHE_DIR: pipCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); }); it('uses pipenv version from Pipfile', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); GlobalConfig.set(dockerAdminConfig); const oldLock = JSON.stringify({ default: { pipenv: { version: '==2020.8.13' } }, } satisfies PipfileLock); mockFiles({ '/Pipfile.lock': [oldLock, oldLock, 'new lock'], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config, }), ).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 PIPENV_CACHE_DIR ' + '-e PIP_CACHE_DIR ' + '-e WORKON_HOME ' + '-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 pipenv 2020.8.13' + ' && ' + 'pipenv lock' + "'", options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/.python-version')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('uses pipenv version from Pipfile dev packages', async () => { GlobalConfig.set(dockerAdminConfig); statMock.mockResolvedValueOnce(partial()); fsExtra.ensureDir.mockResolvedValue(undefined); const oldLock = JSON.stringify({ develop: { pipenv: { version: '==2020.8.13' } }, } satisfies PipfileLock) as never; mockFiles({ '/Pipfile.lock': [oldLock, oldLock, 'new lock'], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config, }), ).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 PIPENV_CACHE_DIR ' + '-e PIP_CACHE_DIR ' + '-e WORKON_HOME ' + '-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 pipenv 2020.8.13' + ' && ' + 'pipenv lock' + "'", options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/.python-version')), 'utf8', ], [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('uses pipenv version from config', async () => { GlobalConfig.set(dockerAdminConfig); statMock.mockResolvedValueOnce(partial()); fsExtra.ensureDir.mockResolvedValue(undefined); const oldLock = JSON.stringify({ default: { pipenv: { version: '==2020.8.13' } }, } satisfies PipfileLock) as never; mockFiles({ '/Pipfile.lock': [oldLock, 'new lock'], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: 'some new content', config: { ...config, constraints: { pipenv: '==2020.1.1' } }, }), ).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 PIPENV_CACHE_DIR ' + '-e PIP_CACHE_DIR ' + '-e WORKON_HOME ' + '-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 pipenv 2020.1.1' + ' && ' + 'pipenv lock' + "'", options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, WORKON_HOME: virtualenvsCacheDir, }, }, }, ]); expect(fsExtra.ensureDir.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pipenv'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/pip'))], [expect.toEndWith(upath.join('/tmp/renovate/cache/others/virtualenvs'))], ]); expect(fsExtra.readFile.mock.calls).toEqual([ [expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile')), 'utf8'], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/.python-version')), 'utf8', ], [ expect.toEndWith(upath.join('/tmp/github/some/repo/Pipfile.lock')), 'utf8', ], ]); }); it('passes private credential environment vars', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile.lock': ['current Pipfile.lock', 'New Pipfile.lock'], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); hostRules.add({ matchHost: 'mypypi.example.com', username: 'usernameOne', password: 'passwordTwo', }); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: Fixtures.get('Pipfile6'), config: { ...config, constraints: { python: '== 3.8.*' } }, }), ).resolves.toEqual([ { file: { contents: 'New Pipfile.lock', path: 'Pipfile.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, WORKON_HOME: virtualenvsCacheDir, USERNAME: 'usernameOne', PASSWORD: 'passwordTwo', }, }, }, ]); }); it('returns no host rule on invalid url', () => { expect(getMatchingHostRule('')).toBeNull(); }); it.each` credential | result ${'$USERNAME'} | ${'USERNAME'} ${'$'} | ${null} ${''} | ${null} ${'${USERNAME}'} | ${'USERNAME'} ${'${USERNAME:-default}'} | ${'USERNAME'} ${'${COMPLEX_NAME_1:-default}'} | ${'COMPLEX_NAME_1'} `('extractEnvironmentVariableName($credential)', ({ credential, result }) => { expect(extractEnvironmentVariableName(credential)).toEqual(result); }); it('warns about duplicate placeholders with different values', () => { const extraEnv: Opt = { FOO: '1', }; addExtraEnvVariable(extraEnv, 'FOO', '2'); expect(logger.warn).toHaveBeenCalledWith( { envVar: 'FOO', }, 'Possible misconfiguration, environment variable already set to a different value', ); }); it('updates extraEnv if variable names differ from default', async () => { fsExtra.ensureDir.mockResolvedValue(undefined); statMock.mockResolvedValueOnce(partial()); mockFiles({ '/Pipfile.lock': ['current Pipfile.lock', 'New Pipfile.lock'], }); const execSnapshots = mockExecAll(); git.getRepoStatus.mockResolvedValue( partial({ modified: ['Pipfile.lock'], }), ); hostRules.add({ matchHost: 'mypypi.example.com', username: 'usernameOne', password: 'passwordTwo', }); await expect( updateArtifacts({ packageFileName: 'Pipfile', updatedDeps: [], newPackageFileContent: Fixtures.get('Pipfile7'), config: { ...config, constraints: { python: '== 3.8.*' } }, }), ).resolves.toEqual([ { file: { contents: 'New Pipfile.lock', path: 'Pipfile.lock', type: 'addition', }, }, ]); expect(execSnapshots).toMatchObject([ { cmd: 'pipenv lock', options: { cwd: upath.join('/tmp/github/some/repo'), env: { PIPENV_CACHE_DIR: pipenvCacheDir, WORKON_HOME: virtualenvsCacheDir, USERNAME_FOO: 'usernameOne', PAZZWORD: 'passwordTwo', }, }, }, ]); }); });