test(compiler-cli): move testing utils to separate package (#39594)

ngtsc has a robust suite of testing utilities, designed for in-memory
testing of a TypeScript compiler. Previously these utilities lived in the
`test` directory for the compiler-cli package.

This commit moves those utilities to an `ngtsc/testing` package, enabling
them to be depended on separately and opening the door for using them from
the upcoming language server testing infrastructure.

As part of this refactoring, the `fake_core` package (a lightweight API
replacement for @angular/core) is expanded to include functionality needed
for Language Service test use cases.

PR Close #39594
This commit is contained in:
Alex Rickabaugh
2020-11-04 12:27:19 -08:00
committed by atscott
parent 806d7aa22c
commit 3613e7c4e5
80 changed files with 88 additions and 132 deletions
@@ -8,9 +8,11 @@ ts_library(
srcs = glob([
"**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/testing",
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/reflection",
"@npm//typescript",
],
@@ -0,0 +1,22 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ng_package", "ts_library")
ts_library(
name = "fake_core",
srcs = [
"index.ts",
],
module_name = "@angular/core",
)
ng_package(
name = "npm_package",
srcs = [
"package.json",
],
entry_point = ":index.ts",
deps = [
":fake_core",
],
)
@@ -0,0 +1,13 @@
`fake_core` is a library designed to expose some of the same symbols as `@angular/core`, without
requiring compilation of the whole of `@angular/core`. This enables unit tests for the compiler to
be written without incurring long rebuilds for every change.
* `@angular/core` is compiled with `@angular/compiler-cli`, and therefore has an implicit dependency
on it. Therefore core must be rebuilt if the compiler changes.
* Tests for the compiler which intend to build code that depends on `@angular/core` must have
a data dependency on `@angular/core`. Therefore core must be built to run the compiler tests, and
thus rebuilt if the compiler changes.
This rebuild cycle is expensive and slow. `fake_core` avoids this by exposing a subset of the
`@angular/core` API, which enables applications to be built by the ngtsc compiler without
needing a full version of core present at compile time.
@@ -0,0 +1,108 @@
/**
* @license
* Copyright Google LLC 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
*/
interface FnWithArg<T> {
(...args: any[]): T;
new(...args: any[]): T;
}
function callableClassDecorator(): FnWithArg<(clazz: any) => any> {
return null!;
}
function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> {
return null!;
}
function callablePropDecorator(): FnWithArg<(a: any, b: any) => any> {
return null!;
}
export const Component = callableClassDecorator();
export const Directive = callableClassDecorator();
export const Injectable = callableClassDecorator();
export const NgModule = callableClassDecorator();
export const Pipe = callableClassDecorator();
export const Attribute = callableParamDecorator();
export const Inject = callableParamDecorator();
export const Self = callableParamDecorator();
export const SkipSelf = callableParamDecorator();
export const Optional = callableParamDecorator();
export const Host = callableParamDecorator();
export const ContentChild = callablePropDecorator();
export const ContentChildren = callablePropDecorator();
export const HostBinding = callablePropDecorator();
export const HostListener = callablePropDecorator();
export const Input = callablePropDecorator();
export const Output = callablePropDecorator();
export const ViewChild = callablePropDecorator();
export const ViewChildren = callablePropDecorator();
// T defaults to `any` to reflect what is currently in core.
export type ModuleWithProviders<T = any> = any;
export class ChangeDetectorRef {}
export class ElementRef {}
export class Injector {}
export class TemplateRef<T = any> {}
export class ViewContainerRef {}
export class Renderer2 {}
export class ɵNgModuleFactory<T> {
constructor(public clazz: T) {}
}
export class InjectionToken<T> {
constructor(description: string) {}
}
export function forwardRef<T>(fn: () => T): T {
return fn();
}
export interface SimpleChanges {
[propName: string]: any;
}
export type ɵɵNgModuleDefWithMeta<ModuleT, DeclarationsT, ImportsT, ExportsT> = any;
export type ɵɵDirectiveDefWithMeta<DirT, SelectorT, ExportAsT, InputsT, OutputsT, QueriesT> = any;
export type ɵɵPipeDefWithMeta<PipeT, NameT> = any;
export enum ViewEncapsulation {
Emulated = 0,
// Historically the 1 value was for `Native` encapsulation which has been removed as of v11.
None = 2,
ShadowDom = 3
}
export enum ChangeDetectionStrategy {
OnPush = 0,
Default = 1
}
export const CUSTOM_ELEMENTS_SCHEMA: any = false;
export const NO_ERRORS_SCHEMA: any = false;
export class EventEmitter<T> {
subscribe(generatorOrNext?: any, error?: any, complete?: any): unknown {
return null;
}
}
export interface QueryList<T>/* implements Iterable<T> */ {
[Symbol.iterator]: () => Iterator<T>;
}
export type NgIterable<T> = Array<T>|Iterable<T>;
export class NgZone {}
export interface PipeTransform {
transform(value: any, ...args: any[]): any;
}
@@ -0,0 +1,5 @@
{
"name": "@angular/core",
"version": "0.0.0-FAKE-FOR-TESTS",
"description": "Fake version of core for ngtsc tests"
}
@@ -5,4 +5,7 @@
* 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
*/
export {expectCompleteReuse, getDeclaration, isNamedDeclaration, makeProgram} from './src/utils';
export * from './src/utils';
export * from './src/cached_source_files';
export * from './src/mock_file_loading';
export * from './src/runfile_helpers';
@@ -0,0 +1,44 @@
/**
* @license
* Copyright Google LLC 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 * as ts from 'typescript';
import {basename} from '../../file_system';
// A cache of source files that are typically used across tests and are expensive to parse.
let sourceFileCache = new Map<string, ts.SourceFile>();
/**
* If the `fileName` is determined to benefit from caching across tests, a parsed `ts.SourceFile`
* is returned from a shared cache. If caching is not applicable for the requested `fileName`, then
* `null` is returned.
*
* Even if a `ts.SourceFile` already exists for the given `fileName` will the contents be loaded
* from disk, such that it can be verified whether the cached `ts.SourceFile` is identical to the
* disk contents. If there is a difference, a new `ts.SourceFile` is parsed from the loaded contents
* which replaces the prior cache entry.
*
* @param fileName the path of the file to request a source file for.
* @param load a callback to load the contents of the file; this is even called when a cache entry
* is available to verify that the cached `ts.SourceFile` corresponds with the contents on disk.
*/
export function getCachedSourceFile(
fileName: string, load: () => string | undefined): ts.SourceFile|null {
if (!/^lib\..+\.d\.ts$/.test(basename(fileName))) {
return null;
}
const content = load();
if (content === undefined) {
return null;
}
if (!sourceFileCache.has(fileName) || sourceFileCache.get(fileName)!.text !== content) {
const sf = ts.createSourceFile(fileName, content, ts.ScriptTarget.ES2015);
sourceFileCache.set(fileName, sf);
}
return sourceFileCache.get(fileName)!;
}
@@ -0,0 +1,119 @@
/**
* @license
* Copyright Google LLC 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
*/
/// <reference types="node" />
import {readdirSync, readFileSync, statSync} from 'fs';
import {resolve} from 'path';
import {AbsoluteFsPath, FileSystem, getFileSystem} from '../../file_system';
import {Folder, MockFileSystemPosix, TestFile} from '../../file_system/testing';
import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from './runfile_helpers';
export function loadTestFiles(files: TestFile[]) {
const fs = getFileSystem();
files.forEach(file => {
fs.ensureDir(fs.dirname(file.name));
fs.writeFile(file.name, file.contents);
});
}
/**
* A folder that is lazily loaded upon first access and then cached.
*/
class CachedFolder {
private folder: Folder|null = null;
constructor(private loader: () => Folder) {}
get(): Folder {
if (this.folder === null) {
this.folder = this.loader();
}
return this.folder;
}
}
const typescriptFolder = new CachedFolder(() => loadFolder(resolveNpmTreeArtifact('typescript')));
const angularFolder = new CachedFolder(loadAngularFolder);
const rxjsFolder = new CachedFolder(() => loadFolder(resolveNpmTreeArtifact('rxjs')));
export function loadStandardTestFiles(
{fakeCore = true, rxjs = false}: {fakeCore?: boolean, rxjs?: boolean} = {}): Folder {
const tmpFs = new MockFileSystemPosix(true);
const basePath = '/' as AbsoluteFsPath;
tmpFs.mount(tmpFs.resolve('/node_modules/typescript'), typescriptFolder.get());
loadTsLib(tmpFs, basePath);
if (fakeCore) {
loadFakeCore(tmpFs, basePath);
} else {
tmpFs.mount(tmpFs.resolve('/node_modules/@angular'), angularFolder.get());
}
if (rxjs) {
tmpFs.mount(tmpFs.resolve('/node_modules/rxjs'), rxjsFolder.get());
}
return tmpFs.dump();
}
export function loadTsLib(fs: FileSystem, basePath: string = '/') {
loadTestDirectory(
fs, resolveNpmTreeArtifact('tslib'), fs.resolve(basePath, 'node_modules/tslib'));
}
export function loadFakeCore(fs: FileSystem, basePath: string = '/') {
loadTestDirectory(
fs,
resolveNpmTreeArtifact(
'angular/packages/compiler-cli/src/ngtsc/testing/fake_core/npm_package'),
fs.resolve(basePath, 'node_modules/@angular/core'));
}
function loadFolder(path: string): Folder {
const tmpFs = new MockFileSystemPosix(true);
// Note that we intentionally pass the native `path`, without resolving it through the file
// system, because the mock posix file system may break paths coming from a non-posix system.
loadTestDirectory(tmpFs, path, tmpFs.resolve('/'));
return tmpFs.dump();
}
function loadAngularFolder(): Folder {
const tmpFs = new MockFileSystemPosix(true);
getAngularPackagesFromRunfiles().forEach(({name, pkgPath}) => {
loadTestDirectory(tmpFs, pkgPath, tmpFs.resolve(name));
});
return tmpFs.dump();
}
/**
* Load real files from the real file-system into a mock file-system.
* @param fs the file-system where the directory is to be loaded.
* @param directoryPath the path to the directory we want to load.
* @param mockPath the path within the mock file-system where the directory is to be loaded.
*/
export function loadTestDirectory(
fs: FileSystem, directoryPath: string, mockPath: AbsoluteFsPath): void {
readdirSync(directoryPath).forEach(item => {
const srcPath = resolve(directoryPath, item);
const targetPath = fs.resolve(mockPath, item);
try {
if (statSync(srcPath).isDirectory()) {
fs.ensureDir(targetPath);
loadTestDirectory(fs, srcPath, targetPath);
} else {
fs.ensureDir(fs.dirname(targetPath));
fs.writeFile(targetPath, readFileSync(srcPath, 'utf-8'));
}
} catch (e) {
console.warn(`Failed to add ${srcPath} to the mock file-system: ${e.message}`);
}
});
}
@@ -0,0 +1,49 @@
/**
* @license
* Copyright Google LLC 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
*/
/// <reference types="node" />
import * as fs from 'fs';
import * as path from 'path';
/**
* Gets all built Angular NPM package artifacts by querying the Bazel runfiles.
* In case there is a runfiles manifest (e.g. on Windows), the packages are resolved
* through the manifest because the runfiles are not symlinked and cannot be searched
* within the real filesystem.
*/
export function getAngularPackagesFromRunfiles() {
// Path to the Bazel runfiles manifest if present. This file is present if runfiles are
// not symlinked into the runfiles directory.
const runfilesManifestPath = process.env.RUNFILES_MANIFEST_FILE;
if (!runfilesManifestPath) {
const packageRunfilesDir = path.join(process.env.RUNFILES!, 'angular/packages');
return fs.readdirSync(packageRunfilesDir)
.map(name => ({name, pkgPath: path.join(packageRunfilesDir, name, 'npm_package/')}))
.filter(({pkgPath}) => fs.existsSync(pkgPath));
}
return fs.readFileSync(runfilesManifestPath, 'utf8')
.split('\n')
.map(mapping => mapping.split(' '))
.filter(([runfilePath]) => runfilePath.match(/^angular\/packages\/[\w-]+\/npm_package$/))
.map(([runfilePath, realPath]) => ({
name: path.relative('angular/packages', runfilePath).split(path.sep)[0],
pkgPath: realPath,
}));
}
/**
* Resolves a NPM package from the Bazel runfiles. We need to resolve the Bazel tree
* artifacts using a "resolve file" because the NodeJS module resolution does not allow
* resolving to directory paths.
*/
export function resolveNpmTreeArtifact(manifestPath: string, resolveFile = 'package.json') {
return path.dirname(require.resolve(path.posix.join(manifestPath, resolveFile)));
}