From 3c447e36dcb622f993527f4fea876638ee799bee Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 01:03:57 +0200 Subject: [PATCH] cli: update e2e-test to use build-workspace and inline more scripts --- packages/cli/e2e-test/cli-e2e-test.js | 225 +++++++++++++++++++--- packages/cli/e2e-test/createTestApp.js | 44 ----- packages/cli/e2e-test/createTestPlugin.js | 44 ----- packages/cli/e2e-test/helpers.js | 6 +- 4 files changed, 199 insertions(+), 120 deletions(-) delete mode 100644 packages/cli/e2e-test/createTestApp.js delete mode 100644 packages/cli/e2e-test/createTestPlugin.js diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index c8374af0eb..ff0e9193b0 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -16,45 +16,210 @@ const os = require('os'); const fs = require('fs-extra'); -const { resolve: resolvePath } = require('path'); +const { resolve: resolvePath, join: joinPath } = require('path'); const Browser = require('zombie'); - +const { execFile: execFileCb } = require('child_process'); +const { promisify } = require('util'); const { spawnPiped, handleError, waitForPageWithText, + waitFor, waitForExit, print, } = require('./helpers'); - -const createTestApp = require('./createTestApp'); -const createTestPlugin = require('./createTestPlugin'); - -Browser.localhost('localhost', 3000); - -async function createTempDir() { - return fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); -} +const execFile = promisify(execFileCb); async function main() { - process.env.BACKSTAGE_E2E_CLI_TEST = 'true'; + const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); + print(`CLI E2E test root: ${rootDir}\n`); - const workDir = process.env.CI ? process.cwd() : await createTempDir(); + print('Building dist workspace'); + const workspaceDir = await buildDistWorkspace('workspace', rootDir); - process.stdout.write(`Initial directory: ${process.cwd()}\n`); - process.chdir(workDir); - process.stdout.write(`Working directory: ${process.cwd()}\n`); + print('Creating a Backstage App'); + const appDir = await createApp('test-app', workspaceDir, rootDir); - await createTestApp(); - - const appDir = resolvePath(workDir, 'test-app'); - process.chdir(appDir); - process.stdout.write(`App directory: ${appDir}\n`); - - await createTestPlugin(); + print('Creating a Backstage Plugin'); + const pluginName = await createPlugin('test-plugin', appDir); print('Starting the app'); - const startApp = spawnPiped(['yarn', 'start']); + await testAppServe(pluginName, appDir); + + print('All tests successful, removing test dir'); + await fs.remove(rootDir); +} + +/** + * Builds a dist workspace that contains the cli and core packages + */ +async function buildDistWorkspace(workspaceName, rootDir) { + const workspaceDir = resolvePath(rootDir, workspaceName); + await fs.ensureDir(workspaceDir); + + print(`Preparing workspace`); + await execFile('yarn', [ + 'backstage-cli', + 'build-workspace', + workspaceDir, + '@backstage/cli', + '@backstage/core', + '@backstage/dev-utils', + '@backstage/test-utils', + ]); + + print('Pinning yarn version in workspace'); + await pinYarnVersion(workspaceDir); + + print('Installing workspace dependencies'); + await execFile('yarn', ['install', '--production', '--frozen-lockfile'], { + cwd: workspaceDir, + }); + + return workspaceDir; +} + +/** + * Pin the yarn version in a directory to the one we're using in the Backstage repo + */ +async function pinYarnVersion(dir) { + const repoRoot = resolvePath(__dirname, '../../..'); + + const yarnRc = await fs.readFile(resolvePath(repoRoot, '.yarnrc'), 'utf8'); + const yarnRcLines = yarnRc.split('\n'); + const yarnPathLine = yarnRcLines.find(line => line.startsWith('yarn-path')); + const [, localYarnPath] = yarnPathLine.match(/"(.*)"/); + const yarnPath = resolvePath(repoRoot, localYarnPath); + + await fs.writeFile(resolvePath(dir, '.yarnrc'), `yarn-path "${yarnPath}"\n`); +} + +/** + * Creates a new app inside rootDir called test-app, using packages from the workspaceDir + */ +async function createApp(appName, workspaceDir, rootDir) { + const child = spawnPiped( + [ + resolvePath(workspaceDir, 'packages/cli/bin/backstage-cli'), + 'create-app', + '--skip-install', + ], + { + cwd: rootDir, + }, + ); + + try { + let stdout = ''; + child.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter a name for the app')); + child.stdin.write(`${appName}\n`); + + print('Waiting for app create script to be done'); + await waitForExit(child); + + const appDir = resolvePath(rootDir, appName); + + print('Rewriting module resolutions of app to use workspace packages'); + await overrideModuleResolutions(appDir, workspaceDir); + + print('Pinning yarn version and registry in app'); + await pinYarnVersion(appDir); + await fs.writeFile( + resolvePath(appDir, '.npmrc'), + 'registry=https://registry.npmjs.org/\n', + ); + + print('Test app created'); + + for (const cmd of ['install', 'tsc', 'build', 'lint:all', 'test:all']) { + print(`Running 'yarn ${cmd}' in newly created app`); + await execFile('yarn', [cmd], { cwd: appDir }); + } + + print(`Running 'yarn test:e2e:ci' in newly created app`); + await execFile('yarn', ['test:e2e:ci'], { + cwd: resolvePath(appDir, 'packages', 'app'), + env: { + ...process.env, + APP_CONFIG_app_baseUrl: '"http://localhost:3001"', + }, + }); + + return appDir; + } finally { + child.kill(); + } +} + +/** + * This points dependency resolutions into the workspace for each package that is present there + */ +async function overrideModuleResolutions(appDir, workspaceDir) { + const pkgJsonPath = resolvePath(appDir, 'package.json'); + const pkgJson = await fs.readJson(pkgJsonPath); + + pkgJson.resolutions = pkgJson.resolutions || {}; + pkgJson.dependencies = pkgJson.dependencies || {}; + + const packageNames = await fs.readdir(resolvePath(workspaceDir, 'packages')); + for (const name of packageNames) { + const pkgPath = joinPath('..', 'workspace', 'packages', name); + + pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`; + pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`; + delete pkgJson.devDependencies[`@backstage/${name}`]; + } + fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); +} + +/** + * Uses create-plugin command to create a new plugin in the app + */ +async function createPlugin(pluginName, appDir) { + const child = spawnPiped(['yarn', 'create-plugin'], { + cwd: appDir, + }); + + try { + let stdout = ''; + child.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter an ID for the plugin')); + child.stdin.write(`${pluginName}\n`); + + // await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); + // child.stdin.write('@someuser\n'); + + print('Waiting for plugin create script to be done'); + await waitForExit(child); + + const pluginDir = resolvePath(appDir, 'plugins', pluginName); + for (const cmd of [['lint'], ['test', '--no-watch']]) { + print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`); + await execFile('yarn', cmd, { cwd: pluginDir }); + } + + return pluginName; + } finally { + child.kill(); + } +} + +/** + * Start serving the newly created app and make sure that the create plugin is rendering correctly + */ +async function testAppServe(pluginName, appDir) { + const startApp = spawnPiped(['yarn', 'start'], { + cwd: appDir, + detached: true, + }); + Browser.localhost('localhost', 3000); try { const browser = new Browser(); @@ -62,19 +227,19 @@ async function main() { await waitForPageWithText(browser, '/', 'Welcome to Backstage'); await waitForPageWithText( browser, - '/test-plugin', - 'Welcome to test-plugin!', + `/${pluginName}`, + `Welcome to ${pluginName}!`, ); print('Both App and Plugin loaded correctly'); + } catch (error) { + throw new Error(`App serve test failed, ${error}`); } finally { - startApp.kill(); + // Kill entire process group, otherwise we'll end up with hanging serve processes + process.kill(-startApp.pid, 'SIGTERM'); } await waitForExit(startApp); - - print('All tests done'); - process.exit(0); } process.on('unhandledRejection', handleError); diff --git a/packages/cli/e2e-test/createTestApp.js b/packages/cli/e2e-test/createTestApp.js deleted file mode 100644 index 5437804975..0000000000 --- a/packages/cli/e2e-test/createTestApp.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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. - */ - -const { resolve: resolvePath } = require('path'); -const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); - -async function createTestApp() { - const cliPath = resolvePath(__dirname, '../bin/backstage-cli'); - - print('Creating a Backstage App'); - const createApp = spawnPiped(['node', cliPath, 'create-app']); - - try { - let stdout = ''; - createApp.stdout.on('data', data => { - stdout = stdout + data.toString('utf8'); - }); - - await waitFor(() => stdout.includes('Enter a name for the app')); - createApp.stdin.write('test-app\n'); - - print('Waiting for app create script to be done'); - await waitForExit(createApp); - - print('Test app created'); - } finally { - createApp.kill(); - } -} - -module.exports = createTestApp; diff --git a/packages/cli/e2e-test/createTestPlugin.js b/packages/cli/e2e-test/createTestPlugin.js deleted file mode 100644 index 59e8d6dd77..0000000000 --- a/packages/cli/e2e-test/createTestPlugin.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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. - */ - -const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); - -async function createTestPlugin() { - print('Creating a Backstage Plugin'); - const createPlugin = spawnPiped(['yarn', 'create-plugin']); - - try { - let stdout = ''; - createPlugin.stdout.on('data', data => { - stdout = stdout + data.toString('utf8'); - }); - - await waitFor(() => stdout.includes('Enter an ID for the plugin')); - createPlugin.stdin.write('test-plugin\n'); - - // await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); - // createPlugin.stdin.write('@someuser\n'); - - print('Waiting for plugin create script to be done'); - await waitForExit(createPlugin); - - print('Test plugin created'); - } finally { - createPlugin.kill(); - } -} - -module.exports = createTestPlugin; diff --git a/packages/cli/e2e-test/helpers.js b/packages/cli/e2e-test/helpers.js index d7f3f28a7a..1292ba19bf 100644 --- a/packages/cli/e2e-test/helpers.js +++ b/packages/cli/e2e-test/helpers.js @@ -43,13 +43,15 @@ function spawnPiped(cmd, options) { process.exit(code); } }); + + const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' '); child.stdout.on( 'data', - pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `), + pipeWithPrefix(process.stdout, `[${logPrefix}].out: `), ); child.stderr.on( 'data', - pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `), + pipeWithPrefix(process.stderr, `[${logPrefix}].err: `), ); return child;