mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
470cf430ce
Optimization Goals:
The primary goals of this optimization are to dramatically reduce the execution time of the language-service test suite and stabilize the mock file system infrastructure. Previously, the suite suffered from significant overhead due to recreating the `MockFileSystem`, `MockServerHost`, and TypeScript `ProjectService` for every single test block, leading to redundant parsing operations, slow test initialization, and reduced spec performance.
How the Goals Were Achieved:
1. **Mock File System Optimizations (Shared State)**:
- Evaluated that standard test files (e.g., TS/Angular lib definitions) are immutable across tests.
- Introduced and utilized `lockMockFileSystem()` to initialize the mock `FileSystem` with `loadStandardTestFiles()` only once per test suite run rather than repeatedly per test.
- Refactored `LanguageServiceTestEnv.setup()` to reuse the singleton file system, completely skipping redundant module loading by eagerly flagging `fsInitialized = true`.
2. **Language Service Test Environment Enhancements (TypeScript Project Reuse)**:
- Implemented partial configuration reloads in the `Project` class via the `update()` method, removing the need to tear down and rebuild the entire `MockServerHost` and TypeScript `ProjectService` from scratch when minimal file changes (like HTML templates or local TS edits) are made dynamically by a test.
- Applied `projectService.reloadProjects()` and `scriptInfo.reloadFromFile()` to synchronously push mock file tree invalidations to the active TS program, skipping expensive environment initialization and saving considerable latency across tests.
- Added `projectName` identifiers inside complex isolate tests (e.g. module alias aliasing) so custom environment injections can sandbox safely without invalidating the global default environment cache.
3. **Test Suite Unification**:
- Flattened fragmented test groups (`grp1`, `grp3`, `grp4`) into a cohesive single directory at `packages/language-service/test/`. This simplifies execution config, improves test runner concurrency, and unifies local development targeting.
- Cleaned out broken inline debug logging and unneeded config reloading loops.
4. **Maintaining Test Isolation**:
- **Explicit TypeScript Configuration**: While the underlying `MockFileSystem` ("disk") is aggressively reused across tests, the TypeScript `ProjectService` and its execution environment are entirely recreated for every test run to ensure isolated ASTs and module resolution caches.
- **Strict tsconfig.json Files Array**: When a project is initialized, it explicitly defines its boundary using the strict `files: [ ... ]` array in `tsconfig.json`. This ensures that any leftover files physically on the mock disk from an older test run are completely invisible to the TS Compiler.
- **Namespace Sandboxing**: For tests doing custom modifications (e.g., overriding module resolution paths), they utilize localized `projectName` arguments (like `"test_alias_completions"`) to configure sandboxed working directories.
235 lines
8.0 KiB
TypeScript
235 lines
8.0 KiB
TypeScript
/**
|
|
* @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.dev/license
|
|
*/
|
|
|
|
import ts from 'typescript';
|
|
|
|
import {
|
|
addElementToArrayLiteral,
|
|
collectMemberMethods,
|
|
ensureArrayWithIdentifier,
|
|
findTightestNode,
|
|
generateImport,
|
|
nonCollidingImportName,
|
|
objectPropertyAssignmentForKey,
|
|
updateImport,
|
|
updateObjectValueForKey,
|
|
} from '../src/utils/ts_utils';
|
|
import {LanguageServiceTestEnv, OpenBuffer, Project} from '../testing';
|
|
|
|
describe('TS util', () => {
|
|
describe('collectMemberMethods', () => {
|
|
it('gets only methods in class, not getters, setters, or properties', () => {
|
|
const files = {
|
|
'app.ts': `
|
|
export class AppCmp {
|
|
prop!: string;
|
|
get myString(): string {
|
|
return '';
|
|
}
|
|
set myString(v: string) {
|
|
}
|
|
|
|
one() {}
|
|
two() {}
|
|
}`,
|
|
};
|
|
const env = LanguageServiceTestEnv.setup();
|
|
const project = env.addProject('test', files);
|
|
const appFile = project.openFile('app.ts');
|
|
appFile.moveCursorToText('AppC¦mp');
|
|
const memberMethods = getMemberMethodNames(project, appFile);
|
|
expect(memberMethods).toEqual(['one', 'two']);
|
|
});
|
|
|
|
it('gets inherited methods in class', () => {
|
|
const files = {
|
|
'app.ts': `
|
|
export class BaseClass {
|
|
baseMethod() {}
|
|
}
|
|
export class AppCmp extends BaseClass {}`,
|
|
};
|
|
const env = LanguageServiceTestEnv.setup();
|
|
const project = env.addProject('test', files);
|
|
const appFile = project.openFile('app.ts');
|
|
appFile.moveCursorToText('AppC¦mp');
|
|
const memberMethods = getMemberMethodNames(project, appFile);
|
|
expect(memberMethods).toEqual(['baseMethod']);
|
|
});
|
|
|
|
it('does not return duplicates if base method is overridden', () => {
|
|
const files = {
|
|
'app.ts': `
|
|
export class BaseClass {
|
|
baseMethod() {}
|
|
}
|
|
export class AppCmp extends BaseClass {
|
|
baseMethod() {}
|
|
}`,
|
|
};
|
|
const env = LanguageServiceTestEnv.setup();
|
|
const project = env.addProject('test', files);
|
|
const appFile = project.openFile('app.ts');
|
|
appFile.moveCursorToText('AppC¦mp');
|
|
const memberMethods = getMemberMethodNames(project, appFile);
|
|
expect(memberMethods).toEqual(['baseMethod']);
|
|
});
|
|
|
|
function getMemberMethodNames(project: Project, file: OpenBuffer): string[] {
|
|
const sf = project.getSourceFile('app.ts')!;
|
|
const node = findTightestNode(sf, file.cursor)!;
|
|
expect(ts.isClassDeclaration(node.parent)).toBe(true);
|
|
return collectMemberMethods(node.parent as ts.ClassDeclaration, project.getTypeChecker())
|
|
.map((m) => m.name.getText())
|
|
.sort();
|
|
}
|
|
});
|
|
|
|
describe('AST method', () => {
|
|
let printer: ts.Printer;
|
|
let sourceFile: ts.SourceFile;
|
|
|
|
function print(node: ts.Node): string {
|
|
return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
|
}
|
|
|
|
beforeAll(() => {
|
|
printer = ts.createPrinter();
|
|
sourceFile = ts.createSourceFile(
|
|
'placeholder.ts',
|
|
'',
|
|
ts.ScriptTarget.ESNext,
|
|
true,
|
|
ts.ScriptKind.TS,
|
|
);
|
|
});
|
|
|
|
describe('addElementToArrayLiteral', () => {
|
|
it('transforms an empty array literal expression', () => {
|
|
const oldArr = ts.factory.createArrayLiteralExpression([], false);
|
|
const newArr = addElementToArrayLiteral(oldArr, ts.factory.createStringLiteral('a'));
|
|
expect(print(newArr)).toEqual('["a"]');
|
|
});
|
|
|
|
it('transforms an existing array literal expression', () => {
|
|
const oldArr = ts.factory.createArrayLiteralExpression(
|
|
[ts.factory.createStringLiteral('a')],
|
|
false,
|
|
);
|
|
const newArr = addElementToArrayLiteral(oldArr, ts.factory.createStringLiteral('b'));
|
|
expect(print(newArr)).toEqual('["a", "b"]');
|
|
});
|
|
|
|
it('addElementToArrayLiteral', () => {
|
|
let arr = ensureArrayWithIdentifier('foo', ts.factory.createIdentifier('foo'));
|
|
arr = addElementToArrayLiteral(arr!, ts.factory.createIdentifier('bar'));
|
|
expect(print(arr)).toEqual('[foo, bar]');
|
|
});
|
|
});
|
|
|
|
describe('objectPropertyAssignmentForKey', () => {
|
|
let oldObj: ts.ObjectLiteralExpression;
|
|
|
|
beforeEach(() => {
|
|
oldObj = ts.factory.createObjectLiteralExpression(
|
|
[
|
|
ts.factory.createPropertyAssignment(
|
|
ts.factory.createIdentifier('foo'),
|
|
ts.factory.createStringLiteral('bar'),
|
|
),
|
|
],
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('returns null when no property exists', () => {
|
|
const prop = objectPropertyAssignmentForKey(oldObj, 'oops');
|
|
expect(prop).toBeNull();
|
|
});
|
|
|
|
it('returns the requested property assignment', () => {
|
|
const prop = objectPropertyAssignmentForKey(oldObj, 'foo');
|
|
expect(print(prop!)).toEqual('foo: "bar"');
|
|
});
|
|
});
|
|
|
|
describe('updateObjectValueForKey', () => {
|
|
let oldObj: ts.ObjectLiteralExpression;
|
|
|
|
const valueAppenderFn = (oldValue?: ts.Expression) => {
|
|
if (!oldValue) return ts.factory.createStringLiteral('baz');
|
|
if (!ts.isStringLiteral(oldValue)) return oldValue;
|
|
return ts.factory.createStringLiteral(oldValue.text + 'baz');
|
|
};
|
|
|
|
beforeEach(() => {
|
|
oldObj = ts.factory.createObjectLiteralExpression(
|
|
[
|
|
ts.factory.createPropertyAssignment(
|
|
ts.factory.createIdentifier('foo'),
|
|
ts.factory.createStringLiteral('bar'),
|
|
),
|
|
],
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('creates a non-existent property', () => {
|
|
const obj = updateObjectValueForKey(oldObj, 'newKey', valueAppenderFn);
|
|
expect(print(obj)).toBe('newKey: "baz"');
|
|
});
|
|
|
|
it('updates an existing property', () => {
|
|
const obj = updateObjectValueForKey(oldObj, 'foo', valueAppenderFn);
|
|
expect(print(obj)).toBe('foo: "barbaz"');
|
|
});
|
|
});
|
|
|
|
it('ensureArrayWithIdentifier', () => {
|
|
let arr = ensureArrayWithIdentifier('foo', ts.factory.createIdentifier('foo'));
|
|
expect(print(arr!)).toEqual('[foo]');
|
|
arr = ensureArrayWithIdentifier('bar', ts.factory.createIdentifier('bar'), arr!);
|
|
expect(print(arr!)).toEqual('[foo, bar]');
|
|
arr = ensureArrayWithIdentifier('bar', ts.factory.createIdentifier('bar'), arr!);
|
|
expect(arr).toEqual(null);
|
|
});
|
|
|
|
it('generateImport', () => {
|
|
let imp = generateImport('Foo', null, './foo');
|
|
expect(print(imp)).toEqual(`import { Foo } from "./foo";`);
|
|
imp = generateImport('Foo', 'Bar', './foo');
|
|
expect(print(imp)).toEqual(`import { Bar as Foo } from "./foo";`);
|
|
});
|
|
|
|
it('updateImport', () => {
|
|
let imp = generateImport('Foo', null, './foo');
|
|
let namedImp = updateImport(imp, 'Bar', null);
|
|
expect(print(namedImp!)).toEqual(`{ Foo, Bar }`);
|
|
namedImp = updateImport(imp, 'Foo_2', 'Foo');
|
|
expect(print(namedImp!)).toEqual(`{ Foo, Foo as Foo_2 }`);
|
|
namedImp = updateImport(imp, 'Bar', 'Bar');
|
|
expect(print(namedImp!)).toEqual(`{ Foo, Bar }`);
|
|
namedImp = updateImport(imp, 'default', 'Baz');
|
|
expect(print(namedImp!)).toEqual(`Baz, { Foo }`);
|
|
imp = generateImport('default', 'Foo', './foo');
|
|
namedImp = updateImport(imp, 'Bar', null);
|
|
expect(print(namedImp!)).toEqual(`Foo, { Bar }`);
|
|
});
|
|
|
|
it('nonCollidingImportName', () => {
|
|
let imps = [
|
|
generateImport('Foo', null, './foo'),
|
|
generateImport('Bar', 'ExternalBar', './bar'),
|
|
];
|
|
expect(nonCollidingImportName(imps, 'Other')).toEqual('Other');
|
|
expect(nonCollidingImportName(imps, 'Foo')).toEqual('Foo_1');
|
|
expect(nonCollidingImportName(imps, 'ExternalBar')).toEqual('ExternalBar');
|
|
});
|
|
});
|
|
});
|