feat(rust): Find and update Cargo.lock for cargo workspaces (#8338)

This commit is contained in:
Amos Wenger 2021-01-19 09:35:48 +01:00 committed by GitHub
parent 207b177d60
commit 52c70f0b2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 5 deletions

View file

@ -114,3 +114,26 @@ Array [
}, },
] ]
`; `;
exports[`.updateArtifacts() returns updated workspace Cargo.lock 1`] = `
Array [
Object {
"cmd": "cargo update --manifest-path crates/one/Cargo.toml --package dep1",
"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",
},
"maxBuffer": 10485760,
"timeout": 900000,
},
},
]
`;

View file

@ -35,6 +35,7 @@ describe('.updateArtifacts()', () => {
docker.resetPrefetchedImages(); docker.resetPrefetchedImages();
}); });
it('returns null if no Cargo.lock found', async () => { it('returns null if no Cargo.lock found', async () => {
fs.stat.mockRejectedValue(new Error('not found!'));
const updatedDeps = ['dep1']; const updatedDeps = ['dep1'];
expect( expect(
await cargo.updateArtifacts({ await cargo.updateArtifacts({
@ -56,9 +57,11 @@ describe('.updateArtifacts()', () => {
).toBeNull(); ).toBeNull();
}); });
it('returns null if unchanged', async () => { it('returns null if unchanged', async () => {
fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any); fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any);
const execSnapshots = mockExecAll(exec); const execSnapshots = mockExecAll(exec);
fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any); fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any);
const updatedDeps = ['dep1']; const updatedDeps = ['dep1'];
expect( expect(
await cargo.updateArtifacts({ await cargo.updateArtifacts({
@ -71,6 +74,7 @@ describe('.updateArtifacts()', () => {
expect(execSnapshots).toMatchSnapshot(); expect(execSnapshots).toMatchSnapshot();
}); });
it('returns updated Cargo.lock', async () => { it('returns updated Cargo.lock', async () => {
fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
git.getFile.mockResolvedValueOnce('Old Cargo.lock'); git.getFile.mockResolvedValueOnce('Old Cargo.lock');
const execSnapshots = mockExecAll(exec); const execSnapshots = mockExecAll(exec);
fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any); fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any);
@ -86,7 +90,28 @@ describe('.updateArtifacts()', () => {
expect(execSnapshots).toMatchSnapshot(); expect(execSnapshots).toMatchSnapshot();
}); });
it('returns updated workspace Cargo.lock', async () => {
fs.stat.mockRejectedValueOnce(new Error('crates/one/Cargo.lock not found'));
fs.stat.mockRejectedValueOnce(new Error('crates/Cargo.lock not found'));
fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
git.getFile.mockResolvedValueOnce('Old Cargo.lock');
const execSnapshots = mockExecAll(exec);
fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any);
const updatedDeps = ['dep1'];
expect(
await cargo.updateArtifacts({
packageFileName: 'crates/one/Cargo.toml',
updatedDeps,
newPackageFileContent: '{}',
config,
})
).not.toBeNull();
expect(execSnapshots).toMatchSnapshot();
});
it('returns updated Cargo.lock for lockfile maintenance', async () => { it('returns updated Cargo.lock for lockfile maintenance', async () => {
fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
git.getFile.mockResolvedValueOnce('Old Cargo.lock'); git.getFile.mockResolvedValueOnce('Old Cargo.lock');
const execSnapshots = mockExecAll(exec); const execSnapshots = mockExecAll(exec);
fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any); fs.readFile.mockResolvedValueOnce('New Cargo.lock' as any);
@ -102,6 +127,7 @@ describe('.updateArtifacts()', () => {
}); });
it('returns updated Cargo.lock with docker', async () => { it('returns updated Cargo.lock with docker', async () => {
fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce(); jest.spyOn(docker, 'removeDanglingContainers').mockResolvedValueOnce();
await setExecConfig({ ...config, binarySource: BinarySource.Docker }); await setExecConfig({ ...config, binarySource: BinarySource.Docker });
git.getFile.mockResolvedValueOnce('Old Cargo.lock'); git.getFile.mockResolvedValueOnce('Old Cargo.lock');
@ -119,6 +145,7 @@ describe('.updateArtifacts()', () => {
expect(execSnapshots).toMatchSnapshot(); expect(execSnapshots).toMatchSnapshot();
}); });
it('catches errors', async () => { it('catches errors', async () => {
fs.stat.mockResolvedValueOnce({ name: 'Cargo.lock' } as any);
fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any); fs.readFile.mockResolvedValueOnce('Current Cargo.lock' as any);
fs.outputFile.mockImplementationOnce(() => { fs.outputFile.mockImplementationOnce(() => {
throw new Error('not found'); throw new Error('not found');

View file

@ -2,7 +2,7 @@ import { quote } from 'shlex';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { ExecOptions, exec } from '../../util/exec'; import { ExecOptions, exec } from '../../util/exec';
import { import {
getSiblingFileName, findLocalSiblingOrParent,
readLocalFile, readLocalFile,
writeLocalFile, writeLocalFile,
} from '../../util/fs'; } from '../../util/fs';
@ -64,8 +64,16 @@ export async function updateArtifacts({
return null; return null;
} }
const lockFileName = getSiblingFileName(packageFileName, 'Cargo.lock'); // For standalone package crates, the `Cargo.lock` will be in the same
const existingLockFileContent = await readLocalFile(lockFileName); // directory as `Cargo.toml` (ie. a sibling). For cargo workspaces, it
// will be further up.
const lockFileName = await findLocalSiblingOrParent(
packageFileName,
'Cargo.lock'
);
const existingLockFileContent = lockFileName
? await readLocalFile(lockFileName)
: null;
if (!existingLockFileContent) { if (!existingLockFileContent) {
logger.debug('No Cargo.lock found'); logger.debug('No Cargo.lock found');
return null; return null;

View file

@ -1,5 +1,13 @@
import { withDir } from 'tmp-promise';
import { getName } from '../../../test/util'; import { getName } from '../../../test/util';
import { getSubDirectory, localPathExists, readLocalFile } from '.'; import {
findLocalSiblingOrParent,
getSubDirectory,
localPathExists,
readLocalFile,
setFsConfig,
writeLocalFile,
} from '.';
describe(getName(__filename), () => { describe(getName(__filename), () => {
describe('readLocalFile', () => { describe('readLocalFile', () => {
@ -32,3 +40,59 @@ describe(getName(__filename), () => {
}); });
}); });
}); });
describe(getName(__filename), () => {
describe('findLocalSiblingOrParent', () => {
it('returns path for file', async () => {
await withDir(
async (localDir) => {
setFsConfig({
localDir: localDir.path,
});
await writeLocalFile('crates/one/Cargo.toml', '');
await writeLocalFile('Cargo.lock', '');
expect(
await findLocalSiblingOrParent(
'crates/one/Cargo.toml',
'Cargo.lock'
)
).toBe('Cargo.lock');
expect(
await findLocalSiblingOrParent(
'crates/one/Cargo.toml',
'Cargo.mock'
)
).toBeNull();
await writeLocalFile('crates/one/Cargo.lock', '');
expect(
await findLocalSiblingOrParent(
'crates/one/Cargo.toml',
'Cargo.lock'
)
).toBe('crates/one/Cargo.lock');
expect(
await findLocalSiblingOrParent('crates/one', 'Cargo.lock')
).toBe('Cargo.lock');
expect(
await findLocalSiblingOrParent(
'crates/one/Cargo.toml',
'Cargo.mock'
)
).toBeNull();
},
{
unsafeCleanup: true,
}
);
});
it('immediately returns null when either path is absolute', async () => {
expect(await findLocalSiblingOrParent('/etc/hosts', 'other')).toBeNull();
expect(await findLocalSiblingOrParent('other', '/etc/hosts')).toBeNull();
});
});
});

View file

@ -1,5 +1,5 @@
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import { join, parse } from 'upath'; import { isAbsolute, join, parse } from 'upath';
import { RenovateConfig } from '../../config/common'; import { RenovateConfig } from '../../config/common';
import { logger } from '../../logger'; import { logger } from '../../logger';
@ -96,3 +96,32 @@ export function localPathExists(pathName: string): Promise<boolean> {
.then((s) => !!s) .then((s) => !!s)
.catch(() => false); .catch(() => false);
} }
/**
* Tries to find `otherFileName` in the directory where
* `existingFileNameWithPath` is, then in its parent directory, then in the
* grandparent, until we reach the top-level directory. All paths
* must be relative to `localDir`.
*/
export async function findLocalSiblingOrParent(
existingFileNameWithPath: string,
otherFileName: string
): Promise<string | null> {
if (isAbsolute(existingFileNameWithPath)) {
return null;
}
if (isAbsolute(otherFileName)) {
return null;
}
let current = existingFileNameWithPath;
while (current !== '') {
current = getSubDirectory(current);
const candidate = join(current, otherFileName);
if (await localPathExists(candidate)) {
return candidate;
}
}
return null;
}