mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 23:11:31 +00:00
fbedd41e82
* test: replace manual process.env mutation with vi.stubEnv
Enable Vitest's `unstubEnvs` so env stubs are reverted automatically
before every test, and convert the manual `process.env.X = ...` /
`delete process.env.X` pairs in specs to `vi.stubEnv()`.
This removes the hand-written `afterEach` cleanup blocks and the
`const OLD_ENV = process.env` / `process.env = { ...OLD_ENV }`
save-restore dance, which silently leaked state between tests when a
test set a variable without a matching delete.
Left as-is (incompatible with `unstubEnvs`, which unstubs before each
test, not after):
- module top-level env writes (cocoapods, gomod, post-update, pip-compile
and bundler artifacts specs)
- `beforeAll` env writes -- the two in `util/http` moved to `beforeEach`
* test: stop asserting on ambient environment variables
Three specs asserted on values that the surrounding environment can
override, so they only passed on a machine that happened not to set them:
- `util/exec/env`: `getChildProcessEnv()` forwards every entry of
`basicEnvVars`, but the spec only seeded a subset. Any other forwarded
variable that was set ambiently (e.g. a lowercase `http_proxy`) leaked
into the `toEqual` assertion. Clear all of them first.
- `util/git/index`: `process.env.GIT_SSH_COMMAND` deliberately overrides
the default that the two env-inheritance tests assert on. Clear it.
- `logger/pretty-stdout`: the colorized level strings are built at import
time, so the `beforeEach` setting `FORCE_COLOR` could never affect them
-- it was dead code, and the test silently assumed a terminal without
colour support. Derive the expectation the same way the module does.
* test(datasource/nuget): use a throwaway cache dir for nupkg downloads
The `determine source URL from nupkg` tests do not mock the filesystem,
so they really wrote downloaded packages into a fixed `/tmp/cache` path.
That fails wherever the system tmpdir is not writable, and two checkouts
running the suite at once would share the same directory.
Use `tmp.dir({ unsafeCleanup: true })` and clean it up afterwards, which
is the pattern the other specs that touch disk already use.
* feat(lint): add renovate/prefer-stub-env rule
Enforces in specs what the migration to `vi.stubEnv` established:
- assigning to or deleting `process.env.X` inside `beforeEach`, `afterEach`,
`it` or `test` is reported, with a fixer that rewrites it to `vi.stubEnv()`
- module scope, `describe` bodies and `beforeAll` / `afterAll` are left alone:
`unstubEnvs` reverts stubs *before* each test, so a stub created there is
gone by the time the first test runs, and those must keep assigning directly
- replacing the `process.env` object is reported everywhere and never fixed.
`vi.stubEnv()` captures the original object when the worker starts, so after
a reassignment its deletes silently target the old object
The rule found 28 further violations in specs the earlier sweep missed, since
that one was anchored on `delete process.env`. Twelve were auto-fixed; the
rest replaced `process.env` wholesale and are now `clearEnv()` from test/util.
`util/exec/index.spec.ts` opts out with a comment: it asserts on the whole
child env, and `vi.stubEnv()` cannot express that because it maps `PROD`,
`DEV` and `SSR` onto '' instead of deleting them.
287 lines
9.2 KiB
TypeScript
287 lines
9.2 KiB
TypeScript
import { GlobalConfig } from '../../config/global.ts';
|
|
import * as _datasource from '../../modules/datasource/index.ts';
|
|
import {
|
|
generateInstallCommands,
|
|
getToolConfig,
|
|
isDynamicInstall,
|
|
resolveConstraint,
|
|
} from './containerbase.ts';
|
|
import type { ToolConstraint, ToolName } from './types.ts';
|
|
|
|
vi.mock('../../modules/datasource/index.ts');
|
|
|
|
const datasource = vi.mocked(_datasource);
|
|
|
|
describe('util/exec/containerbase', () => {
|
|
describe('isDynamicInstall()', () => {
|
|
beforeEach(() => {
|
|
GlobalConfig.reset();
|
|
vi.stubEnv('CONTAINERBASE', undefined);
|
|
});
|
|
|
|
it('returns false if binarySource is not install', () => {
|
|
expect(isDynamicInstall()).toBeFalse();
|
|
});
|
|
|
|
it('returns false if not containerbase', () => {
|
|
GlobalConfig.set({ binarySource: 'install' });
|
|
expect(isDynamicInstall()).toBeFalse();
|
|
});
|
|
|
|
it('returns false if any unsupported tools', () => {
|
|
GlobalConfig.set({ binarySource: 'install' });
|
|
vi.stubEnv('CONTAINERBASE', 'true');
|
|
const toolConstraints: ToolConstraint[] = [
|
|
{ toolName: 'node' },
|
|
// @ts-expect-error -- intentionally using invalid constraint names
|
|
{ toolName: 'invalid' },
|
|
];
|
|
expect(isDynamicInstall(toolConstraints)).toBeFalse();
|
|
});
|
|
|
|
it('returns true if supported tools', () => {
|
|
GlobalConfig.set({ binarySource: 'install' });
|
|
vi.stubEnv('CONTAINERBASE', 'true');
|
|
const toolConstraints: ToolConstraint[] = [{ toolName: 'npm' }];
|
|
expect(isDynamicInstall(toolConstraints)).toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('getToolConfig()', () => {
|
|
it('returns config for a known tool', () => {
|
|
const config = getToolConfig('npm');
|
|
expect(config).toBeDefined();
|
|
expect(config).toMatchObject({
|
|
datasource: expect.toBeString(),
|
|
versioning: expect.toBeString(),
|
|
});
|
|
});
|
|
|
|
it('returns undefined for an unknown tool', () => {
|
|
// @ts-expect-error -- intentionally using invalid tool name
|
|
expect(getToolConfig('unknown-tool')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('resolveConstraint()', () => {
|
|
it('returns from config', async () => {
|
|
expect(
|
|
await resolveConstraint({ toolName: 'composer', constraint: '1.1.0' }),
|
|
).toBe('1.1.0');
|
|
});
|
|
|
|
it('returns highest stable', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [
|
|
{ version: '1.0.0' },
|
|
{ version: '1.1.0' },
|
|
{ version: '1.3.0' },
|
|
{ version: '2.0.14' },
|
|
{ version: '2.1.0' },
|
|
{ version: '2.2.0-pre.0' },
|
|
],
|
|
});
|
|
expect(await resolveConstraint({ toolName: 'composer' })).toBe('2.1.0');
|
|
});
|
|
|
|
it('returns highest unstable', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [{ version: '2.0.14-b.1' }, { version: '2.1.0-a.1' }],
|
|
});
|
|
expect(await resolveConstraint({ toolName: 'composer' })).toBe(
|
|
'2.1.0-a.1',
|
|
);
|
|
});
|
|
|
|
it('respects latest', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
tags: {
|
|
latest: '2.0.14',
|
|
},
|
|
releases: [
|
|
{ version: '1.0.0' },
|
|
{ version: '1.1.0' },
|
|
{ version: '1.3.0' },
|
|
{ version: '2.0.14' },
|
|
{ version: '2.1.0' },
|
|
{ version: '2.2.0-pre.0' },
|
|
],
|
|
});
|
|
expect(await resolveConstraint({ toolName: 'composer' })).toBe('2.0.14');
|
|
});
|
|
|
|
it('supports rust release channels', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [
|
|
{ version: '1' },
|
|
{ version: '1.65' },
|
|
{ version: '1.65.0' },
|
|
],
|
|
});
|
|
expect(await resolveConstraint({ toolName: 'rust' })).toBe('1.65.0');
|
|
});
|
|
|
|
it('supports nightly rust release channels', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [{ version: 'nightly' }, { version: 'nightly-2026-06-19' }],
|
|
});
|
|
expect(await resolveConstraint({ toolName: 'rust' })).toBe(
|
|
'nightly-2026-06-19',
|
|
);
|
|
});
|
|
|
|
it('throws for unknown tools', async () => {
|
|
await expect(
|
|
resolveConstraint({ toolName: 'whoops' as ToolName }),
|
|
).rejects.toThrow('Invalid tool to install: whoops');
|
|
});
|
|
|
|
it('throws no releases', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [],
|
|
});
|
|
await expect(resolveConstraint({ toolName: 'composer' })).rejects.toThrow(
|
|
'No tool releases found.',
|
|
);
|
|
});
|
|
|
|
it('falls back to latest version if no compatible release', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [{ version: '1.2.3' }],
|
|
});
|
|
expect(
|
|
await resolveConstraint({ toolName: 'composer', constraint: '^3.1.0' }),
|
|
).toBe('1.2.3');
|
|
});
|
|
|
|
it('falls back to latest version if invalid constraint', async () => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [{ version: '1.2.3' }],
|
|
});
|
|
expect(
|
|
await resolveConstraint({ toolName: 'composer', constraint: 'whoops' }),
|
|
).toBe('1.2.3');
|
|
});
|
|
|
|
it.each`
|
|
version | expected
|
|
${'^3.9.0'} | ${'3.10.4'}
|
|
${'^3.9'} | ${'3.10.4'}
|
|
${'3.9.*'} | ${'3.9.1'}
|
|
${'>3.8,<3.10'} | ${'3.9.1'}
|
|
${'==3.9.*'} | ${'3.9.1'}
|
|
`(
|
|
'supports python ranges "$version" => "$expected"',
|
|
async ({ version: constraint, expected }) => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [
|
|
{ version: '3.9.0' },
|
|
{ version: '3.9.1' },
|
|
{ version: '3.10.0' },
|
|
{ version: '3.10.4' },
|
|
],
|
|
});
|
|
expect(
|
|
await resolveConstraint({ toolName: 'python', constraint }),
|
|
).toBe(expected);
|
|
},
|
|
);
|
|
|
|
it('removes pep440 ==', async () => {
|
|
expect(
|
|
await resolveConstraint({
|
|
toolName: 'pipenv',
|
|
constraint: '==2020.8.13',
|
|
}),
|
|
).toBe('2020.8.13');
|
|
});
|
|
|
|
it.each`
|
|
version | expected
|
|
${'>=2.5.0 <2.6.0'} | ${'2.5.1'}
|
|
${'>=2.6.0-0.0.pre <2.7.0-0.0.pre'} | ${'2.6.0-0.0.pre'}
|
|
${'>=2.8.0'} | ${'2.8.1'}
|
|
${'<2.9.0-0.1.pre'} | ${'2.8.1'}
|
|
${'2.10.0-0.2.pre'} | ${'2.10.0-0.2.pre'}
|
|
${'>=2.11.0-0.1.pre'} | ${'2.12.0-4.1.pre'}
|
|
${'<=2.10.0'} | ${'2.8.1'}
|
|
`(
|
|
'supports flutter ranges "$version" => "$expected"',
|
|
async ({ version: constraint, expected }) => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [
|
|
{ version: '2.5.0-1.0.pre', isStable: false },
|
|
{ version: '2.5.0', isStable: true },
|
|
{ version: '2.5.1', isStable: true },
|
|
{ version: '2.6.0-0.0.pre', isStable: false },
|
|
{ version: '2.8.0', isStable: true },
|
|
{ version: '2.8.1', isStable: true },
|
|
{ version: '2.9.0-0.1.pre', isStable: false },
|
|
{ version: '2.12.0-4.1.pre', isStable: false },
|
|
],
|
|
});
|
|
expect(
|
|
await resolveConstraint({ toolName: 'flutter', constraint }),
|
|
).toBe(expected);
|
|
},
|
|
);
|
|
|
|
it.each`
|
|
version | expected
|
|
${'>2.17.0 <2.17.8'} | ${'2.17.7'}
|
|
${'>2.19.0'} | ${'2.19.0-81.0.dev'}
|
|
${'<=2.17.5'} | ${'2.17.5'}
|
|
${'<2.17.5'} | ${'2.19.0-81.0.dev'}
|
|
${'<2.17.6'} | ${'2.17.5'}
|
|
`(
|
|
'supports dart ranges "$version" => "$expected"',
|
|
async ({ version: constraint, expected }) => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [
|
|
{ version: '2.17.0-69.2.beta', isStable: false },
|
|
{ version: '2.17.0-7.0.dev', isStable: false },
|
|
{ version: '2.17.5', isStable: true },
|
|
{ version: '2.17.6', isStable: true },
|
|
{ version: '2.17.7', isStable: true },
|
|
{ version: '2.18.0', isStable: true },
|
|
{ version: '2.18.0-44.1.beta', isStable: false },
|
|
{ version: '2.18.0-99.0.dev', isStable: false },
|
|
{ version: '2.18.4', isStable: true },
|
|
{ version: '2.18.5', isStable: true },
|
|
{ version: '2.19.0-255.2.beta', isStable: false },
|
|
{ version: '2.19.0-81.0.dev', isStable: false },
|
|
],
|
|
});
|
|
expect(await resolveConstraint({ toolName: 'dart', constraint })).toBe(
|
|
expected,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('generateInstallCommands()', () => {
|
|
beforeEach(() => {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [
|
|
{ version: '1.0.0' },
|
|
{ version: '1.1.0' },
|
|
{ version: '1.3.0' },
|
|
{ version: '2.0.14' },
|
|
{ version: '2.1.0' },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('returns install commands', async () => {
|
|
const toolConstraints: ToolConstraint[] = [
|
|
{
|
|
toolName: 'composer',
|
|
},
|
|
];
|
|
expect(await generateInstallCommands(toolConstraints)).toEqual([
|
|
'install-tool composer 2.1.0',
|
|
]);
|
|
});
|
|
});
|
|
});
|