From 43566de4e9f1f0ef2c7ad02ffec64ec756c807e5 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Mon, 30 Dec 2024 16:58:11 +0000 Subject: [PATCH 1/2] 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 --- packages/yarn-plugin/package.json | 3 +- packages/yarn-plugin/src/index.test.ts | 304 +++++++++++++++++++++++++ yarn.lock | 1 + 3 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 packages/yarn-plugin/src/index.test.ts diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index f237fa45ad..742b9bbe93 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -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" } } diff --git a/packages/yarn-plugin/src/index.test.ts b/packages/yarn-plugin/src/index.test.ts new file mode 100644 index 0000000000..1d733866ba --- /dev/null +++ b/packages/yarn-plugin/src/index.test.ts @@ -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 = {}; + 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); + }, + ); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index b48c24a935..55e1f521c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 From 15de7a09c9cdffd7db278144e377a3d035bacb50 Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Tue, 7 Jan 2025 12:30:36 +0000 Subject: [PATCH 2/2] yarn-plugin: set timout to 30s for all tests and hooks in index suite Signed-off-by: MT Lewis --- packages/yarn-plugin/src/index.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/yarn-plugin/src/index.test.ts b/packages/yarn-plugin/src/index.test.ts index 1d733866ba..d58387bca9 100644 --- a/packages/yarn-plugin/src/index.test.ts +++ b/packages/yarn-plugin/src/index.test.ts @@ -21,6 +21,8 @@ import yaml from 'yaml'; import { findPaths } from '@backstage/cli-common'; import { createMockDirectory } from '@backstage/backend-test-utils'; +jest.setTimeout(30_000); + const mockDir = createMockDirectory(); const executeCommand = ( @@ -148,7 +150,7 @@ describe('Backstage yarn plugin', () => { cwd: mockDir.path, }, ); - }, 30_000); + }); beforeEach(() => { mockDir.addContent({ @@ -181,7 +183,7 @@ describe('Backstage yarn plugin', () => { await expect( fs.readFile(joinPath(mockDir.path, 'yarn.lock'), 'utf-8'), ).resolves.toEqual(initialLockFileContent); - }, 30_000); + }); describe('with backstage:^ dependencies', () => { beforeEach(async () => { @@ -202,7 +204,7 @@ describe('Backstage yarn plugin', () => { }); await runYarnInstall(); - }, 30_000); + }); it('retains existing lockfile entries', async () => { const lockFileContent = await fs.readFile( @@ -238,7 +240,7 @@ describe('Backstage yarn plugin', () => { // 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 () => { @@ -259,7 +261,7 @@ describe('Backstage yarn plugin', () => { }); await runYarnInstall(); - }, 30_000); + }); it('restores the original yarn.lock content', async () => { const lockFileContent = await fs.readFile( @@ -296,7 +298,7 @@ describe('Backstage yarn plugin', () => { expect(stdout).toContain( `Package ${packageName} not found in manifest for Backstage v0.9.0`, ); - }, 30_000); + }); }, ); });