diff --git a/data/extract.py b/data/extract.py index e49aa52fd8cdceef0ad871e5d1e1c276261f4929..a40ee5c125be6b4e0d6107ab24cb0831d1c5cf06 100644 --- a/data/extract.py +++ b/data/extract.py @@ -25,6 +25,7 @@ def invoke(mock1, mock2): # called arguments are in `mock_setup.call_args` call_args = mock1.call_args or mock2.call_args args, kwargs = call_args - print(json.dumps(kwargs, indent=2)) + with open('renovate-pip_setup-report.json', 'w', encoding='utf-8') as f: + json.dump(kwargs, f, ensure_ascii=False, indent=2) invoke() diff --git a/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap b/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap index b4d6963c1797aef45a6bfab6efc02e748638d356..a61a5f4029a6d328774941f8fa81b1ba63ea0bd1 100644 --- a/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap +++ b/lib/manager/pip_setup/__snapshots__/extract.spec.ts.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`lib/manager/pip_setup/extract getPythonAlias returns the python alias to use 1`] = `"python3.8"`; +exports[`manager/pip_setup/extract getPythonAlias returns the python alias to use 1`] = `"python3.8"`; -exports[`lib/manager/pip_setup/extract getPythonAlias returns the python alias to use 2`] = ` +exports[`manager/pip_setup/extract getPythonAlias returns the python alias to use 2`] = ` Array [ Object { "cmd": "python --version", diff --git a/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap b/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap index 77b2ecd780d81533d9425c0ae686e24ac4021c68..3e073102d1982534761849b5b8f4bece3a6b572e 100644 --- a/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap +++ b/lib/manager/pip_setup/__snapshots__/index.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`lib/manager/pip_setup/index extractPackageFile() catches error 1`] = ` +exports[`manager/pip_setup/index extractPackageFile() catches error 1`] = ` Array [ Object { "cmd": "python --version", @@ -73,7 +73,7 @@ Array [ ] `; -exports[`lib/manager/pip_setup/index extractPackageFile() returns found deps (docker) 1`] = ` +exports[`manager/pip_setup/index extractPackageFile() returns found deps (docker) 1`] = ` Object { "deps": Array [ Object { @@ -201,7 +201,7 @@ Object { } `; -exports[`lib/manager/pip_setup/index extractPackageFile() returns found deps 1`] = ` +exports[`manager/pip_setup/index extractPackageFile() returns found deps 1`] = ` Object { "deps": Array [ Object { @@ -329,7 +329,7 @@ Object { } `; -exports[`lib/manager/pip_setup/index extractPackageFile() returns found deps 2`] = ` +exports[`manager/pip_setup/index extractPackageFile() returns found deps 2`] = ` Array [ Object { "cmd": "python --version", @@ -402,7 +402,80 @@ Array [ ] `; -exports[`lib/manager/pip_setup/index extractPackageFile() should return null for invalid file 1`] = ` +exports[`manager/pip_setup/index extractPackageFile() returns no deps 1`] = ` +Array [ + Object { + "cmd": "python --version", + "options": Object { + "cwd": null, + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "timeout": 900000, + }, + }, + Object { + "cmd": "python3 --version", + "options": Object { + "cwd": null, + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "timeout": 900000, + }, + }, + Object { + "cmd": "python3.8 --version", + "options": Object { + "cwd": null, + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "timeout": 900000, + }, + }, + Object { + "cmd": "<extract.py> \\"lib/manager/pip_setup/__fixtures__/setup.py\\"", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + "timeout": 5000, + }, + }, +] +`; + +exports[`manager/pip_setup/index extractPackageFile() should return null for invalid file 1`] = ` Array [ Object { "cmd": "python --version", diff --git a/lib/manager/pip_setup/extract.spec.ts b/lib/manager/pip_setup/extract.spec.ts index 97ababb696ecb27c530756dfb25801938a7aeaf4..3f8e80f2236db4a64b1397d56b6eb529bfb2e8c6 100644 --- a/lib/manager/pip_setup/extract.spec.ts +++ b/lib/manager/pip_setup/extract.spec.ts @@ -1,8 +1,5 @@ -import { exec as _exec } from 'child_process'; - -import { envMock, mockExecSequence } from '../../../test/execUtil'; -import { mocked } from '../../../test/util'; -import * as _env from '../../util/exec/env'; +import { envMock, exec, mockExecSequence } from '../../../test/execUtil'; +import { env, getName } from '../../../test/util'; import { getPythonAlias, parsePythonVersion, @@ -10,13 +7,10 @@ import { resetModule, } from './extract'; -const exec: jest.Mock<typeof _exec> = _exec as any; -const env = mocked(_env); - jest.mock('child_process'); jest.mock('../../util/exec/env'); -describe('lib/manager/pip_setup/extract', () => { +describe(getName(__filename), () => { beforeEach(() => { jest.resetAllMocks(); jest.resetModules(); @@ -39,7 +33,9 @@ describe('lib/manager/pip_setup/extract', () => { const result = await getPythonAlias(); expect(pythonVersions).toContain(result); expect(result).toMatchSnapshot(); + expect(await getPythonAlias()).toEqual(result); expect(execSnapshots).toMatchSnapshot(); + expect(execSnapshots).toHaveLength(3); }); }); }); diff --git a/lib/manager/pip_setup/extract.ts b/lib/manager/pip_setup/extract.ts index 13baa017e9591a160d441dc40d43b686ec11d02d..f38d8efd5e070137df6fb7a8924f2c87120de33e 100644 --- a/lib/manager/pip_setup/extract.ts +++ b/lib/manager/pip_setup/extract.ts @@ -1,17 +1,16 @@ import * as datasourcePypi from '../../datasource/pypi'; import { logger } from '../../logger'; import { SkipReason } from '../../types'; -import { resolveFile } from '../../util'; import { exec } from '../../util/exec'; import { BinarySource } from '../../util/exec/common'; import { isSkipComment } from '../../util/ignore'; import { ExtractConfig, PackageDependency, PackageFile } from '../common'; import { dependencyPattern } from '../pip_requirements/extract'; +import { PythonSetup, copyExtractFile, parseReport } from './util'; export const pythonVersions = ['python', 'python3', 'python3.8']; let pythonAlias: string | null = null; -// istanbul ignore next export function resetModule(): void { pythonAlias = null; } @@ -22,7 +21,6 @@ export function parsePythonVersion(str: string): number[] { } export async function getPythonAlias(): Promise<string> { - // istanbul ignore if if (pythonAlias) { return pythonAlias; } @@ -34,18 +32,12 @@ export async function getPythonAlias(): Promise<string> { if (version[0] >= 3 && version[1] >= 7) { pythonAlias = pythonVersion; } - } catch (err) /* istanbul ignore next */ { + } catch (err) { logger.debug(`${pythonVersion} alias not found`); } } return pythonAlias; } -interface PythonSetup { - extras_require: string[]; - install_requires: string[]; -} - -let extractPy; export async function extractSetupFile( _content: string, @@ -53,30 +45,10 @@ export async function extractSetupFile( config: ExtractConfig ): Promise<PythonSetup> { const cwd = config.localDir; - let cmd: string; - extractPy = extractPy || (await resolveFile('data/extract.py')); + let cmd = 'python'; + const extractPy = await copyExtractFile(); const args = [`"${extractPy}"`, `"${packageFile}"`]; - if (config.binarySource === BinarySource.Docker) { - logger.debug('Running python via docker'); - await exec(`docker pull renovate/pip`); - cmd = 'docker'; - args.unshift( - 'run', - '-i', - '--rm', - // volume - '-v', - `${cwd}:${cwd}`, - '-v', - `${extractPy}:${extractPy}`, - // cwd - '-w', - cwd, - // image - 'renovate/pip', - 'python' - ); - } else { + if (config.binarySource !== BinarySource.Docker) { logger.debug('Running python via global command'); cmd = await getPythonAlias(); } @@ -84,8 +56,10 @@ export async function extractSetupFile( const res = await exec(`${cmd} ${args.join(' ')}`, { cwd, timeout: 5000, + docker: { + image: 'renovate/pip', + }, }); - // istanbul ignore if if (res.stderr) { const stderr = res.stderr .replace(/.*\n\s*import imp/, '') @@ -95,7 +69,7 @@ export async function extractSetupFile( logger.warn({ stdout: res.stdout, stderr }, 'Error in read setup file'); } } - return JSON.parse(res.stdout); + return parseReport(); } export async function extractPackageFile( @@ -155,7 +129,6 @@ export async function extractPackageFile( ? a.depName.localeCompare(b.depName) : a.managerData.lineNumber - b.managerData.lineNumber ); - // istanbul ignore if if (!deps.length) { return null; } diff --git a/lib/manager/pip_setup/index.spec.ts b/lib/manager/pip_setup/index.spec.ts index f3fdc84bc36786f3e15c607e3bab556cdc02c17c..c17f90d2f56a3de0384edaf79a8bbbc6996ea096 100644 --- a/lib/manager/pip_setup/index.spec.ts +++ b/lib/manager/pip_setup/index.spec.ts @@ -1,14 +1,14 @@ -import { exec as _exec } from 'child_process'; import { readFileSync } from 'fs'; import { ExecSnapshots, envMock, + exec, mockExecAll, mockExecSequence, } from '../../../test/execUtil'; -import { mocked } from '../../../test/util'; +import { env, getName } from '../../../test/util'; import { BinarySource } from '../../util/exec/common'; -import * as _env from '../../util/exec/env'; +import * as fs from '../../util/fs'; import * as extract from './extract'; import { extractPackageFile } from '.'; @@ -22,9 +22,6 @@ const config = { localDir: '/tmp/github/some/repo', }; -const exec: jest.Mock<typeof _exec> = _exec as any; -const env = mocked(_env); - jest.mock('child_process'); jest.mock('../../util/exec/env'); @@ -38,10 +35,10 @@ const pythonVersionCallResults = [ const fixSnapshots = (snapshots: ExecSnapshots): ExecSnapshots => snapshots.map((snapshot) => ({ ...snapshot, - cmd: snapshot.cmd.replace(/^.*\/extract\.py"\s+/, '<extract.py> '), + cmd: snapshot.cmd.replace(/^.*extract\.py"\s+/, '<extract.py> '), })); -describe('lib/manager/pip_setup/index', () => { +describe(getName(__filename), () => { describe('extractPackageFile()', () => { beforeEach(() => { jest.resetAllMocks(); @@ -49,32 +46,57 @@ describe('lib/manager/pip_setup/index', () => { extract.resetModule(); env.getChildProcessEnv.mockReturnValue(envMock.basic); + + // do not copy extract.py + jest.spyOn(fs, 'writeLocalFile').mockResolvedValue(); }); + it('returns found deps', async () => { const execSnapshots = mockExecSequence(exec, [ ...pythonVersionCallResults, - { stdout: jsonContent, stderr: '' }, + { + stdout: '', + stderr: + 'DeprecationWarning: the imp module is deprecated in favour of importlib', + }, ]); + jest.spyOn(fs, 'readLocalFile').mockResolvedValueOnce(jsonContent); expect( await extractPackageFile(content, packageFile, config) ).toMatchSnapshot(); expect(exec).toHaveBeenCalledTimes(4); expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); }); + it('returns found deps (docker)', async () => { const execSnapshots = mockExecSequence(exec, [ - { stdout: '', stderr: '' }, // docker pull - { stdout: jsonContent, stderr: '' }, + { stdout: '', stderr: '' }, ]); + jest.spyOn(fs, 'readLocalFile').mockResolvedValueOnce(jsonContent); expect( await extractPackageFile(content, packageFile, { ...config, binarySource: BinarySource.Docker, }) ).toMatchSnapshot(); - expect(execSnapshots).toHaveLength(2); // TODO: figure out volume arguments in Windows + expect(execSnapshots).toHaveLength(1); // TODO: figure out volume arguments in Windows + }); + + it('returns no deps', async () => { + const execSnapshots = mockExecSequence(exec, [ + ...pythonVersionCallResults, + { + stdout: '', + stderr: 'fatal: No names found, cannot describe anything.', + }, + ]); + jest.spyOn(fs, 'readLocalFile').mockResolvedValueOnce('{}'); + expect(await extractPackageFile(content, packageFile, config)).toBeNull(); + expect(exec).toHaveBeenCalledTimes(4); + expect(fixSnapshots(execSnapshots)).toMatchSnapshot(); }); + it('should return null for invalid file', async () => { const execSnapshots = mockExecSequence(exec, [ ...pythonVersionCallResults, diff --git a/lib/manager/pip_setup/util.ts b/lib/manager/pip_setup/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..7169f41cd950b4591f36a7ed1ffede8da290da4c --- /dev/null +++ b/lib/manager/pip_setup/util.ts @@ -0,0 +1,29 @@ +import { resolveFile } from '../../util'; +import { readFile, readLocalFile, writeLocalFile } from '../../util/fs'; + +// need to match filename in `data/extract.py` +const REPORT = 'renovate-pip_setup-report.json'; +const EXTRACT = 'renovate-pip_setup-extract.py'; + +let extractPy: string | undefined; + +export async function copyExtractFile(): Promise<string> { + if (extractPy === undefined) { + const file = await resolveFile('data/extract.py'); + extractPy = await readFile(file, 'utf8'); + } + + await writeLocalFile(EXTRACT, extractPy); + + return EXTRACT; +} + +export interface PythonSetup { + extras_require: Record<string, string[]>; + install_requires: string[]; +} + +export async function parseReport(): Promise<PythonSetup> { + const data = await readLocalFile(REPORT, 'utf8'); + return JSON.parse(data); +} diff --git a/test/execUtil.ts b/test/execUtil.ts index 479f33559765b33855e645886f2ceb9438201a24..6e12081cc5811cb5b9fdde88774b01f05bb01506 100644 --- a/test/execUtil.ts +++ b/test/execUtil.ts @@ -8,6 +8,9 @@ type CallOptions = ExecOptions | null | undefined; export type ExecResult = { stdout: string; stderr: string } | Error; +export type ExecMock = jest.Mock<typeof _exec>; +export const exec: ExecMock = _exec as any; + interface ExecSnapshot { cmd: string; options?: ExecOptions | null | undefined; @@ -35,7 +38,7 @@ export function execSnapshot(cmd: string, options?: CallOptions): ExecSnapshot { const defaultExecResult = { stdout: '', stderr: '' }; export function mockExecAll( - execFn: jest.Mock<typeof _exec>, + execFn: ExecMock, execResult: ExecResult = defaultExecResult ): ExecSnapshots { const snapshots: ExecSnapshots = []; @@ -51,7 +54,7 @@ export function mockExecAll( } export function mockExecSequence( - execFn: jest.Mock<typeof _exec>, + execFn: ExecMock, execResults: ExecResult[] ): ExecSnapshots { const snapshots: ExecSnapshots = [];