Skip to content

Commit 14f233b

Browse files
committed
test: run tests in isolated subprocess
1 parent 01e1b9c commit 14f233b

12 files changed

+205
-79
lines changed

tests/legacy-cli/e2e/setup/001-create-tmp-dir.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { mkdtempSync, realpathSync } from 'fs';
1+
import { mkdirSync, mkdtempSync, realpathSync } from 'fs';
22
import { tmpdir } from 'os';
33
import { dirname, join } from 'path';
44
import { getGlobalVariable, setGlobalVariable } from '../utils/env';
55

6+
const NPM_ROOT_PREFIX = 'npm-global';
7+
68
export default function () {
79
const argv = getGlobalVariable('argv');
810

@@ -14,7 +16,9 @@ export default function () {
1416
tempRoot = argv.tmpdir;
1517
} else {
1618
tempRoot = mkdtempSync(join(realpathSync(tmpdir()), 'angular-cli-e2e-'));
19+
mkdirSync(join(tempRoot, NPM_ROOT_PREFIX));
1720
}
1821
console.log(` Using "${tempRoot}" as temporary directory for a new project.`);
1922
setGlobalVariable('tmp-root', tempRoot);
23+
setGlobalVariable('npm-root', join(tempRoot, NPM_ROOT_PREFIX));
2024
}
+5-11
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
11
import { mkdir, writeFile } from 'fs/promises';
2-
import { delimiter, join } from 'path';
2+
import { join } from 'path';
33
import { getGlobalVariable } 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');
10-
const npmModulesPrefix = join(tempRoot, 'npm-global');
10+
const npmModulesPrefix: string = getGlobalVariable('npm-root');
1111
const npmrc = join(tempRoot, '.npmrc');
1212

13+
// From this point onward all npm transactions use the "global" npm cache
14+
// isolated within this e2e test invocation.
15+
1316
// Configure npm to use the sandboxed npm globals and rc file
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-
2520
// Ensure the globals directory and npmrc file exist.
2621
// Configure the registry in the npmrc in addition to the environment variable.
2722
await writeFile(npmrc, 'registry=' + getGlobalVariable('package-registry'));
28-
await mkdir(npmModulesPrefix);
2923

3024
console.log(` Using "${npmModulesPrefix}" as e2e test global npm cache.`);
3125
}
+12-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { getGlobalVariable } from '../utils/env';
22
import { exec, silentNpm } from '../utils/process';
33

4+
const NPM_VERSION = '7.5.6';
5+
const YARN_VERSION = '1.22.18';
6+
47
export default async function () {
58
const argv = getGlobalVariable('argv');
69
if (argv.noglobal) {
@@ -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

+82-14
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+
export 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,26 +307,25 @@ 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[]) {
307-
return _exec({}, 'node', args);
328+
return _exec({}, process.execPath, args);
308329
}
309330

310331
export function git(...args: string[]) {
@@ -314,3 +335,50 @@ 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 = 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+
// Add the project node modules bin to the front of the path
372+
paths.push(join(process.cwd(), 'node_modules', '.bin'));
373+
374+
env.PATH = paths.join(delimiter);
375+
376+
return _exec(
377+
{
378+
env,
379+
cwd: process.cwd(),
380+
},
381+
process.execPath,
382+
[resolve(__dirname, 'run_test_process'), entry, ...args],
383+
);
384+
}

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

+19-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,24 @@ export async function prepareProjectForE2e(name) {
4242

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

45+
// Initialize protractor webdriver. Often fails the first time so attempt twice if necessary.
46+
// const webdriverCommand = exec.bind(
47+
// null,
48+
// 'webdriver-manager',
49+
// 'update',
50+
// '--standalone',
51+
// 'false',
52+
// '--gecko',
53+
// 'false',
54+
// '--versions.chrome',
55+
// '101.0.4951.41',
56+
// );
57+
// try {
58+
// await webdriverCommand();
59+
// } catch (e) {
60+
// await webdriverCommand();
61+
// }
62+
4563
await useCIChrome('e2e');
4664
await useCIChrome('');
4765

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
require('../../../../lib/bootstrap-local');
3+
require('./test_process');
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const testScript: string = process.argv[2];
2+
const args = process.argv.slice(2).map((a) => (a === 'true' || a === 'false' ? a === 'true' : a));
3+
4+
const testModule = require(testScript);
5+
const fn: (...args: any[]) => 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(fn(...args)))().then(
15+
() => process.exit(0),
16+
(e) => {
17+
console.error(e);
18+
process.exit(-1);
19+
},
20+
);

0 commit comments

Comments
 (0)