Skip to content

Commit 4558799

Browse files
committed
test: run tests in isolated subprocess
1 parent ef888bd commit 4558799

File tree

11 files changed

+202
-83
lines changed

11 files changed

+202
-83
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/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,
@@ -285,22 +307,21 @@ export function silentNpm(
285307
{
286308
silent: true,
287309
cwd: (options as { cwd?: string } | undefined)?.cwd,
288-
env: extractNpmEnv(),
289310
},
290311
'npm',
291312
params,
292313
);
293314
} else {
294-
return _exec({ silent: true, env: extractNpmEnv() }, 'npm', args as string[]);
315+
return _exec({ silent: true }, 'npm', args as string[]);
295316
}
296317
}
297318

298319
export function silentYarn(...args: string[]) {
299-
return _exec({ silent: true, env: extractNpmEnv() }, 'yarn', args);
320+
return _exec({ silent: true }, 'yarn', args);
300321
}
301322

302323
export function npm(...args: string[]) {
303-
return _exec({ env: extractNpmEnv() }, 'npm', args);
324+
return _exec({}, 'npm', args);
304325
}
305326

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

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');
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const testScript: string = process.argv[2];
2+
const testModule = require(testScript);
3+
const testFunction: () => Promise<void> | void =
4+
typeof testModule == 'function'
5+
? testModule
6+
: typeof testModule.default == 'function'
7+
? testModule.default
8+
: () => {
9+
throw new Error('Invalid test module.');
10+
};
11+
12+
(async () => Promise.resolve(testFunction()))().then(
13+
() => process.exit(0),
14+
(e) => {
15+
console.error(e);
16+
process.exit(-1);
17+
},
18+
);

0 commit comments

Comments
 (0)