From 26f4d662ee79f0647d27544a2eeb6d081c99407c Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 27 May 2022 14:27:09 -0700 Subject: [PATCH 1/3] test: extract test runner method --- tests/legacy-cli/e2e_runner.ts | 220 ++++++++++++++++----------------- 1 file changed, 106 insertions(+), 114 deletions(-) diff --git a/tests/legacy-cli/e2e_runner.ts b/tests/legacy-cli/e2e_runner.ts index 4d15654b45c3..8bfaf3d5c137 100644 --- a/tests/legacy-cli/e2e_runner.ts +++ b/tests/legacy-cli/e2e_runner.ts @@ -70,7 +70,6 @@ function lastLogger() { } const testGlob = argv.glob || 'tests/**/*.ts'; -let currentFileName = ''; const e2eRoot = path.join(__dirname, 'e2e'); const allSetups = glob.sync('setup/**/*.ts', { nodir: true, cwd: e2eRoot }).sort(); @@ -122,124 +121,117 @@ setGlobalVariable('argv', argv); setGlobalVariable('ci', process.env['CI']?.toLowerCase() === 'true' || process.env['CI'] === '1'); setGlobalVariable('package-manager', argv.yarn ? 'yarn' : 'npm'); -Promise.all([findFreePort(), findFreePort()]).then(async ([httpPort, httpsPort]) => { - setGlobalVariable('package-registry', 'http://localhost:' + httpPort); - setGlobalVariable('package-secure-registry', 'http://localhost:' + httpsPort); +Promise.all([findFreePort(), findFreePort()]) + .then(async ([httpPort, httpsPort]) => { + setGlobalVariable('package-registry', 'http://localhost:' + httpPort); + setGlobalVariable('package-secure-registry', 'http://localhost:' + httpsPort); - const registryProcess = await createNpmRegistry(httpPort, httpPort); - const secureRegistryProcess = await createNpmRegistry(httpPort, httpsPort, true); + let lastTestRun: string | null = null; - return ( - testsToRun - .reduce((previous, relativeName, testIndex) => { - // Make sure this is a windows compatible path. - let absoluteName = path.join(e2eRoot, relativeName); - if (/^win/.test(process.platform)) { - absoluteName = absoluteName.replace(/\\/g, path.posix.sep); + // NPM registries for the lifetime of the test execution + const registryProcess = await createNpmRegistry(httpPort, httpPort); + const secureRegistryProcess = await createNpmRegistry(httpPort, httpsPort, true); + + try { + for (const [testIndex, test] of testsToRun.entries()) { + await runTest((lastTestRun = test), testIndex); + } + + console.log(colors.green('Done.')); + } catch (err) { + console.log('\n'); + console.error(colors.red(`Test "${lastTestRun}" failed...`)); + console.error(colors.red(err.message)); + console.error(colors.red(err.stack)); + + if (argv.debug) { + console.log(`Current Directory: ${process.cwd()}`); + console.log('Will loop forever while you debug... CTRL-C to quit.'); + + /* eslint-disable no-constant-condition */ + while (1) { + // That's right! } + } - return previous.then(() => { - currentFileName = relativeName.replace(/\.ts$/, ''); - const start = +new Date(); - - const module = require(absoluteName); - const originalEnvVariables = { - ...process.env, - }; - - const fn: (skipClean?: () => void) => Promise | void = - typeof module == 'function' - ? module - : typeof module.default == 'function' - ? module.default - : () => { - throw new Error('Invalid test module.'); - }; - - let clean = true; - let previousDir: string | null = null; - - return Promise.resolve() - .then(() => printHeader(currentFileName, testIndex)) - .then(() => (previousDir = process.cwd())) - .then(() => logStack.push(lastLogger().createChild(currentFileName))) - .then(() => fn(() => (clean = false))) - .then( - () => logStack.pop(), - (err) => { - logStack.pop(); - throw err; - }, - ) - .then(() => console.log('----')) - .then(() => { - // If we're not in a setup, change the directory back to where it was before the test. - // This allows tests to chdir without worrying about keeping the original directory. - if (!allSetups.includes(relativeName) && previousDir) { - process.chdir(previousDir); - - // Restore env variables before each test. - console.log(' Restoring original environment variables...'); - process.env = originalEnvVariables; - } - }) - .then(() => { - // Only clean after a real test, not a setup step. Also skip cleaning if the test - // requested an exception. - if (!allSetups.includes(relativeName) && clean) { - logStack.push(new logging.NullLogger()); - return gitClean().then( - () => logStack.pop(), - (err) => { - logStack.pop(); - throw err; - }, - ); - } - }) - .then( - () => printFooter(currentFileName, start), - (err) => { - printFooter(currentFileName, start); - console.error(err); - throw err; - }, - ); - }); - }, Promise.resolve()) - // Output success vs failure information. - .then( - () => console.log(colors.green('Done.')), - (err) => { - console.log('\n'); - console.error(colors.red(`Test "${currentFileName}" failed...`)); - console.error(colors.red(err.message)); - console.error(colors.red(err.stack)); - - if (argv.debug) { - console.log(`Current Directory: ${process.cwd()}`); - console.log('Will loop forever while you debug... CTRL-C to quit.'); - - /* eslint-disable no-constant-condition */ - while (1) { - // That's right! - } - } - - return Promise.reject(err); - }, - ) - // Kill the registry processes before exiting. - .finally(() => { - registryProcess.kill(); - secureRegistryProcess.kill(); - }) - .then( - () => process.exit(0), - () => process.exit(1), - ) + throw err; + } finally { + registryProcess.kill(); + secureRegistryProcess.kill(); + } + }) + .then( + () => process.exit(0), + () => process.exit(1), ); -}); + +async function runTest(relativeName: string, testIndex: number) { + // Make sure this is a windows compatible path. + let absoluteName = path.join(e2eRoot, relativeName); + if (/^win/.test(process.platform)) { + absoluteName = absoluteName.replace(/\\/g, path.posix.sep); + } + + const currentFileName = relativeName.replace(/\.ts$/, ''); + const start = +new Date(); + + const module = require(absoluteName); + const originalEnvVariables = { + ...process.env, + }; + + const fn: (skipClean?: () => void) => Promise | void = + typeof module == 'function' + ? module + : typeof module.default == 'function' + ? module.default + : () => { + throw new Error('Invalid test module.'); + }; + + printHeader(currentFileName, testIndex); + + let clean = true; + let previousDir = process.cwd(); + try { + // Run the test function with the current file on the logStack. + logStack.push(lastLogger().createChild(currentFileName)); + try { + await fn(() => (clean = false)); + } finally { + logStack.pop(); + } + + console.log('----'); + + // If we're not in a setup, change the directory back to where it was before the test. + // This allows tests to chdir without worrying about keeping the original directory. + if (!allSetups.includes(relativeName) && previousDir) { + process.chdir(previousDir); + + // Restore env variables before each test. + console.log(' Restoring original environment variables...'); + process.env = originalEnvVariables; + } + + // Only clean after a real test, not a setup step. Also skip cleaning if the test + // requested an exception. + if (!allSetups.includes(relativeName) && clean) { + logStack.push(new logging.NullLogger()); + try { + await gitClean(); + } finally { + logStack.pop(); + } + } + + printFooter(currentFileName, start); + } catch (err) { + printFooter(currentFileName, start); + console.error(err); + throw err; + } +} function printHeader(testName: string, testIndex: number) { const text = `${testIndex + 1} of ${testsToRun.length}`; From a96b2c7298e74f76e43f1c7ba19c1790f7171bd7 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Fri, 27 May 2022 15:38:34 -0700 Subject: [PATCH 2/3] test: separate test vs test setup execution --- tests/legacy-cli/e2e_runner.ts | 129 +++++++++++++++++---------------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/tests/legacy-cli/e2e_runner.ts b/tests/legacy-cli/e2e_runner.ts index 8bfaf3d5c137..cabfcec56468 100644 --- a/tests/legacy-cli/e2e_runner.ts +++ b/tests/legacy-cli/e2e_runner.ts @@ -98,10 +98,9 @@ const tests = allTests.filter((name) => { }); // Remove tests that are not part of this shard. -const shardedTests = tests.filter((name, i) => shardId === null || i % nbShards == shardId); -const testsToRun = allSetups.concat(shardedTests); +const testsToRun = tests.filter((name, i) => shardId === null || i % nbShards == shardId); -if (shardedTests.length === 0) { +if (testsToRun.length === 0) { console.log(`No tests would be ran, aborting.`); process.exit(1); } @@ -114,28 +113,27 @@ console.log(testsToRun.join('\n')); if (testsToRun.length == allTests.length) { console.log(`Running ${testsToRun.length} tests`); } else { - console.log(`Running ${testsToRun.length} tests (${allTests.length + allSetups.length} total)`); + console.log(`Running ${testsToRun.length} tests (${allTests.length} total)`); } setGlobalVariable('argv', argv); setGlobalVariable('ci', process.env['CI']?.toLowerCase() === 'true' || process.env['CI'] === '1'); setGlobalVariable('package-manager', argv.yarn ? 'yarn' : 'npm'); +let lastTestRun: string | null = null; + Promise.all([findFreePort(), findFreePort()]) .then(async ([httpPort, httpsPort]) => { setGlobalVariable('package-registry', 'http://localhost:' + httpPort); setGlobalVariable('package-secure-registry', 'http://localhost:' + httpsPort); - let lastTestRun: string | null = null; - // NPM registries for the lifetime of the test execution const registryProcess = await createNpmRegistry(httpPort, httpPort); const secureRegistryProcess = await createNpmRegistry(httpPort, httpsPort, true); try { - for (const [testIndex, test] of testsToRun.entries()) { - await runTest((lastTestRun = test), testIndex); - } + await runSteps(runSetup, allSetups, 'setup'); + await runSteps(runTest, testsToRun, 'test'); console.log(colors.green('Done.')); } catch (err) { @@ -165,16 +163,43 @@ Promise.all([findFreePort(), findFreePort()]) () => process.exit(1), ); -async function runTest(relativeName: string, testIndex: number) { - // Make sure this is a windows compatible path. - let absoluteName = path.join(e2eRoot, relativeName); - if (/^win/.test(process.platform)) { - absoluteName = absoluteName.replace(/\\/g, path.posix.sep); +async function runSteps( + run: (name: string) => Promise | void, + steps: string[], + type: 'setup' | 'test', +) { + for (const [stepIndex, relativeName] of steps.entries()) { + // Make sure this is a windows compatible path. + let absoluteName = path.join(e2eRoot, relativeName).replace(/\.ts$/, ''); + if (/^win/.test(process.platform)) { + absoluteName = absoluteName.replace(/\\/g, path.posix.sep); + } + + const name = relativeName.replace(/\.ts$/, ''); + const start = Date.now(); + + printHeader(relativeName, stepIndex, steps.length, type); + + // Run the test function with the current file on the logStack. + logStack.push(lastLogger().createChild(absoluteName)); + try { + await run((lastTestRun = absoluteName)); + } finally { + logStack.pop(); + } + + console.log('----'); + printFooter(name, type, start); } +} + +async function runSetup(absoluteName: string) { + const module = require(absoluteName); - const currentFileName = relativeName.replace(/\.ts$/, ''); - const start = +new Date(); + await (typeof module === 'function' ? module : module.default)(); +} +async function runTest(absoluteName: string) { const module = require(absoluteName); const originalEnvVariables = { ...process.env, @@ -189,70 +214,50 @@ async function runTest(relativeName: string, testIndex: number) { throw new Error('Invalid test module.'); }; - printHeader(currentFileName, testIndex); - let clean = true; let previousDir = process.cwd(); - try { - // Run the test function with the current file on the logStack. - logStack.push(lastLogger().createChild(currentFileName)); - try { - await fn(() => (clean = false)); - } finally { - logStack.pop(); - } - console.log('----'); + await fn(() => (clean = false)); - // If we're not in a setup, change the directory back to where it was before the test. - // This allows tests to chdir without worrying about keeping the original directory. - if (!allSetups.includes(relativeName) && previousDir) { - process.chdir(previousDir); + // Change the directory back to where it was before the test. + // This allows tests to chdir without worrying about keeping the original directory. + if (previousDir) { + process.chdir(previousDir); - // Restore env variables before each test. - console.log(' Restoring original environment variables...'); - process.env = originalEnvVariables; - } + // Restore env variables before each test. + console.log('Restoring original environment variables...'); + process.env = originalEnvVariables; + } - // Only clean after a real test, not a setup step. Also skip cleaning if the test - // requested an exception. - if (!allSetups.includes(relativeName) && clean) { - logStack.push(new logging.NullLogger()); - try { - await gitClean(); - } finally { - logStack.pop(); - } + // Skip cleaning if the test requested an exception. + if (clean) { + logStack.push(new logging.NullLogger()); + try { + await gitClean(); + } finally { + logStack.pop(); } - - printFooter(currentFileName, start); - } catch (err) { - printFooter(currentFileName, start); - console.error(err); - throw err; } } -function printHeader(testName: string, testIndex: number) { - const text = `${testIndex + 1} of ${testsToRun.length}`; - const fullIndex = - (testIndex < allSetups.length - ? testIndex - : (testIndex - allSetups.length) * nbShards + shardId + allSetups.length) + 1; - const length = tests.length + allSetups.length; +function printHeader(testName: string, testIndex: number, count: number, type: 'setup' | 'test') { + const text = `${testIndex + 1} of ${count}`; + const fullIndex = testIndex * nbShards + shardId + 1; const shard = - shardId === null + shardId === null || type !== 'test' ? '' - : colors.yellow(` [${shardId}:${nbShards}]` + colors.bold(` (${fullIndex}/${length})`)); + : colors.yellow(` [${shardId}:${nbShards}]` + colors.bold(` (${fullIndex}/${tests.length})`)); console.log( - colors.green(`Running "${colors.bold.blue(testName)}" (${colors.bold.white(text)}${shard})...`), + colors.green( + `Running ${type} "${colors.bold.blue(testName)}" (${colors.bold.white(text)}${shard})...`, + ), ); } -function printFooter(testName: string, startTime: number) { +function printFooter(testName: string, type: 'setup' | 'test', startTime: number) { // Round to hundredth of a second. const t = Math.round((Date.now() - startTime) / 10) / 100; - console.log(colors.green('Last step took ') + colors.bold.blue('' + t) + colors.green('s...')); + console.log(colors.green(`Last ${type} took `) + colors.bold.blue('' + t) + colors.green('s...')); console.log(''); } From 3777a57f1db07ae844f2e96c4402fd8ba234e76c Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Sat, 28 May 2022 02:10:53 -0700 Subject: [PATCH 3/3] test: run tests in isolated subprocess --- tests/legacy-cli/BUILD.bazel | 1 + .../300-log-environment.ts | 0 .../500-create-project.ts | 13 +++- tests/legacy-cli/e2e/initialize/BUILD.bazel | 15 ++++ tests/legacy-cli/e2e/setup/002-npm-sandbox.ts | 20 ++--- tests/legacy-cli/e2e/setup/100-global-cli.ts | 20 +++-- tests/legacy-cli/e2e/tests/basic/e2e.ts | 7 -- .../e2e/tests/packages/webpack/test-app.ts | 4 +- tests/legacy-cli/e2e/utils/env.ts | 22 +++++- tests/legacy-cli/e2e/utils/process.ts | 46 ++++++++++-- tests/legacy-cli/e2e/utils/project.ts | 22 +++++- .../legacy-cli/e2e/utils/run_test_process.js | 3 + tests/legacy-cli/e2e/utils/test_process.ts | 19 +++++ tests/legacy-cli/e2e_runner.ts | 73 +++++++++---------- 14 files changed, 183 insertions(+), 82 deletions(-) rename tests/legacy-cli/e2e/{setup => initialize}/300-log-environment.ts (100%) rename tests/legacy-cli/e2e/{setup => initialize}/500-create-project.ts (67%) create mode 100644 tests/legacy-cli/e2e/initialize/BUILD.bazel create mode 100644 tests/legacy-cli/e2e/utils/run_test_process.js create mode 100644 tests/legacy-cli/e2e/utils/test_process.ts diff --git a/tests/legacy-cli/BUILD.bazel b/tests/legacy-cli/BUILD.bazel index a2c76fbbe307..cbd1ea4fc4ca 100644 --- a/tests/legacy-cli/BUILD.bazel +++ b/tests/legacy-cli/BUILD.bazel @@ -20,6 +20,7 @@ ts_library( # Loaded dynamically at runtime, not compiletime deps "//tests/legacy-cli/e2e/setup", + "//tests/legacy-cli/e2e/initialize", "//tests/legacy-cli/e2e/tests", ], ) diff --git a/tests/legacy-cli/e2e/setup/300-log-environment.ts b/tests/legacy-cli/e2e/initialize/300-log-environment.ts similarity index 100% rename from tests/legacy-cli/e2e/setup/300-log-environment.ts rename to tests/legacy-cli/e2e/initialize/300-log-environment.ts diff --git a/tests/legacy-cli/e2e/setup/500-create-project.ts b/tests/legacy-cli/e2e/initialize/500-create-project.ts similarity index 67% rename from tests/legacy-cli/e2e/setup/500-create-project.ts rename to tests/legacy-cli/e2e/initialize/500-create-project.ts index cfe623ce50f4..d53bae11acea 100644 --- a/tests/legacy-cli/e2e/setup/500-create-project.ts +++ b/tests/legacy-cli/e2e/initialize/500-create-project.ts @@ -1,13 +1,14 @@ import { join } from 'path'; +import yargsParser from 'yargs-parser'; import { getGlobalVariable } from '../utils/env'; import { expectFileToExist } from '../utils/fs'; import { gitClean } from '../utils/git'; -import { setRegistry as setNPMConfigRegistry } from '../utils/packages'; +import { installPackage, setRegistry as setNPMConfigRegistry } from '../utils/packages'; import { ng } from '../utils/process'; import { prepareProjectForE2e, updateJsonFile } from '../utils/project'; export default async function () { - const argv = getGlobalVariable('argv'); + const argv = getGlobalVariable('argv'); if (argv.noproject) { return; @@ -20,6 +21,14 @@ export default async function () { // Ensure local test registry is used when outside a project await setNPMConfigRegistry(true); + // Install puppeteer in the parent directory for use by the CLI within any test project. + // Align the version with the primary project package.json. + const puppeteerVersion = require('../../../../package.json').devDependencies.puppeteer.replace( + /^[\^~]/, + '', + ); + await installPackage(`puppeteer@${puppeteerVersion}`); + await ng('new', 'test-project', '--skip-install'); await expectFileToExist(join(process.cwd(), 'test-project')); process.chdir('./test-project'); diff --git a/tests/legacy-cli/e2e/initialize/BUILD.bazel b/tests/legacy-cli/e2e/initialize/BUILD.bazel new file mode 100644 index 000000000000..00735969e9ab --- /dev/null +++ b/tests/legacy-cli/e2e/initialize/BUILD.bazel @@ -0,0 +1,15 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "initialize", + testonly = True, + srcs = glob(["**/*.ts"]), + data = [ + "//:package.json", + ], + visibility = ["//visibility:public"], + deps = [ + "//tests/legacy-cli/e2e/utils", + "@npm//@types/yargs-parser", + ], +) diff --git a/tests/legacy-cli/e2e/setup/002-npm-sandbox.ts b/tests/legacy-cli/e2e/setup/002-npm-sandbox.ts index eaca4a166e3f..1d1824f25235 100644 --- a/tests/legacy-cli/e2e/setup/002-npm-sandbox.ts +++ b/tests/legacy-cli/e2e/setup/002-npm-sandbox.ts @@ -1,6 +1,6 @@ import { mkdir, writeFile } from 'fs/promises'; -import { delimiter, join } from 'path'; -import { getGlobalVariable } from '../utils/env'; +import { join } from 'path'; +import { getGlobalVariable, setGlobalVariable } from '../utils/env'; /** * Configure npm to use a unique sandboxed environment. @@ -8,23 +8,17 @@ import { getGlobalVariable } from '../utils/env'; export default async function () { const tempRoot: string = getGlobalVariable('tmp-root'); const npmModulesPrefix = join(tempRoot, 'npm-global'); + const npmRegistry: string = getGlobalVariable('package-registry'); const npmrc = join(tempRoot, '.npmrc'); // Configure npm to use the sandboxed npm globals and rc file + // From this point onward all npm transactions use the "global" npm cache + // isolated within this e2e test invocation. process.env.NPM_CONFIG_USERCONFIG = npmrc; process.env.NPM_CONFIG_PREFIX = npmModulesPrefix; - // Ensure the custom npm global bin is first on the PATH - // https://docs.npmjs.com/cli/v8/configuring-npm/folders#executables - if (process.platform.startsWith('win')) { - process.env.PATH = npmModulesPrefix + delimiter + process.env.PATH; - } else { - process.env.PATH = join(npmModulesPrefix, 'bin') + delimiter + process.env.PATH; - } - - // Ensure the globals directory and npmrc file exist. - // Configure the registry in the npmrc in addition to the environment variable. - await writeFile(npmrc, 'registry=' + getGlobalVariable('package-registry')); + // Configure the registry and prefix used within the test sandbox + await writeFile(npmrc, `registry=${npmRegistry}\nprefix=${npmModulesPrefix}`); await mkdir(npmModulesPrefix); console.log(` Using "${npmModulesPrefix}" as e2e test global npm cache.`); diff --git a/tests/legacy-cli/e2e/setup/100-global-cli.ts b/tests/legacy-cli/e2e/setup/100-global-cli.ts index 9f8f1e5f2c5b..3f751bea3d90 100644 --- a/tests/legacy-cli/e2e/setup/100-global-cli.ts +++ b/tests/legacy-cli/e2e/setup/100-global-cli.ts @@ -1,5 +1,8 @@ import { getGlobalVariable } from '../utils/env'; -import { exec, silentNpm } from '../utils/process'; +import { silentNpm } from '../utils/process'; + +const NPM_VERSION = '7.24.0'; +const YARN_VERSION = '1.22.18'; export default async function () { const argv = getGlobalVariable('argv'); @@ -9,10 +12,13 @@ export default async function () { const testRegistry: string = getGlobalVariable('package-registry'); - // Install global Angular CLI. - await silentNpm('install', '--global', '@angular/cli', `--registry=${testRegistry}`); - - try { - await exec(process.platform.startsWith('win') ? 'where' : 'which', 'ng'); - } catch {} + // Install global Angular CLI being tested, npm+yarn used by e2e tests. + await silentNpm( + 'install', + '--global', + `--registry=${testRegistry}`, + '@angular/cli', + `npm@${NPM_VERSION}`, + `yarn@${YARN_VERSION}`, + ); } diff --git a/tests/legacy-cli/e2e/tests/basic/e2e.ts b/tests/legacy-cli/e2e/tests/basic/e2e.ts index bcb710c2a754..8671d203efe8 100644 --- a/tests/legacy-cli/e2e/tests/basic/e2e.ts +++ b/tests/legacy-cli/e2e/tests/basic/e2e.ts @@ -59,13 +59,6 @@ export default function () { // Should run side-by-side with `ng serve` .then(() => execAndWaitForOutputToMatch('ng', ['serve'], / Compiled successfully./)) .then(() => ng('e2e', 'test-project', '--dev-server-target=')) - // Should fail without updated webdriver - .then(() => replaceInFile('e2e/protractor.conf.js', /chromeDriver: String.raw`[^`]*`,/, '')) - .then(() => - expectToFail(() => - ng('e2e', 'test-project', '--no-webdriver-update', '--dev-server-target='), - ), - ) .finally(() => killAllProcesses()) ); } diff --git a/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts b/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts index 91825375d048..37546944cb54 100644 --- a/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts +++ b/tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts @@ -3,7 +3,7 @@ import { createProjectFromAsset } from '../../../utils/assets'; import { expectFileSizeToBeUnder, expectFileToMatch, replaceInFile } from '../../../utils/fs'; import { execWithEnv } from '../../../utils/process'; -export default async function (skipCleaning: () => void) { +export default async function () { const webpackCLIBin = normalize('node_modules/.bin/webpack-cli'); await createProjectFromAsset('webpack/test-app'); @@ -30,6 +30,4 @@ export default async function (skipCleaning: () => void) { 'DISABLE_V8_COMPILE_CACHE': '1', }); await expectFileToMatch('dist/app.main.js', 'AppModule'); - - skipCleaning(); } diff --git a/tests/legacy-cli/e2e/utils/env.ts b/tests/legacy-cli/e2e/utils/env.ts index 6202e1bb597b..d2f0feece0a7 100644 --- a/tests/legacy-cli/e2e/utils/env.ts +++ b/tests/legacy-cli/e2e/utils/env.ts @@ -1,12 +1,26 @@ -const global: { [name: string]: any } = Object.create(null); +const ENV_PREFIX = 'LEGACY_CLI__'; export function setGlobalVariable(name: string, value: any) { - global[name] = value; + if (value === undefined) { + delete process.env[ENV_PREFIX + name]; + } else { + process.env[ENV_PREFIX + name] = JSON.stringify(value); + } } export function getGlobalVariable(name: string): T { - if (!(name in global)) { + const value = process.env[ENV_PREFIX + name]; + if (value === undefined) { throw new Error(`Trying to access variable "${name}" but it's not defined.`); } - return global[name] as T; + return JSON.parse(value) as T; +} + +export function getGlobalVariablesEnv(): NodeJS.ProcessEnv { + return Object.keys(process.env) + .filter((v) => v.startsWith(ENV_PREFIX)) + .reduce((vars, n) => { + vars[n] = process.env[n]; + return vars; + }, {}); } diff --git a/tests/legacy-cli/e2e/utils/process.ts b/tests/legacy-cli/e2e/utils/process.ts index 4960cee26bf6..c14f120fe133 100644 --- a/tests/legacy-cli/e2e/utils/process.ts +++ b/tests/legacy-cli/e2e/utils/process.ts @@ -3,9 +3,10 @@ import { SpawnOptions } from 'child_process'; import * as child_process from 'child_process'; import { concat, defer, EMPTY, from } from 'rxjs'; import { repeat, takeLast } from 'rxjs/operators'; -import { getGlobalVariable } from './env'; +import { getGlobalVariable, getGlobalVariablesEnv } from './env'; import { catchError } from 'rxjs/operators'; import treeKill from 'tree-kill'; +import { delimiter, join, resolve } from 'path'; interface ExecOptions { silent?: boolean; @@ -300,22 +301,21 @@ export function silentNpm( { silent: true, cwd: (options as { cwd?: string } | undefined)?.cwd, - env: extractNpmEnv(), }, 'npm', params, ); } else { - return _exec({ silent: true, env: extractNpmEnv() }, 'npm', args as string[]); + return _exec({ silent: true }, 'npm', args as string[]); } } export function silentYarn(...args: string[]) { - return _exec({ silent: true, env: extractNpmEnv() }, 'yarn', args); + return _exec({ silent: true }, 'yarn', args); } export function npm(...args: string[]) { - return _exec({ env: extractNpmEnv() }, 'npm', args); + return _exec({}, 'npm', args); } export function node(...args: string[]) { @@ -329,3 +329,39 @@ export function git(...args: string[]) { export function silentGit(...args: string[]) { return _exec({ silent: true }, 'git', args); } + +/** + * Launch the given entry in an child process isolated to the test environment. + * + * The test environment includes the local NPM registry, isolated NPM globals, + * the PATH variable only referencing the local node_modules and local NPM + * registry (not the test runner or standard global node_modules). + */ +export async function launchTestProcess(entry: string, ...args: any[]) { + const tempRoot: string = getGlobalVariable('tmp-root'); + + // Extract explicit environment variables for the test process. + const env: NodeJS.ProcessEnv = { + ...extractNpmEnv(), + ...getGlobalVariablesEnv(), + }; + + // Modify the PATH environment variable... + let paths = process.env.PATH!.split(delimiter); + + // Only include paths within the sandboxed test environment or external + // non angular-cli paths such as /usr/bin for generic commands. + paths = paths.filter((p) => p.startsWith(tempRoot) || !p.includes('angular-cli')); + + // Ensure the custom npm global bin is on the PATH + // https://docs.npmjs.com/cli/v8/configuring-npm/folders#executables + if (process.platform.startsWith('win')) { + paths.unshift(env.NPM_CONFIG_PREFIX!); + } else { + paths.unshift(join(env.NPM_CONFIG_PREFIX!, 'bin')); + } + + env.PATH = paths.join(delimiter); + + return _exec({ env }, process.execPath, [resolve(__dirname, 'run_test_process'), entry, ...args]); +} diff --git a/tests/legacy-cli/e2e/utils/project.ts b/tests/legacy-cli/e2e/utils/project.ts index 5843454e2254..f55194f3b8aa 100644 --- a/tests/legacy-cli/e2e/utils/project.ts +++ b/tests/legacy-cli/e2e/utils/project.ts @@ -7,7 +7,7 @@ import { getGlobalVariable } from './env'; import { prependToFile, readFile, replaceInFile, writeFile } from './fs'; import { gitCommit } from './git'; import { installWorkspacePackages } from './packages'; -import { execAndWaitForOutputToMatch, git, ng } from './process'; +import { exec, execAndWaitForOutputToMatch, git, ng } from './process'; export function updateJsonFile(filePath: string, fn: (json: any) => any | void) { return readFile(filePath).then((tsConfigJson) => { @@ -42,6 +42,26 @@ export async function prepareProjectForE2e(name: string) { await ng('generate', 'e2e', '--related-app-name', name); + // Initialize selenium webdriver. + // Often fails the first time so attempt twice if necessary. + const runWebdriverUpdate = () => + exec( + 'node', + 'node_modules/protractor/bin/webdriver-manager', + 'update', + '--standalone', + 'false', + '--gecko', + 'false', + '--versions.chrome', + '101.0.4951.41', + ); + try { + await runWebdriverUpdate(); + } catch (e) { + await runWebdriverUpdate(); + } + await useCIChrome('e2e'); await useCIChrome(''); diff --git a/tests/legacy-cli/e2e/utils/run_test_process.js b/tests/legacy-cli/e2e/utils/run_test_process.js new file mode 100644 index 000000000000..1a7fa92ccfc7 --- /dev/null +++ b/tests/legacy-cli/e2e/utils/run_test_process.js @@ -0,0 +1,3 @@ +'use strict'; +require('../../../../lib/bootstrap-local'); +require('./test_process'); diff --git a/tests/legacy-cli/e2e/utils/test_process.ts b/tests/legacy-cli/e2e/utils/test_process.ts new file mode 100644 index 000000000000..4f0a3a1ef5b5 --- /dev/null +++ b/tests/legacy-cli/e2e/utils/test_process.ts @@ -0,0 +1,19 @@ +import { killAllProcesses } from './process'; + +const testScript: string = process.argv[2]; +const testModule = require(testScript); +const testFunction: () => Promise | void = + typeof testModule == 'function' + ? testModule + : typeof testModule.default == 'function' + ? testModule.default + : () => { + throw new Error('Invalid test module.'); + }; + +(async () => Promise.resolve(testFunction()))() + .finally(killAllProcesses) + .catch((e) => { + console.error(e); + process.exitCode = -1; + }); diff --git a/tests/legacy-cli/e2e_runner.ts b/tests/legacy-cli/e2e_runner.ts index cabfcec56468..d97e62c21975 100644 --- a/tests/legacy-cli/e2e_runner.ts +++ b/tests/legacy-cli/e2e_runner.ts @@ -4,10 +4,12 @@ import * as colors from 'ansi-colors'; import glob from 'glob'; import yargsParser from 'yargs-parser'; import * as path from 'path'; -import { setGlobalVariable } from './e2e/utils/env'; +import { getGlobalVariable, setGlobalVariable } from './e2e/utils/env'; import { gitClean } from './e2e/utils/git'; import { createNpmRegistry } from './e2e/utils/registry'; import { AddressInfo, createServer, Server } from 'net'; +import { launchTestProcess } from './e2e/utils/process'; +import { join } from 'path'; Error.stackTraceLimit = Infinity; @@ -73,6 +75,7 @@ const testGlob = argv.glob || 'tests/**/*.ts'; const e2eRoot = path.join(__dirname, 'e2e'); const allSetups = glob.sync('setup/**/*.ts', { nodir: true, cwd: e2eRoot }).sort(); +const allInitializers = glob.sync('initialize/**/*.ts', { nodir: true, cwd: e2eRoot }).sort(); const allTests = glob .sync(testGlob, { nodir: true, cwd: e2eRoot, ignore: argv.ignore }) // Replace windows slashes. @@ -133,6 +136,7 @@ Promise.all([findFreePort(), findFreePort()]) try { await runSteps(runSetup, allSetups, 'setup'); + await runSteps(runInitializer, allInitializers, 'initializer'); await runSteps(runTest, testsToRun, 'test'); console.log(colors.green('Done.')); @@ -166,7 +170,7 @@ Promise.all([findFreePort(), findFreePort()]) async function runSteps( run: (name: string) => Promise | void, steps: string[], - type: 'setup' | 'test', + type: 'setup' | 'test' | 'initializer', ) { for (const [stepIndex, relativeName] of steps.entries()) { // Make sure this is a windows compatible path. @@ -199,48 +203,37 @@ async function runSetup(absoluteName: string) { await (typeof module === 'function' ? module : module.default)(); } +/** + * Run a file from the projects root directory in a subprocess via launchTestProcess(). + */ +async function runInitializer(absoluteName: string) { + process.chdir(getGlobalVariable('projects-root')); + + await launchTestProcess(absoluteName); +} + +/** + * Run a file from the main 'test-project' directory in a subprocess via launchTestProcess(). + */ async function runTest(absoluteName: string) { - const module = require(absoluteName); - const originalEnvVariables = { - ...process.env, - }; - - const fn: (skipClean?: () => void) => Promise | void = - typeof module == 'function' - ? module - : typeof module.default == 'function' - ? module.default - : () => { - throw new Error('Invalid test module.'); - }; - - let clean = true; - let previousDir = process.cwd(); - - await fn(() => (clean = false)); - - // Change the directory back to where it was before the test. - // This allows tests to chdir without worrying about keeping the original directory. - if (previousDir) { - process.chdir(previousDir); - - // Restore env variables before each test. - console.log('Restoring original environment variables...'); - process.env = originalEnvVariables; - } + process.chdir(join(getGlobalVariable('projects-root'), 'test-project')); - // Skip cleaning if the test requested an exception. - if (clean) { - logStack.push(new logging.NullLogger()); - try { - await gitClean(); - } finally { - logStack.pop(); - } + await launchTestProcess(absoluteName); + + logStack.push(new logging.NullLogger()); + try { + await gitClean(); + } finally { + logStack.pop(); } } -function printHeader(testName: string, testIndex: number, count: number, type: 'setup' | 'test') { +function printHeader( + testName: string, + testIndex: number, + count: number, + type: 'setup' | 'initializer' | 'test', +) { const text = `${testIndex + 1} of ${count}`; const fullIndex = testIndex * nbShards + shardId + 1; const shard = @@ -254,7 +247,7 @@ function printHeader(testName: string, testIndex: number, count: number, type: ' ); } -function printFooter(testName: string, type: 'setup' | 'test', startTime: number) { +function printFooter(testName: string, type: 'setup' | 'initializer' | 'test', startTime: number) { // Round to hundredth of a second. const t = Math.round((Date.now() - startTime) / 10) / 100; console.log(colors.green(`Last ${type} took `) + colors.bold.blue('' + t) + colors.green('s...'));