Skip to content

Commit

Permalink
feat(manager/gleam): extract locked versions (#31000)
Browse files Browse the repository at this point in the history
Co-authored-by: Rhys Arkins <[email protected]>
Co-authored-by: Michael Kriese <[email protected]>
  • Loading branch information
3 people authored Aug 26, 2024
1 parent 6c7316c commit f619736
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 9 deletions.
164 changes: 158 additions & 6 deletions lib/modules/manager/gleam/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { codeBlock } from 'common-tags';
import { mocked } from '../../../../test/util';
import * as _fs from '../../../util/fs';
import * as gleamManager from '.';

jest.mock('../../../util/fs');

const fs = mocked(_fs);

describe('modules/manager/gleam/extract', () => {
it('should extract dev and prod dependencies', () => {
it('should extract dev and prod dependencies', async () => {
const gleamTomlString = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
Expand All @@ -13,7 +19,12 @@ describe('modules/manager/gleam/extract', () => {
[dev-dependencies]
gleeunit = "~> 1.0"
`;
const extracted = gleamManager.extractPackageFile(gleamTomlString);

fs.readLocalFile.mockResolvedValueOnce(gleamTomlString);
const extracted = await gleamManager.extractPackageFile(
gleamTomlString,
'gleam.toml',
);
expect(extracted?.deps).toEqual([
{
currentValue: '~> 0.6.0',
Expand All @@ -30,15 +41,20 @@ describe('modules/manager/gleam/extract', () => {
]);
});

it('should extract dev only dependencies', () => {
it('should extract dev only dependencies', async () => {
const gleamTomlString = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
[dev-dependencies]
gleeunit = "~> 1.0"
`;
const extracted = gleamManager.extractPackageFile(gleamTomlString);

fs.readLocalFile.mockResolvedValueOnce(gleamTomlString);
const extracted = await gleamManager.extractPackageFile(
gleamTomlString,
'gleam.toml',
);
expect(extracted?.deps).toEqual([
{
currentValue: '~> 1.0',
Expand All @@ -49,15 +65,151 @@ describe('modules/manager/gleam/extract', () => {
]);
});

it('should return null when no dependencies are found', () => {
it('should return null when no dependencies are found', async () => {
const gleamTomlString = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
[unknown]
gleam_http = "~> 3.6.0"
`;
const extracted = gleamManager.extractPackageFile(gleamTomlString);

fs.readLocalFile.mockResolvedValueOnce(gleamTomlString);
const extracted = await gleamManager.extractPackageFile(
gleamTomlString,
'gleam.toml',
);
expect(extracted).toBeNull();
});

it('should return null when gleam.toml is invalid', async () => {
fs.readLocalFile.mockResolvedValueOnce('foo');
const extracted = await gleamManager.extractPackageFile(
'foo',
'gleam.toml',
);
expect(extracted).toBeNull();
});

it('should return locked versions', async () => {
const packageFileContent = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
[dependencies]
foo = ">= 1.0.0 and < 2.0.0"
`;
const lockFileContent = codeBlock`
packages = [
{ name = "foo", version = "1.0.4", build_tools = ["gleam"], requirements = ["bar"], otp_app = "foo", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
{ name = "bar", version = "2.1.0", build_tools = ["rebar3"], requirements = [], otp_app = "bar", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
]
[requirements]
foo = { version = ">= 1.0.0 and < 2.0.0" }
`;

fs.getSiblingFileName.mockReturnValueOnce('manifest.toml');
fs.readLocalFile.mockResolvedValueOnce(lockFileContent);
fs.localPathExists.mockResolvedValueOnce(true);
const extracted = await gleamManager.extractPackageFile(
packageFileContent,
'gleam.toml',
);
expect(extracted!.deps.every((dep) => 'lockedVersion' in dep)).toBe(true);
});

it('should fail to extract locked version', async () => {
const packageFileContent = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
[dependencies]
foo = ">= 1.0.0 and < 2.0.0"
`;

fs.getSiblingFileName.mockReturnValueOnce('manifest.toml');
fs.readLocalFile.mockResolvedValueOnce(null);
fs.localPathExists.mockResolvedValueOnce(true);
const extracted = await gleamManager.extractPackageFile(
packageFileContent,
'gleam.toml',
);
expect(extracted!.deps.every((dep) => 'lockedVersion' in dep)).toBe(false);
});

it('should fail to find locked version in range', async () => {
const packageFileContent = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
[dependencies]
foo = ">= 1.0.0 and < 2.0.0"
`;
const lockFileContent = codeBlock`
packages = [
{ name = "foo", version = "2.0.1", build_tools = ["gleam"], requirements = ["bar"], otp_app = "foo", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
{ name = "bar", version = "2.1.0", build_tools = ["rebar3"], requirements = [], otp_app = "bar", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
]
[requirements]
foo = { version = ">= 1.0.0 and < 2.0.0" }
`;

fs.getSiblingFileName.mockReturnValueOnce('manifest.toml');
fs.readLocalFile.mockResolvedValueOnce(lockFileContent);
fs.localPathExists.mockResolvedValueOnce(true);
const extracted = await gleamManager.extractPackageFile(
packageFileContent,
'gleam.toml',
);
expect(extracted!.deps.every((dep) => 'lockedVersion' in dep)).toBe(false);
});

it('should handle invalid versions in lock file', async () => {
const packageFileContent = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
[dependencies]
foo = ">= 1.0.0 and < 2.0.0"
`;
const lockFileContent = codeBlock`
packages = [
{ name = "foo", version = "fooey", build_tools = ["gleam"], requirements = [], otp_app = "foo", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
]
[requirements]
foo = { version = ">= 1.0.0 and < 2.0.0" }
`;

fs.getSiblingFileName.mockReturnValueOnce('manifest.toml');
fs.readLocalFile.mockResolvedValueOnce(lockFileContent);
fs.localPathExists.mockResolvedValueOnce(true);
const extracted = await gleamManager.extractPackageFile(
packageFileContent,
'gleam.toml',
);
expect(extracted!.deps).not.toHaveProperty('lockedVersion');
});

it('should handle lock file parsing and extracting errors', async () => {
const packageFileContent = codeBlock`
name = "test_gleam_toml"
version = "1.0.0"
[dependencies]
foo = ">= 1.0.0 and < 2.0.0"
`;
const lockFileContent = codeBlock`invalid`;

fs.getSiblingFileName.mockReturnValueOnce('manifest.toml');
fs.readLocalFile.mockResolvedValueOnce(lockFileContent);
fs.localPathExists.mockResolvedValueOnce(true);
const extracted = await gleamManager.extractPackageFile(
packageFileContent,
'gleam.toml',
);
expect(extracted!.deps).not.toHaveProperty('lockedVersion');
});
});
59 changes: 56 additions & 3 deletions lib/modules/manager/gleam/extract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { getSiblingFileName, localPathExists } from '../../../util/fs';
import { HexDatasource } from '../../datasource/hex';
import { api as versioning } from '../../versioning/hex';
import type { PackageDependency, PackageFileContent } from '../types';
import { extractLockFileVersions } from './locked-version';
import { GleamToml } from './schema';

const dependencySections = ['dependencies', 'dev-dependencies'] as const;
Expand Down Expand Up @@ -53,7 +58,55 @@ function extractGleamTomlDeps(gleamToml: GleamToml): PackageDependency[] {
);
}

export function extractPackageFile(content: string): PackageFileContent | null {
const deps = extractGleamTomlDeps(GleamToml.parse(content));
return deps.length ? { deps } : null;
export async function extractPackageFile(
content: string,
packageFile: string,
): Promise<PackageFileContent | null> {
const result = GleamToml.safeParse(content);
if (!result.success) {
logger.debug(
{ err: result.error, packageFile },
'Error parsing Gleam package file content',
);
return null;
}

const deps = extractGleamTomlDeps(result.data);
if (!deps.length) {
logger.debug(`No dependencies found in Gleam package file ${packageFile}`);
return null;
}

const packageFileContent: PackageFileContent = { deps };
const lockFileName = getSiblingFileName(packageFile, 'manifest.toml');

const lockFileExists = await localPathExists(lockFileName);
if (!lockFileExists) {
logger.debug(`Lock file ${lockFileName} does not exist.`);
return packageFileContent;
}

const versionsByPackage = await extractLockFileVersions(lockFileName);
if (!versionsByPackage) {
return packageFileContent;
}

packageFileContent.lockFiles = [lockFileName];

for (const dep of packageFileContent.deps) {
const packageName = dep.depName!;
const versions = coerceArray(versionsByPackage.get(packageName));
const lockedVersion = versioning.getSatisfyingVersion(
versions,
dep.currentValue!,
);
if (lockedVersion) {
dep.lockedVersion = lockedVersion;
} else {
logger.debug(
`No locked version found for package ${dep.depName} in the range of ${dep.currentValue}.`,
);
}
}
return packageFileContent;
}
74 changes: 74 additions & 0 deletions lib/modules/manager/gleam/locked-version.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { codeBlock } from 'common-tags';
import { mocked } from '../../../../test/util';
import { logger } from '../../../logger';
import * as _fs from '../../../util/fs';
import { extractLockFileVersions, parseLockFile } from './locked-version';

jest.mock('../../../util/fs');

const fs = mocked(_fs);

const lockFileContent = codeBlock`
packages = [
{ name = "foo", version = "1.0.4", build_tools = ["gleam"], requirements = ["bar"], otp_app = "foo", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
{ name = "bar", version = "2.1.0", build_tools = ["rebar3"], requirements = [], otp_app = "bar", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
]
[requirements]
foo = { version = ">= 1.0.0 and < 2.0.0" }
`;

describe('modules/manager/gleam/locked-version', () => {
describe('extractLockFileVersions()', () => {
it('returns null for missing lock file', async () => {
expect(await extractLockFileVersions('manifest.toml')).toBeNull();
});

it('returns null for invalid lock file', async () => {
fs.readLocalFile.mockResolvedValueOnce('foo');
expect(await extractLockFileVersions('manifest.toml')).toBeNull();
});

it('returns empty map for lock file without packages', async () => {
fs.readLocalFile.mockResolvedValueOnce('[requirements]');
expect(await extractLockFileVersions('manifest.toml')).toEqual(new Map());
});

it('returns a map of package versions', async () => {
fs.readLocalFile.mockResolvedValueOnce(lockFileContent);
expect(await extractLockFileVersions('manifest.toml')).toEqual(
new Map([
['foo', ['1.0.4']],
['bar', ['2.1.0']],
]),
);
});
});

describe('parseLockFile', () => {
it('parses lockfile string into an object', () => {
const parseLockFileResult = parseLockFile(lockFileContent);
logger.debug({ parseLockFileResult }, 'parseLockFile');
expect(parseLockFileResult).toStrictEqual({
packages: [
{
name: 'foo',
version: '1.0.4',
requirements: ['bar'],
},
{
name: 'bar',
version: '2.1.0',
requirements: [],
},
],
});
});

it('can deal with invalid lockfiles', () => {
const lockFile = 'foo';
const parseLockFileResult = parseLockFile(lockFile);
expect(parseLockFileResult).toBeNull();
});
});
});
36 changes: 36 additions & 0 deletions lib/modules/manager/gleam/locked-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { readLocalFile } from '../../../util/fs';
import { ManifestToml } from './schema';

export async function extractLockFileVersions(
lockFilePath: string,
): Promise<Map<string, string[]> | null> {
const content = await readLocalFile(lockFilePath, 'utf8');
if (!content) {
logger.debug(`Gleam lock file ${lockFilePath} not found`);
return null;
}

const versionsByPackage = new Map<string, string[]>();
const lock = parseLockFile(content);
if (!lock) {
logger.debug(`Error parsing Gleam lock file ${lockFilePath}`);
return null;
}
for (const pkg of coerceArray(lock.packages)) {
const versions = coerceArray(versionsByPackage.get(pkg.name));
versions.push(pkg.version);
versionsByPackage.set(pkg.name, versions);
}
return versionsByPackage;
}

export function parseLockFile(lockFileContent: string): ManifestToml | null {
const res = ManifestToml.safeParse(lockFileContent);
if (res.success) {
return res.data;
}
logger.debug({ err: res.error }, 'Error parsing manifest.toml.');
return null;
}
Loading

0 comments on commit f619736

Please sign in to comment.