test(@angular-devkit/build-angular): integrate custom file watching into harness

This change integrates the recently introduced internal custom file watching support into the builder test harness.  This allows the test harness to directly trigger file changes and allows more comprehensive test isolation for builder watch scenarios.
This commit is contained in:
Charles Lyding 2020-12-31 12:08:51 -05:00 committed by Filipe Silva
parent 11bbe7c45f
commit bec3bec3b2
2 changed files with 106 additions and 10 deletions

View File

@ -20,9 +20,10 @@ import {
} from '@angular-devkit/architect';
import { WorkspaceHost } from '@angular-devkit/architect/node';
import { TestProjectHost } from '@angular-devkit/architect/testing';
import { analytics, getSystemPath, json, logging, normalize } from '@angular-devkit/core';
import { analytics, getSystemPath, join, json, logging, normalize } from '@angular-devkit/core';
import { Observable, Subject, from as observableFrom, of as observableOf } from 'rxjs';
import { catchError, first, map, mergeMap, shareReplay } from 'rxjs/operators';
import { catchError, finalize, first, map, mergeMap, shareReplay } from 'rxjs/operators';
import { BuilderWatcherFactory, WatcherNotifier } from './file-watching';
export interface BuilderHarnessExecutionResult<T extends BuilderOutput = BuilderOutput> {
result?: T;
@ -34,6 +35,7 @@ export interface BuilderHarnessExecutionOptions {
configuration: string;
outputLogsOnFailure: boolean;
outputLogsOnException: boolean;
useNativeFileWatching: boolean;
}
export class BuilderHarness<T> {
@ -48,6 +50,7 @@ export class BuilderHarness<T> {
// tslint:disable-next-line: no-any
{ handler: BuilderHandlerFn<any>; info: BuilderInfo; options: json.JsonObject }
>();
private watcherNotifier?: WatcherNotifier;
constructor(
private readonly builderHandler: BuilderHandlerFn<T & json.JsonObject>,
@ -111,12 +114,25 @@ export class BuilderHarness<T> {
execute(
options: Partial<BuilderHarnessExecutionOptions> = {},
): Observable<BuilderHarnessExecutionResult> {
const { configuration, outputLogsOnException = true, outputLogsOnFailure = true } = options;
const {
configuration,
outputLogsOnException = true,
outputLogsOnFailure = true,
useNativeFileWatching = false,
} = options;
const targetOptions = {
...this.options.get(null),
...((configuration && this.options.get(configuration)) ?? {}),
};
if (!useNativeFileWatching) {
if (this.watcherNotifier) {
throw new Error('Only one harness execution at a time is supported.');
}
this.watcherNotifier = new WatcherNotifier();
}
const contextHost: ContextHost = {
findBuilderByTarget: async (project, target) => {
this.validateProjectName(project);
@ -181,6 +197,7 @@ export class BuilderHarness<T> {
this.builderInfo,
getSystemPath(this.host.root()),
contextHost,
useNativeFileWatching ? undefined : this.watcherNotifier,
);
if (this.targetName !== undefined) {
context.target = {
@ -222,12 +239,12 @@ export class BuilderHarness<T> {
return { result, error, logs: currentLogs };
}),
mergeMap(async (executionResult) => {
for (const teardown of context.teardowns) {
await teardown();
}
finalize(() => {
this.watcherNotifier = undefined;
return executionResult;
for (const teardown of context.teardowns) {
teardown();
}
}),
);
}
@ -243,18 +260,36 @@ export class BuilderHarness<T> {
this.host
.scopedSync()
.write(normalize(path), typeof content === 'string' ? Buffer.from(content) : content);
this.watcherNotifier?.notify([
{ path: getSystemPath(join(this.host.root(), path)), type: 'modified' },
]);
}
async writeFiles(files: Record<string, string | Buffer>): Promise<void> {
const watchEvents = this.watcherNotifier
? ([] as { path: string; type: 'modified' | 'deleted' }[])
: undefined;
for (const [path, content] of Object.entries(files)) {
this.host
.scopedSync()
.write(normalize(path), typeof content === 'string' ? Buffer.from(content) : content);
watchEvents?.push({ path: getSystemPath(join(this.host.root(), path)), type: 'modified' });
}
if (watchEvents) {
this.watcherNotifier?.notify(watchEvents);
}
}
async removeFile(path: string): Promise<void> {
return this.host.scopedSync().delete(normalize(path));
this.host.scopedSync().delete(normalize(path));
this.watcherNotifier?.notify([
{ path: getSystemPath(join(this.host.root(), path)), type: 'deleted' },
]);
}
async modifyFile(
@ -263,6 +298,10 @@ export class BuilderHarness<T> {
): Promise<void> {
const content = this.readFile(path);
await this.writeFile(path, await modifier(content));
this.watcherNotifier?.notify([
{ path: getSystemPath(join(this.host.root(), path)), type: 'modified' },
]);
}
hasFile(path: string): boolean {
@ -303,6 +342,7 @@ class HarnessBuilderContext implements BuilderContext {
public builder: BuilderInfo,
basePath: string,
private readonly contextHost: ContextHost,
public readonly watcherFactory: BuilderWatcherFactory | undefined,
) {
this.workspaceRoot = this.currentDirectory = basePath;
}
@ -356,7 +396,12 @@ class HarnessBuilderContext implements BuilderContext {
info.builderName,
);
const context = new HarnessBuilderContext(info, this.workspaceRoot, this.contextHost);
const context = new HarnessBuilderContext(
info,
this.workspaceRoot,
this.contextHost,
this.watcherFactory,
);
context.target = target;
context.logger = scheduleOptions?.logger || this.logger.createChild('');

View File

@ -0,0 +1,51 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
BuilderWatcherCallback,
BuilderWatcherFactory,
} from '../webpack/plugins/builder-watch-plugin';
class WatcherDescriptor {
constructor(
readonly files: ReadonlySet<string>,
readonly directories: ReadonlySet<string>,
readonly callback: BuilderWatcherCallback,
) {}
shouldNotify(path: string): boolean {
return true;
}
}
export class WatcherNotifier implements BuilderWatcherFactory {
private readonly descriptors = new Set<WatcherDescriptor>();
notify(events: Iterable<{ path: string; type: 'modified' | 'deleted' }>): void {
for (const descriptor of this.descriptors) {
for (const { path } of events) {
if (descriptor.shouldNotify(path)) {
descriptor.callback([...events]);
break;
}
}
}
}
watch(
files: Iterable<string>,
directories: Iterable<string>,
callback: BuilderWatcherCallback,
): { close(): void } {
const descriptor = new WatcherDescriptor(new Set(files), new Set(directories), callback);
this.descriptors.add(descriptor);
return { close: () => this.descriptors.delete(descriptor) };
}
}
export { BuilderWatcherFactory };