Files
renovate/lib/modules/manager/rust-toolchain/extract.spec.ts
Sebastian Poxhofer 183ee9caac fix(manager/rust-toolchain): report skipReason instead of dropping deps (#45144)
The manager emitted `logger.warn` four times during extraction and
returned `null` for content that is valid but not updatable. A
`rust-toolchain.toml` using `path` instead of `channel` is a legitimate
file, yet it failed schema validation and produced a maintainer-facing
warning.

Per the logging guidance in `docs/development/best-practices.md`, WARN
is consumed by metrics and error catching services, so it should not
fire for user content Renovate simply cannot update. The manager guide
also asks for a `skipReason` rather than silently dropping a dependency.

The schema now accepts an optional `channel` and an optional `path`, and
extraction reports:

- `invalid-version` when a channel cannot be parsed
- `path-dependency` when the file points at a local toolchain
- `unspecified-version` when neither field is set

`channel` takes precedence when both are present, since rustup rejects
that combination and `channel` is the actionable field.

Files that are not toolchain files at all still return `null`: TOML that
does not parse, TOML without a `[toolchain]` table, and legacy plain
text files that are empty or hold more than one line. The file pattern
matches any file named `rust-toolchain`, so these are treated as false
matches. All remaining logs are DEBUG.
2026-08-10 07:52:31 +00:00

353 lines
9.1 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { logger } from '~test/util.ts';
import { RustVersionDatasource } from '../../datasource/rust-version/index.ts';
import { extractPackageFile } from './extract.ts';
describe('modules/manager/rust-toolchain/extract', () => {
describe('extractPackageFile()', () => {
it('extracts major.minor.patch versions', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "1.89.1"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: '1.89.1',
datasource: RustVersionDatasource.id,
},
],
});
});
it('extracts major.minor ranges', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "1.89"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: '1.89',
datasource: RustVersionDatasource.id,
},
],
});
});
it('extracts stable channel', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "stable"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: 'stable',
datasource: RustVersionDatasource.id,
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('extracts beta channel', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "beta"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: 'beta',
datasource: RustVersionDatasource.id,
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('extracts nightly channel', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "nightly"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: 'nightly',
datasource: RustVersionDatasource.id,
},
],
});
});
it('extracts dated nightly channel', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "nightly-2025-10-12"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: 'nightly-2025-10-12',
datasource: RustVersionDatasource.id,
},
],
});
});
it('returns null for invalid TOML', () => {
const result = extractPackageFile(
'this is not valid toml [',
'rust-toolchain.toml',
);
expect(result).toBeNull();
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns null when [toolchain] section is absent', () => {
const result = extractPackageFile(
'channel = "1.89.1"\n',
'rust-toolchain.toml',
);
expect(result).toBeNull();
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns dep with unspecified-version skipReason when channel is absent', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
components = ["rustfmt"]
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
datasource: RustVersionDatasource.id,
skipReason: 'unspecified-version',
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns dep with unspecified-version skipReason for empty channel', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = ""
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
datasource: RustVersionDatasource.id,
skipReason: 'unspecified-version',
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns dep with path-dependency skipReason when only path is set', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
path = "/path/to/toolchain"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
datasource: RustVersionDatasource.id,
skipReason: 'path-dependency',
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('prefers channel over path when both are set', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "1.89.1"
path = "/path/to/toolchain"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: '1.89.1',
datasource: RustVersionDatasource.id,
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns dep with invalid-version skipReason for unparseable channel value', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "not-a-rust-channel"
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: 'not-a-rust-channel',
datasource: RustVersionDatasource.id,
skipReason: 'invalid-version',
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('can handle additional fields', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "1.89.1"
components = ["rustfmt", "clippy"]
targets = ["wasm32-unknown-unknown"]
`,
'rust-toolchain.toml',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: '1.89.1',
datasource: RustVersionDatasource.id,
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('can read from legacy filename', () => {
const result = extractPackageFile(
codeBlock`
[toolchain]
channel = "1.89.1"
`,
'rust-toolchain',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: '1.89.1',
datasource: RustVersionDatasource.id,
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns null for empty legacy file', () => {
const result = extractPackageFile('', 'rust-toolchain');
expect(result).toBeNull();
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('extracts from legacy format', () => {
const result = extractPackageFile('1.89.1\n', 'rust-toolchain');
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: '1.89.1',
datasource: RustVersionDatasource.id,
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns dep with invalid-version skipReason for legacy file with a single garbage line', () => {
const result = extractPackageFile(
'not-a-rust-channel\n',
'rust-toolchain',
);
expect(result).toEqual({
deps: [
{
depName: 'rust',
depType: 'toolchain',
currentValue: 'not-a-rust-channel',
datasource: RustVersionDatasource.id,
skipReason: 'invalid-version',
},
],
});
expect(logger.logger.warn).not.toHaveBeenCalled();
});
it('returns null for multiline legacy files', () => {
const result = extractPackageFile(
codeBlock`
1.89.1
extra line
another line
`,
'rust-toolchain',
);
expect(result).toBeNull();
expect(logger.logger.warn).not.toHaveBeenCalled();
});
});
});