e2e-test: update to use new run utils from cli-common

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-11-29 13:47:02 +01:00
parent 5cfb2a4ea8
commit ecb44ccdf1
4 changed files with 31 additions and 115 deletions
+2 -2
View File
@@ -15,12 +15,12 @@
*/
import { Command } from 'commander';
import { run } from './run';
import { runCommand } from './runCommand';
export function registerCommands(program: Command) {
program
.command('run')
.option('--keep', 'Do not remove the temporary dir after tests complete')
.description('Run e2e tests')
.action(run);
.action(runCommand);
}
@@ -22,18 +22,12 @@ import killTree from 'tree-kill';
import { resolve as resolvePath, join as joinPath } from 'path';
import path from 'path';
import {
spawnPiped,
runPlain,
waitFor,
waitForExit,
print,
} from '../lib/helpers';
import { waitFor, print } from '../lib/helpers';
import mysql from 'mysql2/promise';
import pgtools from 'pgtools';
import { findPaths } from '@backstage/cli-common';
import { findPaths, runOutput, run } from '@backstage/cli-common';
import { OptionValues } from 'commander';
// eslint-disable-next-line no-restricted-syntax
@@ -46,7 +40,7 @@ const templatePackagePaths = [
'packages/create-app/templates/default-app/packages/backend/package.json.hbs',
];
export async function run(opts: OptionValues) {
export async function runCommand(opts: OptionValues) {
const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
print(`CLI E2E test root: ${rootDir}\n`);
@@ -67,7 +61,7 @@ export async function run(opts: OptionValues) {
await createPlugin({ appDir, pluginId, select: 'backend-plugin' });
print(`Running 'yarn test:e2e' in newly created app with new plugin`);
await runPlain(['yarn', 'test:e2e'], {
await runOutput(['yarn', 'test:e2e'], {
cwd: appDir,
env: { ...process.env, CI: undefined },
});
@@ -75,13 +69,13 @@ export async function run(opts: OptionValues) {
await switchToReact17(appDir);
print(`Running 'yarn install' to install React 17`);
await runPlain(['yarn', 'install'], { cwd: appDir });
await runOutput(['yarn', 'install'], { cwd: appDir });
print(`Running 'yarn tsc' with React 17`);
await runPlain(['yarn', 'tsc'], { cwd: appDir });
await runOutput(['yarn', 'tsc'], { cwd: appDir });
print(`Running 'yarn test:e2e' with React 17`);
await runPlain(['yarn', 'test:e2e'], {
await runOutput(['yarn', 'test:e2e'], {
cwd: appDir,
env: { ...process.env, CI: undefined },
});
@@ -190,7 +184,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) {
appendDeps(require('@backstage/create-app/package.json'));
print(`Preparing workspace`);
await runPlain([
await runOutput([
'yarn',
'backstage-cli',
'build-workspace',
@@ -209,7 +203,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) {
}
print('Installing workspace dependencies');
await runPlain(['yarn', 'workspaces', 'focus', '--all', '--production'], {
await runOutput(['yarn', 'workspaces', 'focus', '--all', '--production'], {
cwd: workspaceDir,
});
@@ -257,7 +251,7 @@ async function createApp(
workspaceDir: string,
rootDir: string,
) {
const child = spawnPiped(
const child = run(
[
'node',
resolvePath(workspaceDir, 'packages/create-app/bin/backstage-create-app'),
@@ -265,6 +259,7 @@ async function createApp(
],
{
cwd: rootDir,
stdio: ['pipe', 'pipe', 'pipe'],
},
);
@@ -278,7 +273,7 @@ async function createApp(
child.stdin?.write(`${appName}\n`);
print('Waiting for app create script to be done');
await waitForExit(child);
await child.waitForExit();
const appDir = resolvePath(rootDir, appName);
@@ -318,7 +313,7 @@ async function createApp(
'test:all',
]) {
print(`Running 'yarn ${cmd}' in newly created app`);
await runPlain(['yarn', cmd], { cwd: appDir });
await runOutput(['yarn', cmd], { cwd: appDir });
}
return appDir;
@@ -378,10 +373,11 @@ async function createPlugin(options: {
select: string;
}) {
const { appDir, pluginId, select } = options;
const child = spawnPiped(
const child = run(
['yarn', 'new', '--select', select, '--option', `pluginId=${pluginId}`],
{
cwd: appDir,
stdio: ['pipe', 'pipe', 'pipe'],
},
);
@@ -392,7 +388,7 @@ async function createPlugin(options: {
});
print('Waiting for plugin create script to be done');
await waitForExit(child);
await child.waitForExit();
const pluginDir = resolvePath(
appDir,
@@ -401,11 +397,11 @@ async function createPlugin(options: {
);
print(`Running 'yarn tsc' in root for newly created plugin`);
await runPlain(['yarn', 'tsc'], { cwd: appDir });
await runOutput(['yarn', 'tsc'], { cwd: appDir });
for (const cmd of [['lint'], ['test', '--no-watch']]) {
print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`);
await runPlain(['yarn', ...cmd], { cwd: pluginDir });
await runOutput(['yarn', ...cmd], { cwd: pluginDir });
}
} finally {
child.kill();
@@ -494,7 +490,7 @@ async function dropClientDatabases(client: string) {
* Start serving the newly created backend and make sure that all db migrations works correctly
*/
async function testBackendStart(appDir: string, ...args: string[]) {
const child = spawnPiped(['yarn', 'workspace', 'backend', 'start', ...args], {
const child = run(['yarn', 'workspace', 'backend', 'start', ...args], {
cwd: appDir,
// Windows does not like piping stdin here, the child process will hang when requiring the 'process' module
stdio: ['ignore', 'pipe', 'pipe'],
@@ -586,7 +582,7 @@ async function testBackendStart(appDir: string, ...args: string[]) {
}
try {
await waitForExit(child);
await child.waitForExit();
} catch (error) {
if (!successful) {
throw new Error(`Backend failed to startup: ${stderr}`);
+9 -2
View File
@@ -18,7 +18,6 @@ import { program } from 'commander';
import chalk from 'chalk';
import { registerCommands } from './commands';
import { version } from '../package.json';
import { exitWithError } from './lib/helpers';
async function main(argv: string[]) {
program.name('e2e-test').version(version);
@@ -36,4 +35,12 @@ async function main(argv: string[]) {
program.parse(argv);
}
main(process.argv).catch(exitWithError);
main(process.argv).catch(err => {
process.stdout.write(`${err.name}: ${err.stack || err.message}\n`);
if (typeof err.code === 'number') {
process.exit(err.code);
} else {
process.exit(1);
}
});
-87
View File
@@ -14,77 +14,6 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import {
spawn,
execFile as execFileCb,
SpawnOptions,
ChildProcess,
} from 'child_process';
import { promisify } from 'util';
const execFile = promisify(execFileCb);
export function spawnPiped(cmd: string[], options?: SpawnOptions) {
function pipeWithPrefix(stream: NodeJS.WriteStream, prefix = '') {
return (data: Buffer) => {
const prefixedMsg = data
.toString('utf8')
.trimEnd()
.replace(/^/gm, prefix);
stream.write(`${prefixedMsg}\n`, 'utf8');
};
}
const child = spawn(cmd[0], cmd.slice(1), {
stdio: 'pipe',
shell: true,
...options,
});
child.on('error', exitWithError);
const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' ');
child.stdout?.on(
'data',
pipeWithPrefix(process.stdout, `[${logPrefix}].out: `),
);
child.stderr?.on(
'data',
pipeWithPrefix(process.stderr, `[${logPrefix}].err: `),
);
return child;
}
export async function runPlain(cmd: string[], options?: SpawnOptions) {
try {
const { stdout } = await execFile(cmd[0], cmd.slice(1), {
...options,
shell: true,
});
return stdout.trim();
} catch (error) {
assertError(error);
if (error.stdout) {
process.stdout.write(error.stdout as Buffer);
}
if (error.stderr) {
process.stderr.write(error.stderr as Buffer);
}
throw error;
}
}
export function exitWithError(err: Error & { code?: unknown }) {
process.stdout.write(`${err.name}: ${err.stack || err.message}\n`);
if (typeof err.code === 'number') {
process.exit(err.code);
} else {
process.exit(1);
}
}
/**
* Waits for fn() to be true
* Checks every 100ms
@@ -108,22 +37,6 @@ export function waitFor(fn: () => boolean, maxSeconds: number = 120) {
});
}
export async function waitForExit(child: ChildProcess) {
if (child.exitCode !== null) {
throw new Error(`Child already exited with code ${child.exitCode}`);
}
await new Promise<void>((resolve, reject) =>
child.once('exit', code => {
if (code) {
reject(new Error(`Child exited with code ${code}`));
} else {
print('Child finished');
resolve();
}
}),
);
}
export function print(msg: string) {
return process.stdout.write(`${msg}\n`);
}