yarn-plugin: add integration test suite

The new test suite fully exercises the plugin,
including onboarding and offboarding from the
plugin and the effect on lockfiles.

Signed-off-by: MT Lewis <mtlewis@users.noreply.github.com>
This commit is contained in:
MT Lewis
2024-12-30 16:58:11 +00:00
parent ec1b68334d
commit 43566de4e9
3 changed files with 307 additions and 1 deletions
+2 -1
View File
@@ -42,6 +42,7 @@
"@backstage/cli": "workspace:^",
"@yarnpkg/builder": "^4.0.0",
"fs-extra": "^11.2.0",
"nodemon": "^3.0.1"
"nodemon": "^3.0.1",
"yaml": "^2.0.0"
}
}
+304
View File
@@ -0,0 +1,304 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { join as joinPath } from 'path';
import { spawn, SpawnOptionsWithoutStdio } from 'child_process';
import fs from 'fs-extra';
import yaml from 'yaml';
import { findPaths } from '@backstage/cli-common';
import { createMockDirectory } from '@backstage/backend-test-utils';
const mockDir = createMockDirectory();
const executeCommand = (
command: string,
args: string[],
options?: SpawnOptionsWithoutStdio,
): Promise<{
exitCode: number;
stdout: string;
stderr: string;
}> => {
return new Promise((resolve, reject) => {
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
const shell = process.platform === 'win32';
const proc = spawn(command, args, { ...options, shell });
proc.stdout?.on('data', data => {
stdout.push(Buffer.from(data));
});
proc.stderr?.on('data', data => {
stderr.push(Buffer.from(data));
});
proc.on('error', reject);
proc.on('exit', exitCode => {
resolve({
exitCode: exitCode ?? 0,
stdout: Buffer.concat(stdout).toString('utf8'),
stderr: Buffer.concat(stderr).toString('utf8'),
});
});
});
};
const runYarnInstall = () =>
executeCommand('yarn', ['--mode=update-lockfile'], { cwd: mockDir.path });
const nonWorkspaceEntries = (lockFileString: string = '') => {
const lockFile = yaml.parse(lockFileString);
const result: Record<string, string> = {};
for (const key of Object.keys(lockFile)) {
if (lockFile[key].version !== '0.0.0-use.local') {
result[key] = lockFile[key];
}
}
return result;
};
describe('Backstage yarn plugin', () => {
describe.each`
yarnVersion
${'4.1.1'}
${'stable'}
`('yarn $yarnVersion', ({ yarnVersion }) => {
let initialLockFileContent: string | undefined;
beforeAll(async () => {
const { targetRoot } = findPaths(process.cwd());
await executeCommand('yarn', ['build'], {
cwd: joinPath(targetRoot, 'packages/yarn-plugin'),
});
mockDir.setContent({
'yarn.lock': '',
'package.json': JSON.stringify({
workspaces: {
packages: ['packages/*'],
},
}),
'backstage.json': JSON.stringify({ version: '0.9.0' }),
packages: {
a: {
'package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/config': '^0.1.2',
},
}),
},
b: {
'package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/cli-common': '^0.1.1',
'@backstage/config': '^0.1.2',
},
}),
},
},
});
await executeCommand('yarn', ['set', 'version', yarnVersion], {
cwd: mockDir.path,
});
await executeCommand(
'yarn',
['config', 'set', 'enableGlobalCache', 'true'],
{ cwd: mockDir.path },
);
await runYarnInstall();
initialLockFileContent = await fs.readFile(
joinPath(mockDir.path, 'yarn.lock'),
'utf-8',
);
await executeCommand(
'yarn',
[
'plugin',
'import',
joinPath(
targetRoot,
'packages/yarn-plugin/bundles/@yarnpkg/plugin-backstage.js',
),
],
{
cwd: mockDir.path,
},
);
}, 30_000);
beforeEach(() => {
mockDir.addContent({
'backstage.json': JSON.stringify({ version: '0.9.0' }),
packages: {
a: {
'package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/config': '^0.1.2',
},
}),
},
b: {
'package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/cli-common': '^0.1.1',
'@backstage/config': '^0.1.2',
},
}),
},
},
});
});
it('does not change yarn.lock when no backstage:^ versions are present', async () => {
await runYarnInstall();
await expect(
fs.readFile(joinPath(mockDir.path, 'yarn.lock'), 'utf-8'),
).resolves.toEqual(initialLockFileContent);
}, 30_000);
describe('with backstage:^ dependencies', () => {
beforeEach(async () => {
mockDir.addContent({
'packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/config': 'backstage:^',
},
}),
'packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/config': 'backstage:^',
'@backstage/cli-common': 'backstage:^',
},
}),
});
await runYarnInstall();
}, 30_000);
it('retains existing lockfile entries', async () => {
const lockFileContent = await fs.readFile(
joinPath(mockDir.path, 'yarn.lock'),
'utf-8',
);
expect(nonWorkspaceEntries(lockFileContent)).toEqual(
nonWorkspaceEntries(initialLockFileContent),
);
});
it('bumps packages when backstage.json changes', async () => {
mockDir.addContent({
'backstage.json': JSON.stringify({
version: '1.0.0',
}),
});
await runYarnInstall();
const lockFileContent = await fs.readFile(
joinPath(mockDir.path, 'yarn.lock'),
'utf-8',
);
const lockFile = yaml.parse(lockFileContent);
// Versions from old manifest no longer appear in lockfile
expect(lockFile['@backstage/cli-common@npm:^0.1.1']).toBeUndefined();
expect(lockFile['@backstage/config@npm:^0.1.2']).toBeUndefined();
// Versions from new manifest have been added to lockfile
expect(lockFile['@backstage/cli-common@npm:^0.1.8']).toBeDefined();
expect(lockFile['@backstage/config@npm:^1.0.0']).toBeDefined();
}, 30_000);
describe('after removing backstage:^ dependencies', () => {
beforeEach(async () => {
mockDir.addContent({
'packages/a/package.json': JSON.stringify({
name: 'a',
dependencies: {
'@backstage/config': '^0.1.2',
},
}),
'packages/b/package.json': JSON.stringify({
name: 'b',
dependencies: {
'@backstage/config': '^0.1.2',
'@backstage/cli-common': '^0.1.1',
},
}),
});
await runYarnInstall();
}, 30_000);
it('restores the original yarn.lock content', async () => {
const lockFileContent = await fs.readFile(
joinPath(mockDir.path, 'yarn.lock'),
'utf-8',
);
expect(lockFileContent).toEqual(initialLockFileContent);
});
});
describe.each`
packageName | description
${'node-fetch'} | ${'non-Backstage package'}
${'@backstage/foo'} | ${'invalid Backstage package'}
`(
'when backstage:^ range is used with $description "$packageName"',
({ packageName }) => {
beforeEach(async () => {
mockDir.addContent({
'packages/c/package.json': JSON.stringify({
name: 'c',
dependencies: {
[packageName]: 'backstage:^',
},
}),
});
});
it('fails to install', async () => {
const { exitCode, stdout } = await runYarnInstall();
expect(exitCode).toEqual(1);
expect(stdout).toContain(
`Package ${packageName} not found in manifest for Backstage v0.9.0`,
);
}, 30_000);
},
);
});
});
});
+1
View File
@@ -47628,6 +47628,7 @@ __metadata:
fs-extra: ^11.2.0
nodemon: ^3.0.1
semver: ^7.6.0
yaml: ^2.0.0
languageName: unknown
linkType: soft