refactor(language-service): Remove old testing helpers (#40966)
All specs have been switched to the new testing package. The old test helpers are no longer needed. PR Close #40966
This commit is contained in:
parent
dcee784b4f
commit
cf687fe8ab
@ -1,226 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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 {TmplAstNode} from '@angular/compiler';
|
|
||||||
import {StrictTemplateOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
|
|
||||||
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, getSourceFileOrError} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
||||||
import {MockFileSystem, TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
|
||||||
import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing';
|
|
||||||
import {OptimizeFor, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
|
||||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
|
||||||
|
|
||||||
import {LanguageService} from '../language_service';
|
|
||||||
|
|
||||||
import {MockServerHost} from './mock_host';
|
|
||||||
|
|
||||||
// TODO(alxhub): replace this environment with //packages/language-service/ivy/testing
|
|
||||||
|
|
||||||
function writeTsconfig(
|
|
||||||
fs: FileSystem, entryFiles: AbsoluteFsPath[], options: TestableOptions): void {
|
|
||||||
fs.writeFile(
|
|
||||||
absoluteFrom('/tsconfig.json'),
|
|
||||||
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
compilerOptions: {
|
|
||||||
strict: true,
|
|
||||||
experimentalDecorators: true,
|
|
||||||
moduleResolution: 'node',
|
|
||||||
target: 'es2015',
|
|
||||||
lib: [
|
|
||||||
'dom',
|
|
||||||
'es2015',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
files: entryFiles,
|
|
||||||
angularCompilerOptions: {
|
|
||||||
strictTemplates: true,
|
|
||||||
...options,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TestableOptions = StrictTemplateOptions;
|
|
||||||
|
|
||||||
export class LanguageServiceTestEnvironment {
|
|
||||||
private constructor(
|
|
||||||
readonly tsLS: ts.LanguageService, readonly ngLS: LanguageService,
|
|
||||||
readonly projectService: ts.server.ProjectService, readonly host: MockServerHost) {}
|
|
||||||
|
|
||||||
static setup(files: TestFile[], options: TestableOptions = {}): LanguageServiceTestEnvironment {
|
|
||||||
const fs = getFileSystem();
|
|
||||||
if (!(fs instanceof MockFileSystem)) {
|
|
||||||
throw new Error(`LanguageServiceTestEnvironment only works with a mock filesystem`);
|
|
||||||
}
|
|
||||||
fs.init(loadStandardTestFiles({
|
|
||||||
fakeCommon: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const host = new MockServerHost(fs);
|
|
||||||
const tsconfigPath = absoluteFrom('/tsconfig.json');
|
|
||||||
|
|
||||||
const entryFiles: AbsoluteFsPath[] = [];
|
|
||||||
for (const {name, contents, isRoot} of files) {
|
|
||||||
fs.writeFile(name, contents);
|
|
||||||
if (isRoot === true) {
|
|
||||||
entryFiles.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entryFiles.length === 0) {
|
|
||||||
throw new Error(`Expected at least one root TestFile.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeTsconfig(fs, files.filter(file => file.isRoot === true).map(file => file.name), options);
|
|
||||||
|
|
||||||
const projectService = new ts.server.ProjectService({
|
|
||||||
host,
|
|
||||||
logger,
|
|
||||||
cancellationToken: ts.server.nullCancellationToken,
|
|
||||||
useSingleInferredProject: true,
|
|
||||||
useInferredProjectPerProjectRoot: true,
|
|
||||||
typingsInstaller: ts.server.nullTypingsInstaller,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open all root files.
|
|
||||||
for (const entryFile of entryFiles) {
|
|
||||||
projectService.openClientFile(entryFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
const project = projectService.findProject(tsconfigPath);
|
|
||||||
if (project === undefined) {
|
|
||||||
throw new Error(`Failed to create project for ${tsconfigPath}`);
|
|
||||||
}
|
|
||||||
// The following operation forces a ts.Program to be created.
|
|
||||||
const tsLS = project.getLanguageService();
|
|
||||||
|
|
||||||
const ngLS = new LanguageService(project, tsLS);
|
|
||||||
return new LanguageServiceTestEnvironment(tsLS, ngLS, projectService, host);
|
|
||||||
}
|
|
||||||
|
|
||||||
getClass(fileName: AbsoluteFsPath, className: string): ts.ClassDeclaration {
|
|
||||||
const program = this.tsLS.getProgram();
|
|
||||||
if (program === undefined) {
|
|
||||||
throw new Error(`Expected to get a ts.Program`);
|
|
||||||
}
|
|
||||||
const sf = getSourceFileOrError(program, fileName);
|
|
||||||
return getClassOrError(sf, className);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFileWithCursor(fileName: AbsoluteFsPath, contents: string): {cursor: number, text: string} {
|
|
||||||
const {cursor, text} = extractCursorInfo(contents);
|
|
||||||
this.updateFile(fileName, text);
|
|
||||||
return {cursor, text};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFile(fileName: AbsoluteFsPath, contents: string): void {
|
|
||||||
const normalFileName = ts.server.toNormalizedPath(fileName);
|
|
||||||
const scriptInfo =
|
|
||||||
this.projectService.getOrCreateScriptInfoForNormalizedPath(normalFileName, true, '');
|
|
||||||
if (scriptInfo === undefined) {
|
|
||||||
throw new Error(`Could not find a file named ${fileName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current contents to find the length
|
|
||||||
const len = scriptInfo.getSnapshot().getLength();
|
|
||||||
scriptInfo.editContent(0, len, contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
expectNoSourceDiagnostics(): void {
|
|
||||||
const program = this.tsLS.getProgram();
|
|
||||||
if (program === undefined) {
|
|
||||||
throw new Error(`Expected to get a ts.Program`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ngCompiler = this.ngLS.compilerFactory.getOrCreate();
|
|
||||||
|
|
||||||
for (const sf of program.getSourceFiles()) {
|
|
||||||
if (sf.isDeclarationFile || sf.fileName.endsWith('.ngtypecheck.ts')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syntactic = program.getSyntacticDiagnostics(sf);
|
|
||||||
expect(syntactic.map(diag => diag.messageText)).toEqual([]);
|
|
||||||
if (syntactic.length > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const semantic = program.getSemanticDiagnostics(sf);
|
|
||||||
expect(semantic.map(diag => diag.messageText)).toEqual([]);
|
|
||||||
if (semantic.length > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's more efficient to optimize for WholeProgram since we call this with every file in the
|
|
||||||
// program.
|
|
||||||
const ngDiagnostics = ngCompiler.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram);
|
|
||||||
expect(ngDiagnostics.map(diag => diag.messageText)).toEqual([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ngLS.compilerFactory.registerLastKnownProgram();
|
|
||||||
}
|
|
||||||
|
|
||||||
expectNoTemplateDiagnostics(fileName: AbsoluteFsPath, className: string): void {
|
|
||||||
const program = this.tsLS.getProgram();
|
|
||||||
if (program === undefined) {
|
|
||||||
throw new Error(`Expected to get a ts.Program`);
|
|
||||||
}
|
|
||||||
const sf = getSourceFileOrError(program, fileName);
|
|
||||||
const component = getClassOrError(sf, className);
|
|
||||||
|
|
||||||
const diags = this.getTemplateTypeChecker().getDiagnosticsForComponent(component);
|
|
||||||
this.ngLS.compilerFactory.registerLastKnownProgram();
|
|
||||||
expect(diags.map(diag => diag.messageText)).toEqual([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTemplateTypeChecker(): TemplateTypeChecker {
|
|
||||||
return this.ngLS.compilerFactory.getOrCreate().getTemplateTypeChecker();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger: ts.server.Logger = {
|
|
||||||
close(): void{},
|
|
||||||
hasLevel(level: ts.server.LogLevel): boolean {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
loggingEnabled(): boolean {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
perftrc(s: string): void{},
|
|
||||||
info(s: string): void{},
|
|
||||||
startGroup(): void{},
|
|
||||||
endGroup(): void{},
|
|
||||||
msg(s: string, type?: ts.server.Msg): void{},
|
|
||||||
getLogFileName(): string |
|
|
||||||
undefined {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function getClassOrError(sf: ts.SourceFile, name: string): ts.ClassDeclaration {
|
|
||||||
for (const stmt of sf.statements) {
|
|
||||||
if (ts.isClassDeclaration(stmt) && stmt.name !== undefined && stmt.name.text === name) {
|
|
||||||
return stmt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractCursorInfo(textWithCursor: string): {cursor: number, text: string} {
|
|
||||||
const cursor = textWithCursor.indexOf('¦');
|
|
||||||
if (cursor === -1 || textWithCursor.indexOf('¦', cursor + 1) !== -1) {
|
|
||||||
throw new Error(`Expected to find exactly one cursor symbol '¦'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
cursor,
|
|
||||||
text: textWithCursor.substr(0, cursor) + textWithCursor.substr(cursor + 1),
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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 {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
||||||
import {MockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
|
||||||
|
|
||||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
|
||||||
|
|
||||||
const NOOP_FILE_WATCHER: ts.FileWatcher = {
|
|
||||||
close() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapts from the `ts.server.ServerHost` API to an in-memory filesystem.
|
|
||||||
*/
|
|
||||||
export class MockServerHost implements ts.server.ServerHost {
|
|
||||||
constructor(private fs: MockFileSystem) {}
|
|
||||||
|
|
||||||
get newLine(): string {
|
|
||||||
return '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
get useCaseSensitiveFileNames(): boolean {
|
|
||||||
return this.fs.isCaseSensitive();
|
|
||||||
}
|
|
||||||
|
|
||||||
readFile(path: string, encoding?: string): string|undefined {
|
|
||||||
return this.fs.readFile(absoluteFrom(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvePath(path: string): string {
|
|
||||||
return this.fs.resolve(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
fileExists(path: string): boolean {
|
|
||||||
const absPath = absoluteFrom(path);
|
|
||||||
return this.fs.exists(absPath) && this.fs.lstat(absPath).isFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
directoryExists(path: string): boolean {
|
|
||||||
const absPath = absoluteFrom(path);
|
|
||||||
return this.fs.exists(absPath) && this.fs.lstat(absPath).isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
createDirectory(path: string): void {
|
|
||||||
this.fs.ensureDir(absoluteFrom(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
getExecutingFilePath(): string {
|
|
||||||
// This is load-bearing, as TypeScript uses the result of this call to locate the directory in
|
|
||||||
// which it expects to find .d.ts files for the "standard libraries" - DOM, ES2015, etc.
|
|
||||||
return '/node_modules/typescript/lib/tsserver.js';
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentDirectory(): string {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
createHash(data: string): string {
|
|
||||||
return ts.sys.createHash!(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
get args(): string[] {
|
|
||||||
throw new Error('Property not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
watchFile(
|
|
||||||
path: string, callback: ts.FileWatcherCallback, pollingInterval?: number,
|
|
||||||
options?: ts.WatchOptions): ts.FileWatcher {
|
|
||||||
return NOOP_FILE_WATCHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
watchDirectory(
|
|
||||||
path: string, callback: ts.DirectoryWatcherCallback, recursive?: boolean,
|
|
||||||
options?: ts.WatchOptions): ts.FileWatcher {
|
|
||||||
return NOOP_FILE_WATCHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(timeoutId: any): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
setImmediate(callback: (...args: any[]) => void, ...args: any[]) {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
clearImmediate(timeoutId: any): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
write(s: string): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectories(path: string): string[] {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
readDirectory(
|
|
||||||
path: string, extensions?: readonly string[], exclude?: readonly string[],
|
|
||||||
include?: readonly string[], depth?: number): string[] {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(exitCode?: number): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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 {absoluteFrom as _} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
||||||
import {TestFile} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
|
|
||||||
import {LanguageServiceTestEnvironment, TestableOptions} from '@angular/language-service/ivy/test/env';
|
|
||||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
|
||||||
|
|
||||||
|
|
||||||
export function getText(contents: string, textSpan: ts.TextSpan) {
|
|
||||||
return contents.substr(textSpan.start, textSpan.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
function last<T>(array: T[]): T {
|
|
||||||
return array[array.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFirstClassDeclaration(declaration: string) {
|
|
||||||
const matches = declaration.match(/(?:export class )(\w+)(?:\s|\{)/);
|
|
||||||
if (matches === null || matches.length !== 2) {
|
|
||||||
throw new Error(`Did not find exactly one exported class in: ${declaration}`);
|
|
||||||
}
|
|
||||||
return matches[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createModuleWithDeclarations(
|
|
||||||
filesWithClassDeclarations: TestFile[], externalResourceFiles: TestFile[] = [],
|
|
||||||
options: TestableOptions = {}): LanguageServiceTestEnvironment {
|
|
||||||
const externalClasses =
|
|
||||||
filesWithClassDeclarations.map(file => getFirstClassDeclaration(file.contents));
|
|
||||||
const externalImports = filesWithClassDeclarations.map(file => {
|
|
||||||
const className = getFirstClassDeclaration(file.contents);
|
|
||||||
const fileName = last(file.name.split('/')).replace('.ts', '');
|
|
||||||
return `import {${className}} from './${fileName}';`;
|
|
||||||
});
|
|
||||||
const contents = `
|
|
||||||
import {NgModule} from '@angular/core';
|
|
||||||
import {CommonModule} from '@angular/common';
|
|
||||||
${externalImports.join('\n')}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [${externalClasses.join(',')}],
|
|
||||||
imports: [CommonModule],
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
||||||
`;
|
|
||||||
const moduleFile = {name: _('/app-module.ts'), contents, isRoot: true};
|
|
||||||
return LanguageServiceTestEnvironment.setup(
|
|
||||||
[moduleFile, ...filesWithClassDeclarations, ...externalResourceFiles], options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function humanizeDocumentSpanLike<T extends ts.DocumentSpan>(
|
|
||||||
item: T, env: LanguageServiceTestEnvironment, overrides: Map<string, string> = new Map()): T&
|
|
||||||
Stringy<ts.DocumentSpan> {
|
|
||||||
const fileContents = (overrides.has(item.fileName) ? overrides.get(item.fileName) :
|
|
||||||
env.host.readFile(item.fileName)) ??
|
|
||||||
'';
|
|
||||||
if (!fileContents) {
|
|
||||||
throw new Error(`Could not read file ${item.fileName}`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
textSpan: getText(fileContents, item.textSpan),
|
|
||||||
contextSpan: item.contextSpan ? getText(fileContents, item.contextSpan) : undefined,
|
|
||||||
originalTextSpan: item.originalTextSpan ? getText(fileContents, item.originalTextSpan) :
|
|
||||||
undefined,
|
|
||||||
originalContextSpan:
|
|
||||||
item.originalContextSpan ? getText(fileContents, item.originalContextSpan) : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
type Stringy<T> = {
|
|
||||||
[P in keyof T]: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function assertFileNames(refs: Array<{fileName: string}>, expectedFileNames: string[]) {
|
|
||||||
const actualPaths = refs.map(r => r.fileName);
|
|
||||||
const actualFileNames = actualPaths.map(p => last(p.split('/')));
|
|
||||||
expect(new Set(actualFileNames)).toEqual(new Set(expectedFileNames));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assertTextSpans(items: Array<{textSpan: string}>, expectedTextSpans: string[]) {
|
|
||||||
const actualSpans = items.map(item => item.textSpan);
|
|
||||||
expect(new Set(actualSpans)).toEqual(new Set(expectedTextSpans));
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user