test(language-service): add new mock host for testing ivy (#36879)
This commit adds a new mock host for testing the ivy language service. Unlike the existing mock_host which mocks the LanguageServiceHost, the Ivy mock host mocks just the filesystem interface, aka ts.ServerHost. This is because Ivy language service requires an actual Project to perform operations like adding synthetic typecheck files to the project, and by extension, to the ts.Program. These requirements make the existing mock host unsuitable to be reused. This new testing structure also improves test performance, because the old mock host copies (it actually creates symlinks, but still that's relatively expensive due to the sheer number of files involved) all @angular/* packages along with the typescript package to a temporary node_modules directory. This is done every time setup() is called. Instead, this new mock host just loads them from a pre-determined path in Bazel runfiles. PR Close #36879
This commit is contained in:
parent
6046e86506
commit
58ea040570
28
packages/language-service/ivy/test/BUILD.bazel
Normal file
28
packages/language-service/ivy/test/BUILD.bazel
Normal file
@ -0,0 +1,28 @@
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["*.ts"]),
|
||||
deps = [
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
data = [
|
||||
# Note that we used to depend on the npm_package of common, core, and
|
||||
# forms, but this is no longer the case. We did it for View Engine
|
||||
# because we wanted to load the flat dts, which is only available in the
|
||||
# npm_package. Ivy does not currently produce flat dts, so we might
|
||||
# as well just depend on the outputs of ng_module.
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/forms",
|
||||
"//packages/language-service/test:project",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
)
|
77
packages/language-service/ivy/test/mock_host.ts
Normal file
77
packages/language-service/ivy/test/mock_host.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @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 {join} from 'path';
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
export const TEST_SRCDIR = process.env.TEST_SRCDIR!;
|
||||
export const PROJECT_DIR =
|
||||
join(TEST_SRCDIR, 'angular', 'packages', 'language-service', 'test', 'project');
|
||||
export const TSCONFIG = join(PROJECT_DIR, 'tsconfig.json');
|
||||
export const APP_COMPONENT = join(PROJECT_DIR, 'app', 'app.component.ts');
|
||||
export const APP_MAIN = join(PROJECT_DIR, 'app', 'main.ts');
|
||||
export const PARSING_CASES = join(PROJECT_DIR, 'app', 'parsing-cases.ts');
|
||||
|
||||
export const host: ts.server.ServerHost = {
|
||||
...ts.sys,
|
||||
readFile(absPath: string, encoding?: string): string |
|
||||
undefined {
|
||||
// TODO: Need to remove all annotations in templates like we do in
|
||||
// MockTypescriptHost
|
||||
return ts.sys.readFile(absPath, encoding);
|
||||
},
|
||||
// TODO: Need to cast as never because this is not a proper ServerHost interface.
|
||||
// ts.sys lacks methods like watchFile() and watchDirectory(), but these are not
|
||||
// needed for now.
|
||||
} as never;
|
||||
|
||||
/**
|
||||
* Create a ConfiguredProject and an actual program for the test project located
|
||||
* in packages/language-service/test/project. Project creation exercises the
|
||||
* actual code path, but a mock host is used for the filesystem to intercept
|
||||
* and modify test files.
|
||||
*/
|
||||
export function setup() {
|
||||
const projectService = new ts.server.ProjectService({
|
||||
host,
|
||||
logger,
|
||||
cancellationToken: ts.server.nullCancellationToken,
|
||||
useSingleInferredProject: true,
|
||||
useInferredProjectPerProjectRoot: true,
|
||||
typingsInstaller: ts.server.nullTypingsInstaller,
|
||||
});
|
||||
// Opening APP_COMPONENT forces a new ConfiguredProject to be created based
|
||||
// on the tsconfig.json in the test project.
|
||||
projectService.openClientFile(APP_COMPONENT);
|
||||
const project = projectService.findProject(TSCONFIG);
|
||||
if (!project) {
|
||||
throw new Error(`Failed to create project for ${TSCONFIG}`);
|
||||
}
|
||||
// The following operation forces a ts.Program to be created.
|
||||
const tsLS = project.getLanguageService();
|
||||
return {projectService, project, tsLS};
|
||||
}
|
47
packages/language-service/ivy/test/mock_host_spec.ts
Normal file
47
packages/language-service/ivy/test/mock_host_spec.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @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 * as ts from 'typescript/lib/tsserverlibrary';
|
||||
|
||||
import {APP_MAIN, setup, TEST_SRCDIR} from './mock_host';
|
||||
|
||||
describe('mock host', () => {
|
||||
it('can load test project from Bazel runfiles', () => {
|
||||
const {project, tsLS} = setup();
|
||||
expect(project).toBeInstanceOf(ts.server.ConfiguredProject);
|
||||
const program = tsLS.getProgram();
|
||||
expect(program).toBeDefined();
|
||||
const sourceFiles = program!.getSourceFiles().map(sf => {
|
||||
const {fileName} = sf;
|
||||
if (fileName.startsWith(TEST_SRCDIR)) {
|
||||
return fileName.substring(TEST_SRCDIR.length);
|
||||
}
|
||||
return fileName;
|
||||
});
|
||||
expect(sourceFiles).toEqual(jasmine.arrayContaining([
|
||||
// This shows that module resolution works
|
||||
'/angular/packages/common/src/common.d.ts',
|
||||
'/angular/packages/core/src/core.d.ts',
|
||||
'/angular/packages/forms/src/forms.d.ts',
|
||||
// This shows that project files are present
|
||||
'/angular/packages/language-service/test/project/app/app.component.ts',
|
||||
'/angular/packages/language-service/test/project/app/main.ts',
|
||||
'/angular/packages/language-service/test/project/app/parsing-cases.ts',
|
||||
]));
|
||||
});
|
||||
|
||||
it('produces no TS error for test project', () => {
|
||||
const {project, tsLS} = setup();
|
||||
const errors = project.getAllProjectErrors();
|
||||
expect(errors).toEqual([]);
|
||||
const globalErrors = project.getGlobalProjectErrors();
|
||||
expect(globalErrors).toEqual([]);
|
||||
const diags = tsLS.getSemanticDiagnostics(APP_MAIN);
|
||||
expect(diags).toEqual([]);
|
||||
});
|
||||
});
|
@ -1,12 +1,18 @@
|
||||
{
|
||||
"//00": "This file is used for IDE only, actual compilation options is in MockTypescriptHost in test_utils.ts",
|
||||
"//00": "This file is used for both IDE and the actual Project creation in Ivy language service for testing purpose",
|
||||
"compilerOptions": {
|
||||
"lib": ["es2015"],
|
||||
"lib": ["es2015", "dom"],
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es2015",
|
||||
"baseUrl": "../../../..",
|
||||
"paths": {
|
||||
"@angular/*": ["packages/*"]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user