Skip to content

Commit 317f948

Browse files
committed
test: run tests in isolated subprocess
1 parent 67d54aa commit 317f948

File tree

12 files changed

+164
-80
lines changed

12 files changed

+164
-80
lines changed

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

+9-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,14 @@ 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 for use by the CLI within any test project.
26+
// Align the version with the primary project package.json.
27+
const puppeteerVersion = require('../../../../package.json').devDependencies.puppeteer.replace(
28+
/^[\^~]/,
29+
'',
30+
);
31+
await installPackage(`puppeteer@${puppeteerVersion}`);
32+
2533
await ng('new', 'test-project', '--skip-install', ...extraArgs);
2634
await expectFileToExist(join(process.cwd(), 'test-project'));
2735
process.chdir('./test-project');

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 = 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(name: string): any {
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];
16+
return JSON.parse(value);
17+
}
18+
19+
export function getGlobalVariablesEnv(): NodeJS.ProcessEnv {
20+
return Object.keys(process.env)
21+
.filter((v) => v.startsWith(ENV_PREFIX))
22+
.reduce((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;
@@ -299,22 +300,21 @@ export function silentNpm(
299300
{
300301
silent: true,
301302
cwd: (options as { cwd?: string } | undefined)?.cwd,
302-
env: extractNpmEnv(),
303303
},
304304
'npm',
305305
params,
306306
);
307307
} else {
308-
return _exec({ silent: true, env: extractNpmEnv() }, 'npm', args as string[]);
308+
return _exec({ silent: true }, 'npm', args as string[]);
309309
}
310310
}
311311

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

316316
export function npm(...args: string[]) {
317-
return _exec({ env: extractNpmEnv() }, 'npm', args);
317+
return _exec({}, 'npm', args);
318318
}
319319

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

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');
+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)