Skip to content

Commit

Permalink
feat(@angular-devkit/architect): use temp folder in TestProjectHost
Browse files Browse the repository at this point in the history
Using Git had a nasty tendency to clear changes while testing.
  • Loading branch information
filipesilva authored and hansl committed Jun 6, 2018
1 parent 63d898d commit 2ab8b76
Show file tree
Hide file tree
Showing 49 changed files with 266 additions and 266 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Outputs
bazel-*
test-project-host-*
dist/

# IDEs
Expand Down
3 changes: 1 addition & 2 deletions packages/angular_devkit/architect/testing/run-target-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ import { TestProjectHost } from './test-project-host';


export function runTargetSpec(
workspaceRoot: Path,
host: TestProjectHost,
targetSpec: TargetSpecifier,
overrides = {},
logger: logging.Logger = new logging.NullLogger(),
): Observable<BuildEvent> {
targetSpec = { ...targetSpec, overrides };
const workspaceFile = normalize('angular.json');
const workspace = new experimental.workspace.Workspace(workspaceRoot, host);
const workspace = new experimental.workspace.Workspace(host.root(), host);

return workspace.loadWorkspaceFromHost(workspaceFile).pipe(
concatMap(ws => new Architect(ws).loadArchitect()),
Expand Down
141 changes: 78 additions & 63 deletions packages/angular_devkit/architect/testing/test-project-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,92 +8,96 @@

import {
Path,
getSystemPath,
basename,
dirname,
join,
normalize,
relative,
virtualFs,
} from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { SpawnOptions, spawn } from 'child_process';
import { Stats } from 'fs';
import { EMPTY, Observable } from 'rxjs';
import { concatMap, map } from 'rxjs/operators';
import { EMPTY, Observable, from, of } from 'rxjs';
import { concatMap, delay, map, mergeMap, retry, tap } from 'rxjs/operators';


interface ProcessOutput {
stdout: string;
stderr: string;
}

export class TestProjectHost extends NodeJsSyncHost {
private _scopedSyncHost: virtualFs.SyncDelegateHost<Stats>;
private _currentRoot: Path | null = null;
private _scopedSyncHost: virtualFs.SyncDelegateHost<Stats> | null = null;

constructor(protected _root: Path) {
constructor(protected _templateRoot: Path) {
super();
this._scopedSyncHost = new virtualFs.SyncDelegateHost(new virtualFs.ScopedHost(this, _root));
}

scopedSync() {
return this._scopedSyncHost;
}
root(): Path {
if (this._currentRoot === null) {
throw new Error('TestProjectHost must be initialized before being used.');
}

initialize(): Observable<void> {
return this.exists(normalize('.git')).pipe(
concatMap(exists => !exists ? this._gitInit() : EMPTY),
);
// tslint:disable-next-line:non-null-operator
return this._currentRoot!;
}

restore(): Observable<void> {
return this._gitClean();
scopedSync(): virtualFs.SyncDelegateHost<Stats> {
if (this._currentRoot === null || this._scopedSyncHost === null) {
throw new Error('TestProjectHost must be initialized before being used.');
}

// tslint:disable-next-line:non-null-operator
return this._scopedSyncHost!;
}

private _gitClean(): Observable<void> {
return this._exec('git', ['clean', '-fd']).pipe(
concatMap(() => this._exec('git', ['checkout', '.'])),
map(() => { }),
initialize(): Observable<void> {
const recursiveList = (path: Path): Observable<Path> => this.list(path).pipe(
// Emit each fragment individually.
concatMap(fragments => from(fragments)),
// Join the path with fragment.
map(fragment => join(path, fragment)),
// Emit directory content paths instead of the directory path.
mergeMap(path => this.isDirectory(path).pipe(
concatMap(isDir => isDir ? recursiveList(path) : of(path)),
)),
);
}

private _gitInit(): Observable<void> {
return this._exec('git', ['init']).pipe(
concatMap(() => this._exec('git', ['config', 'user.email', 'angular-core+e2e@google.com'])),
concatMap(() => this._exec('git', ['config', 'user.name', 'Angular DevKit Tests'])),
concatMap(() => this._exec('git', ['add', '--all'])),
concatMap(() => this._exec('git', ['commit', '-am', '"Initial commit"'])),
// Find a unique folder that we can write to to use as current root.
return this.findUniqueFolderPath().pipe(
// Save the path and create a scoped host for it.
tap(newFolderPath => {
this._currentRoot = newFolderPath;
this._scopedSyncHost = new virtualFs.SyncDelegateHost(
new virtualFs.ScopedHost(this, this.root()));
}),
// List all files in root.
concatMap(() => recursiveList(this._templateRoot)),
// Copy them over to the current root.
concatMap(from => {
const to = join(this.root(), relative(this._templateRoot, from));

return this.read(from).pipe(
concatMap(buffer => this.write(to, buffer)),
);
}),
map(() => { }),
);
}

private _exec(cmd: string, args: string[]): Observable<ProcessOutput> {
return new Observable(obs => {
args = args.filter(x => x !== undefined);
let stdout = '';
let stderr = '';

const spawnOptions: SpawnOptions = { cwd: getSystemPath(this._root) };

if (process.platform.startsWith('win')) {
args.unshift('/c', cmd);
cmd = 'cmd.exe';
spawnOptions['stdio'] = 'pipe';
}

const childProcess = spawn(cmd, args, spawnOptions);
childProcess.stdout.on('data', (data: Buffer) => stdout += data.toString('utf-8'));
childProcess.stderr.on('data', (data: Buffer) => stderr += data.toString('utf-8'));

// Create the error here so the stack shows who called this function.
const err = new Error(`Running "${cmd} ${args.join(' ')}" returned error code `);

childProcess.on('exit', (code) => {
if (!code) {
obs.next({ stdout, stderr });
} else {
err.message += `${code}.\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}\n`;
obs.error(err);
}
obs.complete();
});
});
restore(): Observable<void> {
if (this._currentRoot === null) {
return EMPTY;
}

// Delete the current root and clear the variables.
// Wait 50ms and retry up to 10 times, to give time for file locks to clear.
return this.exists(this.root()).pipe(
delay(50),
concatMap(exists => exists ? this.delete(this.root()) : of(null)),
retry(10),
tap(() => {
this._currentRoot = null;
this._scopedSyncHost = null;
}),
map(() => { }),
);
}

writeMultipleFiles(files: { [path: string]: string | ArrayBufferLike | Buffer }): void {
Expand Down Expand Up @@ -137,4 +141,15 @@ export class TestProjectHost extends NodeJsSyncHost {
const content = this.scopedSync().read(normalize(from));
this.scopedSync().write(normalize(to), content);
}

private findUniqueFolderPath(): Observable<Path> {
// 11 character alphanumeric string.
const randomString = Math.random().toString(36).slice(2);
const newFolderName = `test-project-host-${basename(this._templateRoot)}-${randomString}`;
const newFolderPath = join(dirname(this._templateRoot), newFolderName);

return this.exists(newFolderPath).pipe(
concatMap(exists => exists ? this.findUniqueFolderPath() : of(newFolderPath)),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { runTargetSpec } from '@angular-devkit/architect/testing';
import { normalize, virtualFs } from '@angular-devkit/core';
import { tap } from 'rxjs/operators';
import { Timeout, host, workspaceRoot } from '../utils';
import { Timeout, host } from '../utils';


describe('AppShell Builder', () => {
Expand Down Expand Up @@ -38,7 +38,7 @@ describe('AppShell Builder', () => {
`,
});

runTargetSpec(workspaceRoot, host, { project: 'app', target: 'app-shell' }).pipe(
runTargetSpec(host, { project: 'app', target: 'app-shell' }).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
const fileName = 'dist/index.html';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { runTargetSpec } from '@angular-devkit/architect/testing';
import { tap } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, workspaceRoot } from '../utils';
import { Timeout, browserTargetSpec, host } from '../utils';


describe('Browser Builder allow js', () => {
Expand All @@ -24,7 +24,7 @@ describe('Browser Builder allow js', () => {
// TODO: this test originally edited tsconfig to have `"allowJs": true` but works without it.
// Investigate.

runTargetSpec(workspaceRoot, host, browserTargetSpec).pipe(
runTargetSpec(host, browserTargetSpec).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
).toPromise().then(done, done.fail);
}, Timeout.Basic);
Expand All @@ -37,7 +37,7 @@ describe('Browser Builder allow js', () => {

const overrides = { aot: true };

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides).pipe(
runTargetSpec(host, browserTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
).toPromise().then(done, done.fail);
}, Timeout.Basic);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { runTargetSpec } from '@angular-devkit/architect/testing';
import { join, normalize, virtualFs } from '@angular-devkit/core';
import { tap } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, workspaceRoot } from '../utils';
import { Timeout, browserTargetSpec, host } from '../utils';


describe('Browser Builder AOT', () => {
Expand All @@ -21,7 +21,7 @@ describe('Browser Builder AOT', () => {
it('works', (done) => {
const overrides = { aot: true };

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides).pipe(
runTargetSpec(host, browserTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
const fileName = join(outputPath, 'main.js');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { runTargetSpec } from '@angular-devkit/architect/testing';
import { normalize, virtualFs } from '@angular-devkit/core';
import { tap, toArray } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, workspaceRoot } from '../utils';
import { Timeout, browserTargetSpec, host } from '../utils';


describe('Browser Builder assets', () => {
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('Browser Builder assets', () => {
],
};

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides).pipe(
runTargetSpec(host, browserTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
// Assets we expect should be there.
Expand All @@ -70,7 +70,7 @@ describe('Browser Builder assets', () => {
}],
};

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides)
runTargetSpec(host, browserTargetSpec, overrides)
.subscribe(undefined, () => done(), done.fail);

// The node_modules folder must be deleted, otherwise code that tries to find the
Expand All @@ -87,7 +87,7 @@ describe('Browser Builder assets', () => {
assets: ['not-source-root/file.txt'],
};

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides)
runTargetSpec(host, browserTargetSpec, overrides)
.subscribe(undefined, () => done(), done.fail);

// The node_modules folder must be deleted, otherwise code that tries to find the
Expand All @@ -100,7 +100,7 @@ describe('Browser Builder assets', () => {
assets: [],
};

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides).pipe(
runTargetSpec(host, browserTargetSpec, overrides).pipe(
toArray(),
tap((buildEvents) => expect(buildEvents.length).toBe(1)),
).toPromise().then(done, done.fail);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { runTargetSpec } from '@angular-devkit/architect/testing';
import { join, normalize, virtualFs } from '@angular-devkit/core';
import { tap } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, workspaceRoot } from '../utils';
import { Timeout, browserTargetSpec, host } from '../utils';


describe('Browser Builder base href', () => {
Expand All @@ -26,7 +26,7 @@ describe('Browser Builder base href', () => {

const overrides = { baseHref: '/myUrl' };

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides).pipe(
runTargetSpec(host, browserTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
const fileName = join(outputPath, 'index.html');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { runTargetSpec } from '@angular-devkit/architect/testing';
import { join, normalize, virtualFs } from '@angular-devkit/core';
import { tap } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, workspaceRoot } from '../utils';
import { Timeout, browserTargetSpec, host } from '../utils';


describe('Browser Builder build optimizer', () => {
Expand All @@ -20,7 +20,7 @@ describe('Browser Builder build optimizer', () => {

it('works', (done) => {
const overrides = { aot: true, buildOptimizer: true };
runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides).pipe(
runTargetSpec(host, browserTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => {
const fileName = join(outputPath, 'main.js');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { TestLogger, runTargetSpec } from '@angular-devkit/architect/testing';
import { tap } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, workspaceRoot } from '../utils';
import { Timeout, browserTargetSpec, host } from '../utils';


describe('Browser Builder bundle budgets', () => {
Expand All @@ -24,7 +24,7 @@ describe('Browser Builder bundle budgets', () => {

const logger = new TestLogger('rebuild-type-errors');

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides, logger).pipe(
runTargetSpec(host, browserTargetSpec, overrides, logger).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => expect(logger.includes('WARNING')).toBe(false)),
).toPromise().then(done, done.fail);
Expand All @@ -36,7 +36,7 @@ describe('Browser Builder bundle budgets', () => {
budgets: [{ type: 'all', maximumError: '100b' }],
};

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides).pipe(
runTargetSpec(host, browserTargetSpec, overrides).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(false)),
).toPromise().then(done, done.fail);
}, Timeout.Complex);
Expand All @@ -49,7 +49,7 @@ describe('Browser Builder bundle budgets', () => {

const logger = new TestLogger('rebuild-type-errors');

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides, logger).pipe(
runTargetSpec(host, browserTargetSpec, overrides, logger).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => expect(logger.includes('WARNING')).toBe(true)),
).toPromise().then(done, done.fail);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { TestLogger, runTargetSpec } from '@angular-devkit/architect/testing';
import { tap } from 'rxjs/operators';
import { Timeout, browserTargetSpec, host, workspaceRoot } from '../utils';
import { Timeout, browserTargetSpec, host } from '../utils';


describe('Browser Builder circular dependency detection', () => {
Expand All @@ -22,7 +22,7 @@ describe('Browser Builder circular dependency detection', () => {
const overrides = { baseHref: '/myUrl' };
const logger = new TestLogger('circular-dependencies');

runTargetSpec(workspaceRoot, host, browserTargetSpec, overrides, logger).pipe(
runTargetSpec(host, browserTargetSpec, overrides, logger).pipe(
tap((buildEvent) => expect(buildEvent.success).toBe(true)),
tap(() => expect(logger.includes('Circular dependency detected')).toBe(true)),
).toPromise().then(done, done.fail);
Expand Down

0 comments on commit 2ab8b76

Please sign in to comment.