From b286418cc7ce4c655925bf63010f5f30c5dd481c Mon Sep 17 00:00:00 2001 From: ghe Date: Fri, 26 Mar 2021 17:25:14 +0000 Subject: [PATCH] feat: v1 support for previously fixed reqs.txt --- .../python/handlers/pip-requirements/index.ts | 122 +++++-- .../update-dependencies.spec.ts.snap | 37 ++ .../update-dependencies.spec.ts | 328 ++++++++---------- .../app-with-already-fixed/base.txt | 1 + .../core/requirements.txt | 1 + .../lib/requirements.txt | 1 + .../app-with-already-fixed/requirements.txt | 4 + 7 files changed, 283 insertions(+), 211 deletions(-) create mode 100644 packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/base.txt create mode 100644 packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/core/requirements.txt create mode 100644 packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/lib/requirements.txt create mode 100644 packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/requirements.txt diff --git a/packages/snyk-fix/src/plugins/python/handlers/pip-requirements/index.ts b/packages/snyk-fix/src/plugins/python/handlers/pip-requirements/index.ts index d57d45acdd..4467986837 100644 --- a/packages/snyk-fix/src/plugins/python/handlers/pip-requirements/index.ts +++ b/packages/snyk-fix/src/plugins/python/handlers/pip-requirements/index.ts @@ -1,8 +1,9 @@ import * as debugLib from 'debug'; import * as pathLib from 'path'; +const sortBy = require('lodash.sortby'); +const groupBy = require('lodash.groupby'); import { - DependencyPins, EntityToFix, FixChangesSummary, FixOptions, @@ -15,14 +16,10 @@ import { MissingRemediationDataError } from '../../../../lib/errors/missing-reme import { MissingFileNameError } from '../../../../lib/errors/missing-file-name'; import { partitionByFixable } from './is-supported'; import { NoFixesCouldBeAppliedError } from '../../../../lib/errors/no-fixes-applied'; -import { - extractProvenance, - PythonProvenance, -} from './extract-version-provenance'; +import { extractProvenance } from './extract-version-provenance'; import { ParsedRequirements, parseRequirementsFile, - Requirement, } from './update-dependencies/requirements-file-parser'; const debug = debugLib('snyk-fix:python:requirements.txt'); @@ -38,27 +35,23 @@ export async function pipRequirementsTxt( skipped: [], }; - const { fixable, skipped } = await partitionByFixable(entities); - handlerResult.skipped.push(...skipped); + const { fixable, skipped: notFixable } = await partitionByFixable(entities); + handlerResult.skipped.push(...notFixable); - for (const entity of fixable) { - try { - const { changes } = await applyAllFixes( - entity, - // dir, - // base, - // remediation, - // provenance, - options, - ); - if (!changes.length) { - debug('Manifest has not changed!'); - throw new NoFixesCouldBeAppliedError(); - } - handlerResult.succeeded.push({ original: entity, changes }); - } catch (e) { - handlerResult.failed.push({ original: entity, error: e }); - } + const ordered = sortByDirectory(fixable); + const fixedFilesCache: string[] = []; + for (const dir of Object.keys(ordered)) { + debug(`Fixing entities in directory ${dir}`); + const entitiesPerDirectory = ordered[dir].map((e) => e.entity); + const { failed, succeeded, skipped, fixedFiles } = await fixAll( + entitiesPerDirectory, + options, + fixedFilesCache, + ); + fixedFilesCache.push(...fixedFiles); + handlerResult.succeeded.push(...succeeded); + handlerResult.failed.push(...failed); + handlerResult.skipped.push(...skipped); } return handlerResult; } @@ -85,6 +78,42 @@ export function getRequiredData( return { targetFile, remediation, workspace }; } +async function fixAll( + entities: EntityToFix[], + options: FixOptions, + fixedCache: string[], +): Promise { + const handlerResult: PluginFixResponse = { + succeeded: [], + failed: [], + skipped: [], + }; + for (const entity of entities) { + const targetFile = entity.scanResult.identity.targetFile!; + try { + const { dir, base } = pathLib.parse(targetFile); + // parse & join again to support correct separator + if (fixedCache.includes(pathLib.join(dir, base))) { + handlerResult.succeeded.push({ + original: entity, + changes: [{ success: true, userMessage: 'Previously fixed' }], + }); + continue; + } + const { changes, fixedFiles } = await applyAllFixes(entity, options); + if (!changes.length) { + debug('Manifest has not changed!'); + throw new NoFixesCouldBeAppliedError(); + } + fixedCache.push(...fixedFiles); + handlerResult.succeeded.push({ original: entity, changes }); + } catch (e) { + debug(`Failed to fix ${targetFile}.\nERROR: ${e}`); + handlerResult.failed.push({ original: entity, error: e }); + } + } + return { ...handlerResult, fixedFiles: [] }; +} // TODO: optionally verify the deps install export async function fixIndividualRequirementsTxt( workspace: Workspace, @@ -103,7 +132,12 @@ export async function fixIndividualRequirementsTxt( directUpgradesOnly, pathLib.join(dir, entryFileName) !== fullFilePath ? fileName : undefined, ); - if (!options.dryRun && changes.length > 0) { + + if (!changes.length) { + return { changes, appliedRemediation }; + } + + if (!options.dryRun) { debug('Writing changes to file'); await workspace.writeFile(pathLib.join(dir, fileName), updatedManifest); } else { @@ -116,14 +150,16 @@ export async function fixIndividualRequirementsTxt( export async function applyAllFixes( entity: EntityToFix, options: FixOptions, -): Promise<{ changes: FixChangesSummary[] }> { +): Promise<{ changes: FixChangesSummary[]; fixedFiles: string[] }> { const { remediation, targetFile: entryFileName, workspace } = getRequiredData( entity, ); + const fixedFiles: string[] = []; const { dir, base } = pathLib.parse(entryFileName); const provenance = await extractProvenance(workspace, dir, base); const upgradeChanges: FixChangesSummary[] = []; const appliedUpgradeRemediation: string[] = []; + /* Apply all upgrades first across all files that are included */ for (const fileName of Object.keys(provenance)) { const skipApplyingPins = true; const { changes, appliedRemediation } = await fixIndividualRequirementsTxt( @@ -137,10 +173,11 @@ export async function applyAllFixes( skipApplyingPins, ); appliedUpgradeRemediation.push(...appliedRemediation); - // what if we saw the file before and already fixed it? upgradeChanges.push(...changes); + fixedFiles.push(pathLib.join(dir, fileName)); } - // now do left overs as pins + add tests + + /* Apply all left over remediation as pins in the entry targetFile */ const requirementsTxt = await workspace.readFile(entryFileName); const toPin: RemediationChanges = filterOutAppliedUpgrades( @@ -159,7 +196,7 @@ export async function applyAllFixes( directUpgradesOnly, ); - return { changes: [...upgradeChanges, ...pinnedChanges] }; + return { changes: [...upgradeChanges, ...pinnedChanges], fixedFiles }; } function filterOutAppliedUpgrades( @@ -168,7 +205,7 @@ function filterOutAppliedUpgrades( ): RemediationChanges { const pinRemediation: RemediationChanges = { ...remediation, - pin: {}, // delete the pin remediation so we can add only not applied + pin: {}, // delete the pin remediation so we can collect un-applied remediation }; const pins = remediation.pin; const lowerCasedAppliedRemediation = appliedRemediation.map((i) => @@ -181,3 +218,24 @@ function filterOutAppliedUpgrades( } return pinRemediation; } + +function sortByDirectory( + entities: EntityToFix[], +): { + [dir: string]: Array<{ + entity: EntityToFix; + dir: string; + base: string; + ext: string; + root: string; + name: string; + }>; +} { + const mapped = entities.map((e) => ({ + entity: e, + ...pathLib.parse(e.scanResult.identity.targetFile!), + })); + + const sorted = sortBy(mapped, 'dir'); + return groupBy(sorted, 'dir'); +} diff --git a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/__snapshots__/update-dependencies.spec.ts.snap b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/__snapshots__/update-dependencies.spec.ts.snap index 980322383a..d554e57d51 100644 --- a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/__snapshots__/update-dependencies.spec.ts.snap +++ b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/__snapshots__/update-dependencies.spec.ts.snap @@ -13,6 +13,43 @@ Array [ ] `; +exports[`fix *req*.txt / *.txt Python projects fixes multiple files via -r with the same name (some were already fixed) 1`] = ` +"Successful fixes: + + app-with-already-fixed/requirements.txt + ✔ Upgraded Django from 1.6.1 to 2.0.1 + ✔ Upgraded Django from 1.6.1 to 2.0.1 (upgraded in core/requirements.txt) + ✔ Upgraded Jinja2 from 2.7.2 to 2.7.3 (upgraded in lib/requirements.txt) + + app-with-already-fixed/core/requirements.txt + ✔ Previously fixed + + app-with-already-fixed/lib/requirements.txt + ✔ Previously fixed + +Summary: + + 0 items were not fixed + 3 items were successfully fixed" +`; + +exports[`fix *req*.txt / *.txt Python projects fixes multiple files via -r with the same name (some were already fixed) 2`] = ` +Array [ + Object { + "success": true, + "userMessage": "Upgraded Django from 1.6.1 to 2.0.1", + }, + Object { + "success": true, + "userMessage": "Upgraded Django from 1.6.1 to 2.0.1 (upgraded in core/requirements.txt)", + }, + Object { + "success": true, + "userMessage": "Upgraded Jinja2 from 2.7.2 to 2.7.3 (upgraded in lib/requirements.txt)", + }, +] +`; + exports[`fix *req*.txt / *.txt Python projects retains python markers 1`] = ` "amqp==2.4.2 apscheduler==3.6.0 diff --git a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/update-dependencies.spec.ts b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/update-dependencies.spec.ts index bb3d000608..11fec6844e 100644 --- a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/update-dependencies.spec.ts +++ b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/update-dependencies.spec.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as pathLib from 'path'; import * as snykFix from '../../../../../src'; +import { TestResult } from '../../../../../src/types'; import { generateScanResult, generateTestResult, @@ -16,6 +17,9 @@ describe('fix *req*.txt / *.txt Python projects', () => { it('fixes project with a -r option', async () => { // Arrange const targetFile = 'with-require/dev.txt'; + filesToDelete = [ + pathLib.join(workspacesPath, 'with-require/fixed-dev.txt'), + ]; const testResult = { ...generateTestResult(), @@ -39,25 +43,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -97,6 +87,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { workspacesPath, 'basic/fixed-prod.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), @@ -120,25 +111,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -185,6 +162,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { workspacesPath, 'basic-with-newline/fixed-prod.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), @@ -208,25 +186,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -273,6 +237,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { workspacesPath, 'with-custom-formatting/fixed-requirements.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), @@ -296,25 +261,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -362,6 +313,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { workspacesPath, 'lower-case-dep/fixed-req.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), @@ -380,25 +332,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -441,6 +379,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { workspacesPath, 'basic/fixed-prod.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), @@ -460,25 +399,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -521,6 +446,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { workspacesPath, 'long-versions/fixed-prod.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), @@ -539,25 +465,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -600,6 +512,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { workspacesPath, 'with-comparator/fixed-prod.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), @@ -623,25 +536,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -688,6 +587,7 @@ describe('fix *req*.txt / *.txt Python projects', () => { 'python-markers/fixed-prod.txt', ); + filesToDelete = [fixedFilePath]; const testResult = { ...generateTestResult(), remediation: { @@ -705,25 +605,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - filesToDelete = [fixedPath]; - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); // Act const result = await snykFix.fix([entityToFix], { @@ -783,24 +669,11 @@ describe('fix *req*.txt / *.txt Python projects', () => { }, }, }; - const entityToFix = { - workspace: { - readFile: async (path: string) => { - return readFileHelper(workspacesPath, path); - }, - writeFile: async (path: string, contents: string) => { - const res = pathLib.parse(path); - const fixedPath = pathLib.resolve( - workspacesPath, - res.dir, - `fixed-${res.base}`, - ); - fs.writeFileSync(fixedPath, contents, 'utf-8'); - }, - }, - scanResult: generateScanResult('pip', targetFile), + const entityToFix = generateEntityToFix( + workspacesPath, + targetFile, testResult, - }; + ); const writeFileSpy = jest.spyOn(entityToFix.workspace, 'writeFile'); // Act @@ -814,6 +687,77 @@ describe('fix *req*.txt / *.txt Python projects', () => { expect(result.results.python.succeeded[0].original).toEqual(entityToFix); expect(result.results.python.succeeded[0].changes).toMatchSnapshot(); }); + it('fixes multiple files via -r with the same name (some were already fixed)', async () => { + // Arrange + const targetFile1 = 'app-with-already-fixed/requirements.txt'; + const targetFile2 = 'app-with-already-fixed/lib/requirements.txt'; + const targetFile3 = 'app-with-already-fixed/core/requirements.txt'; + + filesToDelete = [ + pathLib.resolve( + workspacesPath, + 'app-with-already-fixed/fixed-requirements.txt', + ), + pathLib.resolve( + workspacesPath, + 'app-with-already-fixed/lib/fixed-requirements.txt', + ), + pathLib.resolve( + workspacesPath, + 'app-with-already-fixed/core/fixed-requirements.txt', + ), + ]; + const testResult = { + ...generateTestResult(), + remediation: { + unresolved: [], + upgrade: {}, + patch: {}, + ignore: {}, + pin: { + 'django@1.6.1': { + upgradeTo: 'django@2.0.1', + vulns: [], + isTransitive: false, + }, + 'Jinja2@2.7.2': { + upgradeTo: 'Jinja2@2.7.3', + vulns: [], + isTransitive: true, + }, + }, + }, + }; + const entityToFix1 = generateEntityToFix( + workspacesPath, + targetFile1, + testResult, + ); + const entityToFix2 = generateEntityToFix( + workspacesPath, + targetFile2, + testResult, + ); + const entityToFix3 = generateEntityToFix( + workspacesPath, + targetFile3, + testResult, + ); + const writeFileSpy = jest.spyOn(entityToFix1.workspace, 'writeFile'); + // Act + const result = await snykFix.fix( + [entityToFix2, entityToFix3, entityToFix1], + { + quiet: true, + stripAnsi: true, + }, + ); + // 3 files needed to have changes + expect(result.fixSummary).toMatchSnapshot(); + expect(writeFileSpy).toHaveBeenCalledTimes(3); + expect(result.results.python.succeeded[0].original).toEqual(entityToFix1); + expect(result.results.python.succeeded[0].changes).toMatchSnapshot(); + }); }); function readFileHelper(workspacesPath: string, path: string): string { @@ -834,3 +778,29 @@ function readFileHelper(workspacesPath: string, path: string): string { } return file; } + +function generateEntityToFix( + workspacesPath: string, + targetFile: string, + testResult: TestResult, +): snykFix.EntityToFix { + const entityToFix = { + workspace: { + readFile: async (path: string) => { + return readFileHelper(workspacesPath, path); + }, + writeFile: async (path: string, contents: string) => { + const res = pathLib.parse(path); + const fixedPath = pathLib.resolve( + workspacesPath, + res.dir, + `fixed-${res.base}`, + ); + fs.writeFileSync(fixedPath, contents, 'utf-8'); + }, + }, + scanResult: generateScanResult('pip', targetFile), + testResult, + }; + return entityToFix; +} diff --git a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/base.txt b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/base.txt new file mode 100644 index 0000000000..aba69a5a27 --- /dev/null +++ b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/base.txt @@ -0,0 +1 @@ +click>7.0 diff --git a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/core/requirements.txt b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/core/requirements.txt new file mode 100644 index 0000000000..e5568357b9 --- /dev/null +++ b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/core/requirements.txt @@ -0,0 +1 @@ +Django==1.6.1 ; python_version > '1.0' diff --git a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/lib/requirements.txt b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/lib/requirements.txt new file mode 100644 index 0000000000..c3eeef4fb7 --- /dev/null +++ b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/lib/requirements.txt @@ -0,0 +1 @@ +Jinja2==2.7.2 diff --git a/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/requirements.txt b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/requirements.txt new file mode 100644 index 0000000000..3693dd3bd5 --- /dev/null +++ b/packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/workspaces/app-with-already-fixed/requirements.txt @@ -0,0 +1,4 @@ +-r core/requirements.txt +-r lib/requirements.txt +-r base.txt +Django==1.6.1