Skip to content

Commit 4326d07

Browse files
committed
test: run tests in isolated subprocess
1 parent a96b2c7 commit 4326d07

14 files changed

+183
-82
lines changed

tests/legacy-cli/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ts_library(
2020

2121
# Loaded dynamically at runtime, not compiletime deps
2222
"//tests/legacy-cli/e2e/setup",
23+
"//tests/legacy-cli/e2e/initialize",
2324
"//tests/legacy-cli/e2e/tests",
2425
],
2526
)

tests/legacy-cli/e2e/setup/500-create-project.ts renamed to tests/legacy-cli/e2e/initialize/500-create-project.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { join } from 'path';
2+
import yargsParser from 'yargs-parser';
23
import { getGlobalVariable } from '../utils/env';
34
import { expectFileToExist } from '../utils/fs';
45
import { gitClean } from '../utils/git';
5-
import { setRegistry as setNPMConfigRegistry } from '../utils/packages';
6+
import { installPackage, setRegistry as setNPMConfigRegistry } from '../utils/packages';
67
import { ng } from '../utils/process';
78
import { prepareProjectForE2e, updateJsonFile } from '../utils/project';
89

910
export default async function () {
10-
const argv = getGlobalVariable('argv');
11+
const argv = getGlobalVariable<yargsParser.Arguments>('argv');
1112

1213
if (argv.noproject) {
1314
return;
@@ -20,6 +21,14 @@ export default async function () {
2021
// Ensure local test registry is used when outside a project
2122
await setNPMConfigRegistry(true);
2223

24+
// Install puppeteer in the parent directory for use by the CLI within any test project.
25+
// Align the version with the primary project package.json.
26+
const puppeteerVersion = require('../../../../package.json').devDependencies.puppeteer.replace(
27+
/^[\^~]/,
28+
'',
29+
);
30+
await installPackage(`puppeteer@${puppeteerVersion}`);
31+
2332
await ng('new', 'test-project', '--skip-install');
2433
await expectFileToExist(join(process.cwd(), 'test-project'));
2534
process.chdir('./test-project');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "initialize",
5+
testonly = True,
6+
srcs = glob(["**/*.ts"]),
7+
data = [
8+
"//:package.json",
9+
],
10+
visibility = ["//visibility:public"],
11+
deps = [
12+
"//tests/legacy-cli/e2e/utils",
13+
"@npm//@types/yargs-parser",
14+
],
15+
)

tests/legacy-cli/e2e/setup/002-npm-sandbox.ts

+7-13
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
11
import { mkdir, writeFile } from 'fs/promises';
2-
import { delimiter, join } from 'path';
3-
import { getGlobalVariable } from '../utils/env';
2+
import { join } from 'path';
3+
import { getGlobalVariable, setGlobalVariable } from '../utils/env';
44

55
/**
66
* Configure npm to use a unique sandboxed environment.
77
*/
88
export default async function () {
99
const tempRoot: string = getGlobalVariable('tmp-root');
1010
const npmModulesPrefix = join(tempRoot, 'npm-global');
11+
const npmRegistry: string = getGlobalVariable('package-registry');
1112
const npmrc = join(tempRoot, '.npmrc');
1213

1314
// Configure npm to use the sandboxed npm globals and rc file
15+
// From this point onward all npm transactions use the "global" npm cache
16+
// isolated within this e2e test invocation.
1417
process.env.NPM_CONFIG_USERCONFIG = npmrc;
1518
process.env.NPM_CONFIG_PREFIX = npmModulesPrefix;
1619

17-
// Ensure the custom npm global bin is first on the PATH
18-
// https://docs.npmjs.com/cli/v8/configuring-npm/folders#executables
19-
if (process.platform.startsWith('win')) {
20-
process.env.PATH = npmModulesPrefix + delimiter + process.env.PATH;
21-
} else {
22-
process.env.PATH = join(npmModulesPrefix, 'bin') + delimiter + process.env.PATH;
23-
}
24-
25-
// Ensure the globals directory and npmrc file exist.
26-
// Configure the registry in the npmrc in addition to the environment variable.
27-
await writeFile(npmrc, 'registry=' + getGlobalVariable('package-registry'));
20+
// Configure the registry and prefix used within the test sandbox
21+
await writeFile(npmrc, `registry=${npmRegistry}\nprefix=${npmModulesPrefix}`);
2822
await mkdir(npmModulesPrefix);
2923

3024
console.log(` Using "${npmModulesPrefix}" as e2e test global npm cache.`);
+13-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { getGlobalVariable } from '../utils/env';
2-
import { exec, silentNpm } from '../utils/process';
2+
import { silentNpm } from '../utils/process';
3+
4+
const NPM_VERSION = '7.24.0';
5+
const YARN_VERSION = '1.22.18';
36

47
export default async function () {
58
const argv = getGlobalVariable('argv');
@@ -9,10 +12,13 @@ export default async function () {
912

1013
const testRegistry: string = getGlobalVariable('package-registry');
1114

12-
// Install global Angular CLI.
13-
await silentNpm('install', '--global', '@angular/cli', `--registry=${testRegistry}`);
14-
15-
try {
16-
await exec(process.platform.startsWith('win') ? 'where' : 'which', 'ng');
17-
} catch {}
15+
// Install global Angular CLI being tested, npm+yarn used by e2e tests.
16+
await silentNpm(
17+
'install',
18+
'--global',
19+
`--registry=${testRegistry}`,
20+
'@angular/cli',
21+
`npm@${NPM_VERSION}`,
22+
`yarn@${YARN_VERSION}`,
23+
);
1824
}

tests/legacy-cli/e2e/tests/basic/e2e.ts

-7
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,6 @@ export default function () {
5959
// Should run side-by-side with `ng serve`
6060
.then(() => execAndWaitForOutputToMatch('ng', ['serve'], / Compiled successfully./))
6161
.then(() => ng('e2e', 'test-project', '--dev-server-target='))
62-
// Should fail without updated webdriver
63-
.then(() => replaceInFile('e2e/protractor.conf.js', /chromeDriver: String.raw`[^`]*`,/, ''))
64-
.then(() =>
65-
expectToFail(() =>
66-
ng('e2e', 'test-project', '--no-webdriver-update', '--dev-server-target='),
67-
),
68-
)
6962
.finally(() => killAllProcesses())
7063
);
7164
}

tests/legacy-cli/e2e/tests/packages/webpack/test-app.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createProjectFromAsset } from '../../../utils/assets';
33
import { expectFileSizeToBeUnder, expectFileToMatch, replaceInFile } from '../../../utils/fs';
44
import { execWithEnv } from '../../../utils/process';
55

6-
export default async function (skipCleaning: () => void) {
6+
export default async function () {
77
const webpackCLIBin = normalize('node_modules/.bin/webpack-cli');
88

99
await createProjectFromAsset('webpack/test-app');
@@ -30,6 +30,4 @@ export default async function (skipCleaning: () => void) {
3030
'DISABLE_V8_COMPILE_CACHE': '1',
3131
});
3232
await expectFileToMatch('dist/app.main.js', 'AppModule');
33-
34-
skipCleaning();
3533
}

tests/legacy-cli/e2e/utils/env.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
1-
const global: { [name: string]: any } = Object.create(null);
1+
const ENV_PREFIX = 'LEGACY_CLI__';
22

33
export function setGlobalVariable(name: string, value: any) {
4-
global[name] = value;
4+
if (value === undefined) {
5+
delete process.env[ENV_PREFIX + name];
6+
} else {
7+
process.env[ENV_PREFIX + name] = JSON.stringify(value);
8+
}
59
}
610

711
export function getGlobalVariable<T = any>(name: string): T {
8-
if (!(name in global)) {
12+
const value = process.env[ENV_PREFIX + name];
13+
if (value === undefined) {
914
throw new Error(`Trying to access variable "${name}" but it's not defined.`);
1015
}
11-
return global[name] as T;
16+
return JSON.parse(value) as T;
17+
}
18+
19+
export function getGlobalVariablesEnv(): NodeJS.ProcessEnv {
20+
return Object.keys(process.env)
21+
.filter((v) => v.startsWith(ENV_PREFIX))
22+
.reduce<NodeJS.ProcessEnv>((vars, n) => {
23+
vars[n] = process.env[n];
24+
return vars;
25+
}, {});
1226
}

tests/legacy-cli/e2e/utils/process.ts

+41-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { SpawnOptions } from 'child_process';
33
import * as child_process from 'child_process';
44
import { concat, defer, EMPTY, from } from 'rxjs';
55
import { repeat, takeLast } from 'rxjs/operators';
6-
import { getGlobalVariable } from './env';
6+
import { getGlobalVariable, getGlobalVariablesEnv } from './env';
77
import { catchError } from 'rxjs/operators';
88
import treeKill from 'tree-kill';
9+
import { delimiter, join, resolve } from 'path';
910

1011
interface ExecOptions {
1112
silent?: boolean;
@@ -300,22 +301,21 @@ export function silentNpm(
300301
{
301302
silent: true,
302303
cwd: (options as { cwd?: string } | undefined)?.cwd,
303-
env: extractNpmEnv(),
304304
},
305305
'npm',
306306
params,
307307
);
308308
} else {
309-
return _exec({ silent: true, env: extractNpmEnv() }, 'npm', args as string[]);
309+
return _exec({ silent: true }, 'npm', args as string[]);
310310
}
311311
}
312312

313313
export function silentYarn(...args: string[]) {
314-
return _exec({ silent: true, env: extractNpmEnv() }, 'yarn', args);
314+
return _exec({ silent: true }, 'yarn', args);
315315
}
316316

317317
export function npm(...args: string[]) {
318-
return _exec({ env: extractNpmEnv() }, 'npm', args);
318+
return _exec({}, 'npm', args);
319319
}
320320

321321
export function node(...args: string[]) {
@@ -329,3 +329,39 @@ export function git(...args: string[]) {
329329
export function silentGit(...args: string[]) {
330330
return _exec({ silent: true }, 'git', args);
331331
}
332+
333+
/**
334+
* Launch the given entry in an child process isolated to the test environment.
335+
*
336+
* The test environment includes the local NPM registry, isolated NPM globals,
337+
* the PATH variable only referencing the local node_modules and local NPM
338+
* registry (not the test runner or standard global node_modules).
339+
*/
340+
export async function launchTestProcess(entry: string, ...args: any[]) {
341+
const tempRoot: string = getGlobalVariable('tmp-root');
342+
343+
// Extract explicit environment variables for the test process.
344+
const env: NodeJS.ProcessEnv = {
345+
...extractNpmEnv(),
346+
...getGlobalVariablesEnv(),
347+
};
348+
349+
// Modify the PATH environment variable...
350+
let paths = process.env.PATH!.split(delimiter);
351+
352+
// Only include paths within the sandboxed test environment or external
353+
// non angular-cli paths such as /usr/bin for generic commands.
354+
paths = paths.filter((p) => p.startsWith(tempRoot) || !p.includes('angular-cli'));
355+
356+
// Ensure the custom npm global bin is on the PATH
357+
// https://docs.npmjs.com/cli/v8/configuring-npm/folders#executables
358+
if (process.platform.startsWith('win')) {
359+
paths.unshift(env.NPM_CONFIG_PREFIX!);
360+
} else {
361+
paths.unshift(join(env.NPM_CONFIG_PREFIX!, 'bin'));
362+
}
363+
364+
env.PATH = paths.join(delimiter);
365+
366+
return _exec({ env }, process.execPath, [resolve(__dirname, 'run_test_process'), entry, ...args]);
367+
}

tests/legacy-cli/e2e/utils/project.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { getGlobalVariable } from './env';
77
import { prependToFile, readFile, replaceInFile, writeFile } from './fs';
88
import { gitCommit } from './git';
99
import { installWorkspacePackages } from './packages';
10-
import { execAndWaitForOutputToMatch, git, ng } from './process';
10+
import { exec, execAndWaitForOutputToMatch, git, ng } from './process';
1111

1212
export function updateJsonFile(filePath: string, fn: (json: any) => any | void) {
1313
return readFile(filePath).then((tsConfigJson) => {
@@ -42,6 +42,26 @@ export async function prepareProjectForE2e(name: string) {
4242

4343
await ng('generate', 'e2e', '--related-app-name', name);
4444

45+
// Initialize selenium webdriver.
46+
// Often fails the first time so attempt twice if necessary.
47+
const runWebdriverUpdate = () =>
48+
exec(
49+
'node',
50+
'node_modules/protractor/bin/webdriver-manager',
51+
'update',
52+
'--standalone',
53+
'false',
54+
'--gecko',
55+
'false',
56+
'--versions.chrome',
57+
'101.0.4951.41',
58+
);
59+
try {
60+
await runWebdriverUpdate();
61+
} catch (e) {
62+
await runWebdriverUpdate();
63+
}
64+
4565
await useCIChrome('e2e');
4666
await useCIChrome('');
4767

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
require('../../../../lib/bootstrap-local');
3+
require('./test_process');
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { killAllProcesses } from './process';
2+
3+
const testScript: string = process.argv[2];
4+
const testModule = require(testScript);
5+
const testFunction: () => Promise<void> | void =
6+
typeof testModule == 'function'
7+
? testModule
8+
: typeof testModule.default == 'function'
9+
? testModule.default
10+
: () => {
11+
throw new Error('Invalid test module.');
12+
};
13+
14+
(async () => Promise.resolve(testFunction()))()
15+
.finally(killAllProcesses)
16+
.catch((e) => {
17+
console.error(e);
18+
process.exitCode = -1;
19+
});

0 commit comments

Comments
 (0)