mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
@@ -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)));
|
||||
}
|
||||
Reference in New Issue
Block a user