Skip to content

Commit 89cb1d1

Browse files
committed
test: run tests in isolated subprocess
1 parent 432f722 commit 89cb1d1

File tree

12 files changed

+190
-88
lines changed

12 files changed

+190
-88
lines changed

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { join } from 'path';
22
import { getGlobalVariable } from '../utils/env';
33
import { expectFileToExist } from '../utils/fs';
44
import { gitClean } from '../utils/git';
5-
import { setRegistry as setNPMConfigRegistry } from '../utils/packages';
5+
import { installPackage, setRegistry as setNPMConfigRegistry } from '../utils/packages';
66
import { ng } from '../utils/process';
77
import { prepareProjectForE2e, updateJsonFile } from '../utils/project';
88

@@ -22,6 +22,10 @@ export default async function () {
2222
// Ensure local test registry is used when outside a project
2323
await setNPMConfigRegistry(true);
2424

25+
// Install puppeteer in the parent directory of the test projects for use
26+
// by the CLI within any test project
27+
await installPackage('[email protected]');
28+
2529
await ng('new', 'test-project', '--skip-install', ...extraArgs);
2630
await expectFileToExist(join(process.cwd(), 'test-project'));
2731
process.chdir('./test-project');
+8-13
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
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

24+
setGlobalVariable('npm-root', npmModulesPrefix);
3025
console.log(` Using "${npmModulesPrefix}" as e2e test global npm cache.`);
3126
}
+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.5.6';
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 = 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
.then(
7063
() => killAllProcesses(),
7164
(err) => {

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

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

33
export function setGlobalVariable(name: string, value: any) {
4-
global[name] = value;
4+
process.env[ENV_PREFIX + name] = JSON.stringify(value);
55
}
66

77
export function getGlobalVariable(name: string): any {
8-
if (!(name in global)) {
8+
const envName = ENV_PREFIX + name;
9+
if (!(envName in process.env)) {
910
throw new Error(`Trying to access variable "${name}" but it's not defined.`);
1011
}
11-
return global[name];
12+
return JSON.parse(process.env[envName]);
13+
}
14+
15+
export function getGlobalVariablesEnv(): { [k: string]: string } {
16+
return Object.keys(process.env)
17+
.filter((v) => v.startsWith(ENV_PREFIX))
18+
.reduce((vars, n) => {
19+
vars[n] = process.env[n];
20+
return vars;
21+
}, {});
1222
}

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

+71-13
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ 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';
8+
import { delimiter, join, resolve } from 'path';
89
const treeKill = require('tree-kill');
910

1011
interface ExecOptions {
1112
silent?: boolean;
13+
announce?: boolean;
1214
waitForMatch?: RegExp;
1315
env?: { [varname: string]: string };
1416
stdin?: string;
@@ -24,18 +26,22 @@ export type ProcessOutput = {
2426
stderr: string;
2527
};
2628

27-
function _exec(options: ExecOptions, cmd: string, args: string[]): Promise<ProcessOutput> {
29+
async function _exec(options: ExecOptions, cmd: string, args: string[]): Promise<ProcessOutput> {
2830
// Create a separate instance to prevent unintended global changes to the color configuration
2931
// Create function is not defined in the typings. See: https://github.com/doowb/ansi-colors/pull/44
3032
const colors = (ansiColors as typeof ansiColors & { create: () => typeof ansiColors }).create();
3133

34+
const announce = options.announce ?? true;
35+
3236
let stdout = '';
3337
let stderr = '';
3438
const cwd = options.cwd ?? process.cwd();
3539
const env = options.env;
36-
console.log(
37-
`==========================================================================================`,
38-
);
40+
if (announce) {
41+
console.log(
42+
`==========================================================================================`,
43+
);
44+
}
3945

4046
args = args.filter((x) => x !== undefined);
4147
const flags = [
@@ -46,9 +52,15 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise<Proce
4652
.join(', ')
4753
.replace(/^(.+)$/, ' [$1]'); // Proper formatting.
4854

49-
console.log(colors.blue(`Running \`${cmd} ${args.map((x) => `"${x}"`).join(' ')}\`${flags}...`));
50-
console.log(colors.blue(`CWD: ${cwd}`));
51-
console.log(colors.blue(`ENV: ${JSON.stringify(env)}`));
55+
if (announce) {
56+
console.log(
57+
colors.blue(`Running \`${cmd} ${args.map((x) => `"${x}"`).join(' ')}\`${flags}...`),
58+
);
59+
console.log(colors.blue(`CWD: ${cwd}`));
60+
61+
console.log(colors.blue(`ENV: ${JSON.stringify(env)}`));
62+
}
63+
5264
const spawnOptions: SpawnOptions = {
5365
cwd,
5466
...(env ? { env } : {}),
@@ -140,7 +152,7 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise<Proce
140152
});
141153
}
142154

143-
export function extractNpmEnv() {
155+
export function extractNpmEnv(): { [k: string]: string } {
144156
return Object.keys(process.env)
145157
.filter((v) => NPM_CONFIG_RE.test(v))
146158
.reduce(
@@ -154,6 +166,16 @@ export function extractNpmEnv() {
154166
);
155167
}
156168

169+
function getNpmSandboxEnv(): { [k: string]: string } {
170+
const tempRoot: string = getGlobalVariable('tmp-root');
171+
const npmModulesPrefix: string = getGlobalVariable('npm-root');
172+
173+
return {
174+
NPM_CONFIG_USERCONFIG: join(tempRoot, '.npmrc'),
175+
NPM_CONFIG_PREFIX: npmModulesPrefix,
176+
};
177+
}
178+
157179
export function waitForAnyProcessOutputToMatch(
158180
match: RegExp,
159181
timeout = 30000,
@@ -299,22 +321,21 @@ export function silentNpm(
299321
{
300322
silent: true,
301323
cwd: (options as { cwd?: string } | undefined)?.cwd,
302-
env: extractNpmEnv(),
303324
},
304325
'npm',
305326
params,
306327
);
307328
} else {
308-
return _exec({ silent: true, env: extractNpmEnv() }, 'npm', args as string[]);
329+
return _exec({ silent: true }, 'npm', args as string[]);
309330
}
310331
}
311332

312333
export function silentYarn(...args: string[]) {
313-
return _exec({ silent: true, env: extractNpmEnv() }, 'yarn', args);
334+
return _exec({ silent: true }, 'yarn', args);
314335
}
315336

316337
export function npm(...args: string[]) {
317-
return _exec({ env: extractNpmEnv() }, 'npm', args);
338+
return _exec({}, 'npm', args);
318339
}
319340

320341
export function node(...args: string[]) {
@@ -328,3 +349,40 @@ export function git(...args: string[]) {
328349
export function silentGit(...args: string[]) {
329350
return _exec({ silent: true }, 'git', args);
330351
}
352+
353+
/**
354+
* Launch the given entry in an child process isolated to the test environment.
355+
*
356+
* The test environment includes the local NPM registry, isolated NPM globals,
357+
* the PATH variable only referencing the local node_modules and local NPM
358+
* registry (not the test runner or standard global node_modules).
359+
*/
360+
export async function launchTestProcess(entry: string, ...args: any[]) {
361+
const tempRoot: string = getGlobalVariable('tmp-root');
362+
363+
// Extract explicit environment variables for the test process.
364+
const env = {
365+
...extractNpmEnv(),
366+
...getGlobalVariablesEnv(),
367+
...getNpmSandboxEnv(),
368+
};
369+
370+
// Modify the PATH environment variable...
371+
let paths = process.env.PATH.split(delimiter);
372+
373+
// Only include paths within the sandboxed test environment or external
374+
// non angular-cli paths such as /usr/bin for generic commands.
375+
paths = paths.filter((p) => p.startsWith(tempRoot) || !p.includes('angular-cli'));
376+
377+
// Ensure the custom npm global bin is on the PATH
378+
// https://docs.npmjs.com/cli/v8/configuring-npm/folders#executables
379+
if (process.platform.startsWith('win')) {
380+
paths.unshift(env.NPM_CONFIG_PREFIX);
381+
} else {
382+
paths.unshift(join(env.NPM_CONFIG_PREFIX, 'bin'));
383+
}
384+
385+
env.PATH = paths.join(delimiter);
386+
387+
return _exec({ env }, process.execPath, [resolve(__dirname, 'run_test_process'), entry, ...args]);
388+
}

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

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

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

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

45+
// Initialize welenium webdrivers.
46+
// Often fails the first time so attempt twice if necessary.
47+
const webdriverCommand = exec.bind(
48+
null,
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 webdriverCommand();
61+
} catch (e) {
62+
await webdriverCommand();
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');
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
.then(
17+
() => process.exit(0),
18+
(e) => {
19+
console.error(e);
20+
process.exit(-1);
21+
},
22+
);

0 commit comments

Comments
 (0)