mirror of
https://github.com/microsoft/pyright.git
synced 2024-10-03 19:37:39 +03:00
Add test(s) that validate Pyright can talk over LSP (#7172)
* Everything building, but not running * More tests passing * Fix test to open a file * Remove unused functions and consolidate others * Remove unused custom lsp messages * Add comments
This commit is contained in:
parent
7585378936
commit
e01b0fe205
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@ -84,6 +84,9 @@
|
||||
"args": [
|
||||
"${fileBasenameNoExtension}",
|
||||
"--runInBand",
|
||||
"--detectOpenHandles",
|
||||
"--forceExit",
|
||||
"--testTimeout=180000"
|
||||
],
|
||||
"cwd": "${workspaceRoot}/packages/pyright-internal",
|
||||
"console": "integratedTerminal",
|
||||
@ -94,7 +97,7 @@
|
||||
"name": "Pyright jest selected test",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": ["${fileBasenameNoExtension}", "--runInBand", "-t", "${selectedText}"],
|
||||
"args": ["${fileBasenameNoExtension}", "--runInBand", "-t", "${selectedText}", "--forceExit", "--testTimeout=180000"],
|
||||
"cwd": "${workspaceRoot}/packages/pyright-internal",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
|
34
.vscode/tasks.json
vendored
34
.vscode/tasks.json
vendored
@ -40,6 +40,40 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Watch test server",
|
||||
"type": "npm",
|
||||
"script": "watch:testserver",
|
||||
"isBackground": true,
|
||||
// From vscode-tsl-problem-matcher.
|
||||
"problemMatcher": {
|
||||
"owner": "custom",
|
||||
"fileLocation": "absolute",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "\\[tsl\\] (ERROR|WARNING) in (.*)?\\((\\d+),(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"line": 3,
|
||||
"column": 4
|
||||
},
|
||||
{
|
||||
"regexp": "\\s*TS(\\d+):\\s*(.*)$",
|
||||
"code": 1,
|
||||
"message": 2
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "PublicPath: .*?"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "webpack compiled in .*? ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
"build:extension:dev": "cd packages/vscode-pyright && npm run webpack",
|
||||
"build:cli:dev": "cd packages/pyright && npm run webpack",
|
||||
"watch:extension": "cd packages/vscode-pyright && npm run webpack-dev",
|
||||
"watch:testserver": "cd packages/pyright-internal && npm run webpack:testserver:watch",
|
||||
"check": "npm run check:syncpack && npm run check:eslint && npm run check:prettier",
|
||||
"check:syncpack": "syncpack list-mismatches",
|
||||
"fix:syncpack": "syncpack fix-mismatches --indent \" \" && npm run install:all",
|
||||
|
2921
packages/pyright-internal/package-lock.json
generated
2921
packages/pyright-internal/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,10 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "shx rm -rf ./dist ./out",
|
||||
"test": "jest --forceExit",
|
||||
"webpack:testserver": "webpack --config ./src/tests/lsp/webpack.testserver.config.js --mode=development",
|
||||
"webpack:testserver:watch": "npm run clean && webpack --config ./src/tests/lsp/webpack.testserver.config.js --mode development --watch --progress",
|
||||
"test": "npm run webpack:testserver && jest --forceExit",
|
||||
"test:norebuild": "jest --forceExit",
|
||||
"test:coverage": "jest --forceExit --reporters=jest-junit --reporters=default --coverage --coverageReporters=cobertura --coverageReporters=html --coverageReporters=json",
|
||||
"test:imports": "jest importResolver.test --forceExit --runInBand"
|
||||
},
|
||||
@ -39,11 +42,16 @@
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "^17.0.45",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"esbuild-loader": "^3.0.1",
|
||||
"jest": "^29.6.1",
|
||||
"jest-junit": "^16.0.0",
|
||||
"shx": "^0.3.4",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"typescript": "~5.2",
|
||||
"webpack": "^5.88.1",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"word-wrap": "1.2.4"
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import { ConsoleWithLogLevel, LogLevel, convertLogLevel } from './common/console
|
||||
import { isDebugMode, isString } from './common/core';
|
||||
import { expandPathVariables } from './common/envVarUtils';
|
||||
import { FileBasedCancellationProvider } from './common/fileBasedCancellationUtils';
|
||||
import { FileSystem } from './common/fileSystem';
|
||||
import { FullAccessHost } from './common/fullAccessHost';
|
||||
import { Host } from './common/host';
|
||||
import { ProgressReporter } from './common/progressReporter';
|
||||
@ -46,13 +47,13 @@ const maxAnalysisTimeInForeground = { openFilesTimeInMs: 50, noOpenFilesTimeInMs
|
||||
export class PyrightServer extends LanguageServerBase {
|
||||
private _controller: CommandController;
|
||||
|
||||
constructor(connection: Connection) {
|
||||
constructor(connection: Connection, realFileSystem?: FileSystem) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const version = require('../package.json').version || '';
|
||||
|
||||
const console = new ConsoleWithLogLevel(connection.console);
|
||||
const fileWatcherProvider = new WorkspaceFileWatcherProvider();
|
||||
const fileSystem = createFromRealFileSystem(console, fileWatcherProvider);
|
||||
const fileSystem = realFileSystem ?? createFromRealFileSystem(console, fileWatcherProvider);
|
||||
const pyrightFs = new PyrightFileSystem(fileSystem);
|
||||
const tempFile = new RealTempFile(pyrightFs.isCaseSensitive);
|
||||
const cacheManager = new CacheManager();
|
||||
|
193
packages/pyright-internal/src/tests/languageServer.test.ts
Normal file
193
packages/pyright-internal/src/tests/languageServer.test.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/*
|
||||
* languageServer.test.ts
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT license.
|
||||
*
|
||||
* Tests to verify Pyright works as the backend for a language server.
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import {
|
||||
CancellationToken,
|
||||
CompletionRequest,
|
||||
ConfigurationItem,
|
||||
InitializedNotification,
|
||||
InitializeRequest,
|
||||
MarkupContent,
|
||||
} from 'vscode-languageserver';
|
||||
|
||||
import { convertOffsetToPosition } from '../common/positionUtils';
|
||||
import { PythonVersion } from '../common/pythonVersion';
|
||||
|
||||
import { isArray } from '../common/core';
|
||||
import { normalizeSlashes } from '../common/pathUtils';
|
||||
import {
|
||||
cleanupAfterAll,
|
||||
DEFAULT_WORKSPACE_ROOT,
|
||||
getParseResults,
|
||||
hover,
|
||||
initializeLanguageServer,
|
||||
openFile,
|
||||
PyrightServerInfo,
|
||||
runPyrightServer,
|
||||
waitForDiagnostics,
|
||||
} from './lsp/languageServerTestUtils';
|
||||
|
||||
describe(`Basic language server tests`, () => {
|
||||
let serverInfo: PyrightServerInfo | undefined;
|
||||
async function runLanguageServer(
|
||||
projectRoots: string[] | string,
|
||||
code: string,
|
||||
callInitialize = true,
|
||||
extraSettings?: { item: ConfigurationItem; value: any }[],
|
||||
pythonVersion: PythonVersion = PythonVersion.V3_10,
|
||||
supportsBackgroundThread?: boolean
|
||||
) {
|
||||
const result = await runPyrightServer(
|
||||
projectRoots,
|
||||
code,
|
||||
callInitialize,
|
||||
extraSettings,
|
||||
pythonVersion,
|
||||
supportsBackgroundThread
|
||||
);
|
||||
serverInfo = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
if (serverInfo) {
|
||||
await serverInfo.dispose();
|
||||
serverInfo = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupAfterAll();
|
||||
});
|
||||
|
||||
test('Basic Initialize', async () => {
|
||||
const code = `
|
||||
// @filename: test.py
|
||||
//// # empty file
|
||||
`;
|
||||
const serverInfo = await runLanguageServer(DEFAULT_WORKSPACE_ROOT, code, /* callInitialize */ false);
|
||||
|
||||
const initializeResult = await initializeLanguageServer(serverInfo);
|
||||
|
||||
assert(initializeResult);
|
||||
assert(initializeResult.capabilities.completionProvider?.resolveProvider);
|
||||
});
|
||||
|
||||
test('Initialize without workspace folder support', async () => {
|
||||
const code = `
|
||||
// @filename: test.py
|
||||
//// import [|/*marker*/os|]
|
||||
`;
|
||||
const info = await runLanguageServer(DEFAULT_WORKSPACE_ROOT, code, /* callInitialize */ false);
|
||||
|
||||
// This will test clients with no folder and configuration support.
|
||||
const params = info.getInitializeParams();
|
||||
params.capabilities.workspace!.workspaceFolders = false;
|
||||
params.capabilities.workspace!.configuration = false;
|
||||
|
||||
// Perform LSP Initialize/Initialized handshake.
|
||||
const result = await info.connection.sendRequest(InitializeRequest.type, params, CancellationToken.None);
|
||||
assert(result);
|
||||
|
||||
await info.connection.sendNotification(InitializedNotification.type, {});
|
||||
|
||||
// Do simple hover request to verify our server works with a client that doesn't support
|
||||
// workspace folder/configuration capabilities.
|
||||
openFile(info, 'marker');
|
||||
const hoverResult = await hover(info, 'marker');
|
||||
assert(hoverResult);
|
||||
assert(MarkupContent.is(hoverResult.contents));
|
||||
assert.strictEqual(hoverResult.contents.value, '```python\n(module) os\n```');
|
||||
});
|
||||
test('Hover', async () => {
|
||||
const code = `
|
||||
// @filename: test.py
|
||||
//// import [|/*marker*/os|]
|
||||
`;
|
||||
const info = await runLanguageServer(DEFAULT_WORKSPACE_ROOT, code, /* callInitialize */ true);
|
||||
|
||||
// Do simple hover request
|
||||
openFile(info, 'marker');
|
||||
const hoverResult = await hover(info, 'marker');
|
||||
assert(hoverResult);
|
||||
assert(MarkupContent.is(hoverResult.contents));
|
||||
assert.strictEqual(hoverResult.contents.value, '```python\n(module) os\n```');
|
||||
});
|
||||
test('Completions', async () => {
|
||||
const code = `
|
||||
// @filename: test.py
|
||||
//// import os
|
||||
//// os.[|/*marker*/|]
|
||||
`;
|
||||
const info = await runLanguageServer(DEFAULT_WORKSPACE_ROOT, code, /* callInitialize */ true);
|
||||
|
||||
// Do simple completion request
|
||||
openFile(info, 'marker');
|
||||
const marker = info.testData.markerPositions.get('marker')!;
|
||||
const fileUri = marker.fileUri;
|
||||
const text = info.testData.files.find((d) => d.fileName === marker.fileName)!.content;
|
||||
const parseResult = getParseResults(text);
|
||||
const completionResult = await info.connection.sendRequest(
|
||||
CompletionRequest.type,
|
||||
{
|
||||
textDocument: { uri: fileUri.toString() },
|
||||
position: convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines),
|
||||
},
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
assert(completionResult);
|
||||
assert(!isArray(completionResult));
|
||||
|
||||
const completionItem = completionResult.items.find((i) => i.label === 'path')!;
|
||||
assert(completionItem);
|
||||
});
|
||||
|
||||
test('background thread diagnostics', async () => {
|
||||
const code = `
|
||||
// @filename: root/test.py
|
||||
//// from math import cos, sin
|
||||
//// import sys
|
||||
//// [|/*marker*/|]
|
||||
`;
|
||||
const settings = [
|
||||
{
|
||||
item: {
|
||||
scopeUri: `file://${normalizeSlashes(DEFAULT_WORKSPACE_ROOT, '/')}`,
|
||||
section: 'python.analysis',
|
||||
},
|
||||
value: {
|
||||
typeCheckingMode: 'strict',
|
||||
diagnosticMode: 'workspace',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const info = await runLanguageServer(
|
||||
DEFAULT_WORKSPACE_ROOT,
|
||||
code,
|
||||
/* callInitialize */ true,
|
||||
settings,
|
||||
undefined,
|
||||
/* supportsBackgroundThread */ true
|
||||
);
|
||||
|
||||
// get the file containing the marker that also contains our task list comments
|
||||
await openFile(info, 'marker');
|
||||
|
||||
// Wait for the diagnostics to publish
|
||||
const diagnostics = await waitForDiagnostics(info);
|
||||
assert.equal(diagnostics[0]!.diagnostics.length, 6);
|
||||
|
||||
// Make sure the error has a special rule
|
||||
assert.equal(diagnostics[0].diagnostics[1].code, 'reportUnusedImport');
|
||||
assert.equal(diagnostics[0].diagnostics[3].code, 'reportUnusedImport');
|
||||
assert.equal(diagnostics[0].diagnostics[5].code, 'reportUnusedImport');
|
||||
});
|
||||
});
|
170
packages/pyright-internal/src/tests/lsp/customLsp.ts
Normal file
170
packages/pyright-internal/src/tests/lsp/customLsp.ts
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* customLsp.ts
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT license.
|
||||
*
|
||||
* Custom messages and notifications on top of the LSP used for testing.
|
||||
*/
|
||||
|
||||
import {
|
||||
CancellationToken,
|
||||
DidChangeConfigurationParams,
|
||||
DidChangeNotebookDocumentParams,
|
||||
Disposable,
|
||||
NotificationHandler,
|
||||
RequestHandler,
|
||||
} from 'vscode-languageserver-protocol';
|
||||
|
||||
import { Uri } from '../../common/uri/uri';
|
||||
|
||||
export interface RequestSender {
|
||||
sendRequest<R>(method: string, params: any, token?: CancellationToken): Promise<R>;
|
||||
}
|
||||
|
||||
export interface NotificationSender {
|
||||
sendNotification: (method: string, params?: any) => void;
|
||||
}
|
||||
|
||||
export interface RequestReceiver {
|
||||
onRequest<P, R, E>(method: string, handler: RequestHandler<P, R, E>): Disposable;
|
||||
}
|
||||
|
||||
export interface NotificationReceiver {
|
||||
onNotification<P>(method: string, handler: NotificationHandler<P>): Disposable;
|
||||
}
|
||||
|
||||
export interface WorkspaceInfo {
|
||||
rootUri: Uri;
|
||||
kinds: string[];
|
||||
pythonPath: Uri | undefined;
|
||||
pythonPathKind: string;
|
||||
}
|
||||
|
||||
// Type-safe LSP wrappers for our custom calls.
|
||||
export namespace CustomLSP {
|
||||
export enum TestSignalKinds {
|
||||
Initialization = 'initialization',
|
||||
DidOpenDocument = 'didopendocument',
|
||||
DidChangeDocument = 'didchangedocument',
|
||||
}
|
||||
|
||||
export interface TestSignal {
|
||||
uri: string;
|
||||
kind: TestSignalKinds;
|
||||
}
|
||||
|
||||
export enum Requests {
|
||||
GetDiagnostics = 'test/getDiagnostics',
|
||||
}
|
||||
|
||||
export enum Notifications {
|
||||
SetStatusBarMessage = 'python/setStatusBarMessage',
|
||||
BeginProgress = 'python/beginProgress',
|
||||
ReportProgress = 'python/reportProgress',
|
||||
EndProgress = 'python/endProgress',
|
||||
WorkspaceTrusted = 'python/workspaceTrusted',
|
||||
TestSignal = 'test/signal',
|
||||
|
||||
// Due to some restrictions on vscode-languageserver-node package,
|
||||
// we can't mix use types from the package in 2 different extensions.
|
||||
// Basically due to how lsp package utilizes singleton objects internally,
|
||||
// if we use a client created from python core extension, which uses LSP library
|
||||
// they imported, with LSP types from LSP library we imported, LSP will throw
|
||||
// an exception saying internal singleton objects are not same.
|
||||
//
|
||||
// To workaround it, we won't use some of LSP types directly but create our own
|
||||
// and use them with the client.
|
||||
DidChangeConfiguration = 'workspace/didChangeConfiguration',
|
||||
DidChangeNotebookDocument = 'notebookDocument/didChange',
|
||||
CacheDirCreate = 'python/cacheDirCreate',
|
||||
CacheFileWrite = 'python/cacheFileWrite',
|
||||
// Starting/stopping the server are all notifications so they pass
|
||||
// through without any interference.
|
||||
TestStartServer = 'test/startServer',
|
||||
TestStartServerResponse = 'test/startServerResponse',
|
||||
}
|
||||
|
||||
interface Params {
|
||||
[Requests.GetDiagnostics]: { uri: string };
|
||||
[Notifications.CacheDirCreate]: { uri: string };
|
||||
[Notifications.CacheFileWrite]: { uri: string; contents: string; overwrite: boolean };
|
||||
[Notifications.SetStatusBarMessage]: string;
|
||||
[Notifications.BeginProgress]: undefined;
|
||||
[Notifications.ReportProgress]: string;
|
||||
[Notifications.EndProgress]: undefined;
|
||||
[Notifications.WorkspaceTrusted]: { isTrusted: boolean };
|
||||
[Notifications.TestSignal]: TestSignal;
|
||||
[Notifications.DidChangeConfiguration]: DidChangeConfigurationParams;
|
||||
[Notifications.DidChangeNotebookDocument]: DidChangeNotebookDocumentParams;
|
||||
[Notifications.TestStartServer]: TestServerStartOptions;
|
||||
[Notifications.TestStartServerResponse]: { testName: string };
|
||||
}
|
||||
|
||||
interface Response {
|
||||
[Requests.GetDiagnostics]: { diagnostics: string };
|
||||
}
|
||||
|
||||
// Interface for returning config options as we cannot return a
|
||||
// class instance from the server.
|
||||
export interface IFileSpec {
|
||||
wildcardRoot: Uri;
|
||||
regExp: string;
|
||||
hasDirectoryWildcard: boolean;
|
||||
}
|
||||
export interface IConfigOptions {
|
||||
projectRoot: Uri;
|
||||
pythonPath?: Uri;
|
||||
typeshedPath?: Uri;
|
||||
include: IFileSpec[];
|
||||
exclude: IFileSpec[];
|
||||
ignore: IFileSpec[];
|
||||
strict: IFileSpec[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data passed to the server worker thread in order to setup
|
||||
* a test server.
|
||||
*/
|
||||
export interface TestServerStartOptions {
|
||||
testName: string; // Helpful for debugging
|
||||
pid: string; // Helpful for debugging
|
||||
logFile: Uri; // Helpful for debugging
|
||||
code: string; // Fourslash data.
|
||||
projectRoots: Uri[];
|
||||
pythonVersion: number;
|
||||
backgroundAnalysis?: boolean;
|
||||
}
|
||||
|
||||
export function sendRequest<P extends Params, R extends Response, M extends Requests & keyof P & keyof R & string>(
|
||||
connection: RequestSender,
|
||||
method: M,
|
||||
params: P[M],
|
||||
token?: CancellationToken
|
||||
): Promise<R[M]> {
|
||||
return connection.sendRequest(method, params, token);
|
||||
}
|
||||
|
||||
export function sendNotification<P extends Params, M extends Notifications & keyof P & string>(
|
||||
connection: NotificationSender,
|
||||
method: M,
|
||||
params: P[M]
|
||||
): void {
|
||||
connection.sendNotification(method, params);
|
||||
}
|
||||
|
||||
export function onRequest<P extends Params, R extends Response, M extends Requests & keyof P & keyof R & string, E>(
|
||||
connection: RequestReceiver,
|
||||
method: M,
|
||||
handler: RequestHandler<P[M], R[M], E>
|
||||
): Disposable {
|
||||
return connection.onRequest(method, handler);
|
||||
}
|
||||
|
||||
export function onNotification<P extends Params, M extends Notifications & keyof P & string>(
|
||||
connection: NotificationReceiver,
|
||||
method: M,
|
||||
handler: NotificationHandler<P[M]>
|
||||
): Disposable {
|
||||
return connection.onNotification(method, handler);
|
||||
}
|
||||
}
|
385
packages/pyright-internal/src/tests/lsp/languageServer.ts
Normal file
385
packages/pyright-internal/src/tests/lsp/languageServer.ts
Normal file
@ -0,0 +1,385 @@
|
||||
/*
|
||||
* languageServer.ts
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT license.
|
||||
*
|
||||
* Test language server wrapper that lets us run the language server during a test.
|
||||
*/
|
||||
import {
|
||||
CancellationToken,
|
||||
Connection,
|
||||
Disposable,
|
||||
Message,
|
||||
MessageReader,
|
||||
MessageWriter,
|
||||
PortMessageReader,
|
||||
PortMessageWriter,
|
||||
ShutdownRequest,
|
||||
createConnection,
|
||||
} from 'vscode-languageserver/node';
|
||||
import { MessagePort, getEnvironmentData, parentPort, setEnvironmentData } from 'worker_threads';
|
||||
|
||||
import { Deferred, createDeferred } from '../../common/deferred';
|
||||
import { FileSystemEntries, resolvePaths } from '../../common/pathUtils';
|
||||
import { ServiceProvider } from '../../common/serviceProvider';
|
||||
import { Uri } from '../../common/uri/uri';
|
||||
import { parseTestData } from '../harness/fourslash/fourSlashParser';
|
||||
import * as PyrightTestHost from '../harness/testHost';
|
||||
import { clearCache } from '../harness/vfs/factory';
|
||||
|
||||
import { BackgroundAnalysis, BackgroundAnalysisRunner } from '../../backgroundAnalysis';
|
||||
import { BackgroundAnalysisBase } from '../../backgroundAnalysisBase';
|
||||
import { serialize } from '../../backgroundThreadBase';
|
||||
import { FileSystem } from '../../common/fileSystem';
|
||||
import { ServiceKeys } from '../../common/serviceProviderExtensions';
|
||||
import { ServerSettings } from '../../languageServerBase';
|
||||
import { PyrightFileSystem } from '../../pyrightFileSystem';
|
||||
import { PyrightServer } from '../../server';
|
||||
import { InitStatus, Workspace } from '../../workspaceFactory';
|
||||
import { CustomLSP } from './customLsp';
|
||||
import {
|
||||
DEFAULT_WORKSPACE_ROOT,
|
||||
TestHost,
|
||||
TestHostOptions,
|
||||
createFileSystem,
|
||||
getFileLikePath,
|
||||
logToDisk,
|
||||
sleep,
|
||||
} from './languageServerTestUtils';
|
||||
|
||||
const WORKER_STARTED = 'WORKER_STARTED';
|
||||
const WORKER_BACKGROUND_DATA = 'WORKER_BACKGROUND_DATA';
|
||||
|
||||
function getCommonRoot(files: Uri[]) {
|
||||
let root = files[0]?.getPath() || DEFAULT_WORKSPACE_ROOT;
|
||||
for (let i = 1; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
while (root.length > 0 && !file.pathStartsWith(root)) {
|
||||
root = root.slice(0, root.lastIndexOf('/'));
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
class TestPyrightHost implements PyrightTestHost.TestHost {
|
||||
constructor(private _host: PyrightTestHost.TestHost) {}
|
||||
useCaseSensitiveFileNames(): boolean {
|
||||
return this._host.useCaseSensitiveFileNames();
|
||||
}
|
||||
getAccessibleFileSystemEntries(dirname: string): FileSystemEntries {
|
||||
return this._host.getAccessibleFileSystemEntries(dirname);
|
||||
}
|
||||
directoryExists(path: string): boolean {
|
||||
return this._host.directoryExists(path);
|
||||
}
|
||||
fileExists(fileName: string): boolean {
|
||||
return this._host.fileExists(fileName);
|
||||
}
|
||||
getFileSize(path: string): number {
|
||||
return this._host.getFileSize(path);
|
||||
}
|
||||
readFile(path: string): string | undefined {
|
||||
return this._host.readFile(path);
|
||||
}
|
||||
getWorkspaceRoot(): string {
|
||||
// The default workspace root is wrong. It should be based on where the bundle is running.
|
||||
// That's where the typeshed fallback and other bundled files are located.
|
||||
return resolvePaths(__dirname);
|
||||
}
|
||||
writeFile(path: string, contents: string): void {
|
||||
this._host.writeFile(path, contents);
|
||||
}
|
||||
listFiles(
|
||||
path: string,
|
||||
filter?: RegExp | undefined,
|
||||
options?: { recursive?: boolean | undefined } | undefined
|
||||
): string[] {
|
||||
return this._host.listFiles(path, filter, options);
|
||||
}
|
||||
log(text: string): void {
|
||||
this._host.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
function createTestHost(testServerData: CustomLSP.TestServerStartOptions) {
|
||||
const scriptOutput = '';
|
||||
const runScript = async (
|
||||
pythonPath: Uri | undefined,
|
||||
scriptPath: Uri,
|
||||
args: string[],
|
||||
cwd: Uri,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
return { stdout: scriptOutput, stderr: '', exitCode: 0 };
|
||||
};
|
||||
const options = new TestHostOptions({ version: testServerData.pythonVersion, runScript });
|
||||
const projectRootPaths = testServerData.projectRoots.map((p) => getFileLikePath(p));
|
||||
const testData = parseTestData(
|
||||
testServerData.projectRoots.length === 1 ? projectRootPaths[0] : DEFAULT_WORKSPACE_ROOT,
|
||||
testServerData.code,
|
||||
'noname.py'
|
||||
);
|
||||
const commonRoot = getCommonRoot(testServerData.projectRoots);
|
||||
|
||||
// Make sure global variables from previous tests are cleared.
|
||||
clearCache();
|
||||
|
||||
// create a test file system using the test data.
|
||||
const fs = createFileSystem(commonRoot, testData, new TestPyrightHost(PyrightTestHost.HOST));
|
||||
|
||||
return new TestHost(fs, fs, testData, projectRootPaths, options);
|
||||
}
|
||||
|
||||
class TestServer extends PyrightServer {
|
||||
constructor(
|
||||
connection: Connection,
|
||||
fs: FileSystem,
|
||||
private readonly _supportsBackgroundAnalysis: boolean | undefined
|
||||
) {
|
||||
super(connection, fs);
|
||||
}
|
||||
|
||||
test_onDidChangeWatchedFiles(params: any) {
|
||||
this.onDidChangeWatchedFiles(params);
|
||||
}
|
||||
override async updateSettingsForWorkspace(
|
||||
workspace: Workspace,
|
||||
status: InitStatus | undefined,
|
||||
serverSettings?: ServerSettings | undefined
|
||||
): Promise<void> {
|
||||
const result = await super.updateSettingsForWorkspace(workspace, status, serverSettings);
|
||||
|
||||
// LSP notification only allows synchronous callback. because of that, the one that sent the notification can't know
|
||||
// when the work caused by the notification actually ended. To workaround that issue, we will send custom lsp to indicate
|
||||
// something has been done.
|
||||
CustomLSP.sendNotification(this.connection, CustomLSP.Notifications.TestSignal, {
|
||||
uri: workspace.rootUri.toString(),
|
||||
kind: CustomLSP.TestSignalKinds.Initialization,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
override createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined {
|
||||
if (this._supportsBackgroundAnalysis) {
|
||||
return new BackgroundAnalysis(this.serverOptions.serviceProvider);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function runServer(
|
||||
testServerData: CustomLSP.TestServerStartOptions,
|
||||
reader: MessageReader,
|
||||
writer: MessageWriter,
|
||||
connectionFactory: (reader: MessageReader, writer: MessageWriter) => Connection
|
||||
): Promise<{ disposables: Disposable[]; connection: Connection }> {
|
||||
// Create connection back to the client first.
|
||||
const connection = connectionFactory(reader, writer);
|
||||
|
||||
// Fixup the input data.
|
||||
testServerData = {
|
||||
...testServerData,
|
||||
projectRoots: testServerData.projectRoots.map((p) => Uri.fromJsonObj(p)),
|
||||
logFile: Uri.fromJsonObj(testServerData.logFile),
|
||||
};
|
||||
|
||||
try {
|
||||
// Create a host so we can control the file system for the PyrightServer.
|
||||
const disposables: Disposable[] = [];
|
||||
const host = createTestHost(testServerData);
|
||||
const server = new TestServer(connection, host.fs, testServerData.backgroundAnalysis);
|
||||
|
||||
// Listen for the test messages from the client. These messages
|
||||
// are how the test code queries the state of the server.
|
||||
disposables.push(
|
||||
CustomLSP.onRequest(connection, CustomLSP.Requests.GetDiagnostics, async (params, token) => {
|
||||
const filePath = Uri.parse(params.uri, true);
|
||||
const workspace = await server.getWorkspaceForFile(filePath);
|
||||
workspace.service.test_program.analyze(undefined, token);
|
||||
const file = workspace.service.test_program.getBoundSourceFile(filePath);
|
||||
const diagnostics = file?.getDiagnostics(workspace.service.test_program.configOptions) || [];
|
||||
return { diagnostics: serialize(diagnostics) };
|
||||
})
|
||||
);
|
||||
|
||||
// Dispose the server and connection when terminating the server.
|
||||
disposables.push(server);
|
||||
disposables.push(connection);
|
||||
|
||||
return { disposables, connection };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return { disposables: [], connection };
|
||||
}
|
||||
}
|
||||
|
||||
class ListeningPortMessageWriter extends PortMessageWriter {
|
||||
private _callbacks: ((msg: Message) => Promise<void>)[] = [];
|
||||
constructor(port: MessagePort) {
|
||||
super(port);
|
||||
}
|
||||
override async write(msg: Message): Promise<void> {
|
||||
await Promise.all(this._callbacks.map((c) => c(msg)));
|
||||
return super.write(msg);
|
||||
}
|
||||
|
||||
onPostMessage(callback: (msg: Message) => Promise<void>) {
|
||||
this._callbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object that exists in the worker thread that starts and stops (and cleans up after) the main server.
|
||||
*/
|
||||
class ServerStateManager {
|
||||
private _instances: { disposables: Disposable[]; connection: Connection }[] = [];
|
||||
private _pendingDispose: Deferred<void> | undefined;
|
||||
private _reader = new PortMessageReader(parentPort!);
|
||||
private _writer = new ListeningPortMessageWriter(parentPort!);
|
||||
private _currentOptions: CustomLSP.TestServerStartOptions | undefined;
|
||||
private _shutdownId: string | number | null = null;
|
||||
constructor(private readonly _connectionFactory: (reader: MessageReader, writer: MessageWriter) => Connection) {
|
||||
// Listen for shutdown response.
|
||||
this._writer.onPostMessage(async (msg: Message) => {
|
||||
if (Message.isResponse(msg) && msg.id === this._shutdownId) {
|
||||
await this._handleShutdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run() {
|
||||
parentPort?.on('message', (message) => this._handleMessage(message));
|
||||
}
|
||||
|
||||
private _handleMessage(message: any) {
|
||||
try {
|
||||
// Debug output to help diagnose sync issues.
|
||||
if (message && message.method === CustomLSP.Notifications.TestStartServer) {
|
||||
this._handleStart(message.params);
|
||||
} else if (Message.isRequest(message) && message.method === ShutdownRequest.method) {
|
||||
this._shutdownId = message.id;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleStart(options: CustomLSP.TestServerStartOptions) {
|
||||
logToDisk(`Starting server for ${options.testName}`, options.logFile);
|
||||
|
||||
// Every time we start the server, remove all message handlers from our PortMessageReader.
|
||||
// This prevents the old servers from responding to messages for new ones.
|
||||
this._reader.dispose();
|
||||
// Wait for the previous server to finish. This should be okay because the test
|
||||
// client waits for the response message before sending anything else. Otherwise
|
||||
// we'd receive the initialize message for the server and drop it before the server
|
||||
// actually started.
|
||||
if (this._pendingDispose) {
|
||||
logToDisk(
|
||||
`Waiting for previous server ${this._currentOptions?.testName} to finish for ${options.testName}`,
|
||||
options.logFile
|
||||
);
|
||||
await this._pendingDispose.promise;
|
||||
this._pendingDispose = undefined;
|
||||
}
|
||||
this._currentOptions = options;
|
||||
|
||||
// Set the worker data for the current test. Any background threads
|
||||
// started after this point will pick up this value.
|
||||
setEnvironmentData(WORKER_BACKGROUND_DATA, options);
|
||||
|
||||
// Create an instance of the server.
|
||||
const { disposables, connection } = await runServer(
|
||||
options,
|
||||
this._reader,
|
||||
this._writer,
|
||||
this._connectionFactory
|
||||
);
|
||||
this._instances.push({ disposables, connection });
|
||||
|
||||
// Enable this to help diagnose sync issues.
|
||||
logToDisk(`Started server for ${options.testName}`, options.logFile);
|
||||
|
||||
// Respond back.
|
||||
parentPort?.postMessage({
|
||||
jsonrpc: '2.0',
|
||||
method: CustomLSP.Notifications.TestStartServerResponse,
|
||||
params: options,
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleShutdown() {
|
||||
if (this._currentOptions) {
|
||||
logToDisk(`Stopping ${this._currentOptions?.testName}`, this._currentOptions.logFile);
|
||||
}
|
||||
this._shutdownId = null;
|
||||
const instance = this._instances.pop();
|
||||
if (instance) {
|
||||
this._pendingDispose = createDeferred<void>();
|
||||
|
||||
// Wait for our connection to finish first. Give it 10 tries.
|
||||
// This is a bit of a hack but there are no good ways to cancel all running requests
|
||||
// on shutdown.
|
||||
let count = 0;
|
||||
while (count < 10 && (instance.connection as any).console?._rawConnection?.hasPendingResponse()) {
|
||||
await sleep(10);
|
||||
count += 1;
|
||||
}
|
||||
this._pendingDispose.resolve();
|
||||
instance.disposables.forEach((d) => {
|
||||
d.dispose();
|
||||
});
|
||||
this._pendingDispose = undefined;
|
||||
if (this._currentOptions) {
|
||||
logToDisk(`Stopped ${this._currentOptions?.testName}`, this._currentOptions.logFile);
|
||||
}
|
||||
} else {
|
||||
if (this._currentOptions) {
|
||||
logToDisk(`Failed to stop ${this._currentOptions?.testName}`, this._currentOptions.logFile);
|
||||
}
|
||||
}
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runTestBackgroundThread() {
|
||||
let options = getEnvironmentData(WORKER_BACKGROUND_DATA) as CustomLSP.TestServerStartOptions;
|
||||
|
||||
// Normalize the options.
|
||||
options = {
|
||||
...options,
|
||||
projectRoots: options.projectRoots.map((p) => Uri.fromJsonObj(p)),
|
||||
logFile: Uri.fromJsonObj(options.logFile),
|
||||
};
|
||||
try {
|
||||
// Create a host on the background thread too so that it uses
|
||||
// the host's file system. Has to be sync so that we don't
|
||||
// drop any messages sent to the background thread.
|
||||
const host = createTestHost(options);
|
||||
const fs = new PyrightFileSystem(host.fs);
|
||||
const serviceProvider = new ServiceProvider();
|
||||
serviceProvider.add(ServiceKeys.fs, fs);
|
||||
|
||||
// run default background runner
|
||||
const runner = new BackgroundAnalysisRunner(serviceProvider);
|
||||
runner.start();
|
||||
} catch (e) {
|
||||
console.error(`BackgroundThread crashed with ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function run() {
|
||||
// Start the background thread if this is not the first worker.
|
||||
if (getEnvironmentData(WORKER_STARTED) === 'true') {
|
||||
runTestBackgroundThread();
|
||||
} else {
|
||||
setEnvironmentData(WORKER_STARTED, 'true');
|
||||
|
||||
// Start the server state manager.
|
||||
const stateManager = new ServerStateManager((reader, writer) => createConnection(reader, writer, {}));
|
||||
stateManager.run();
|
||||
}
|
||||
}
|
@ -0,0 +1,973 @@
|
||||
/*
|
||||
* languageServerTestUtils.ts
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT license.
|
||||
*
|
||||
* Utilities for running tests against the LSP server.
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import * as fs from 'fs-extra';
|
||||
import { isMainThread, threadId, Worker } from 'node:worker_threads';
|
||||
import path from 'path';
|
||||
import {
|
||||
ApplyWorkspaceEditParams,
|
||||
ApplyWorkspaceEditRequest,
|
||||
CancellationToken,
|
||||
ConfigurationItem,
|
||||
ConfigurationRequest,
|
||||
DidChangeWorkspaceFoldersNotification,
|
||||
DidOpenTextDocumentNotification,
|
||||
Disposable,
|
||||
InitializedNotification,
|
||||
InitializeParams,
|
||||
InitializeRequest,
|
||||
InlayHintRefreshRequest,
|
||||
LogMessageNotification,
|
||||
LogMessageParams,
|
||||
PublishDiagnosticsNotification,
|
||||
PublishDiagnosticsParams,
|
||||
Registration,
|
||||
RegistrationRequest,
|
||||
SemanticTokensRefreshRequest,
|
||||
ShutdownRequest,
|
||||
TelemetryEventNotification,
|
||||
UnregistrationRequest,
|
||||
} from 'vscode-languageserver-protocol';
|
||||
import {
|
||||
Connection,
|
||||
createConnection,
|
||||
Emitter,
|
||||
Event,
|
||||
HoverRequest,
|
||||
NotificationHandler,
|
||||
PortMessageReader,
|
||||
PortMessageWriter,
|
||||
ProgressToken,
|
||||
ProgressType,
|
||||
ProtocolNotificationType,
|
||||
WorkDoneProgress,
|
||||
WorkDoneProgressCancelNotification,
|
||||
WorkDoneProgressCreateRequest,
|
||||
} from 'vscode-languageserver/node';
|
||||
import { PythonPathResult } from '../../analyzer/pythonPathUtils';
|
||||
import { IPythonMode } from '../../analyzer/sourceFile';
|
||||
import { PythonPlatform } from '../../common/configOptions';
|
||||
import { createDeferred, Deferred } from '../../common/deferred';
|
||||
import { DiagnosticSink } from '../../common/diagnosticSink';
|
||||
import { FileSystem } from '../../common/fileSystem';
|
||||
import { LimitedAccessHost } from '../../common/fullAccessHost';
|
||||
import { HostKind, ScriptOutput } from '../../common/host';
|
||||
import { combinePaths, resolvePaths } from '../../common/pathUtils';
|
||||
import { convertOffsetToPosition } from '../../common/positionUtils';
|
||||
import { PythonVersion } from '../../common/pythonVersion';
|
||||
import { FileUri } from '../../common/uri/fileUri';
|
||||
import { Uri } from '../../common/uri/uri';
|
||||
import { ParseOptions, Parser } from '../../parser/parser';
|
||||
import { parseTestData } from '../harness/fourslash/fourSlashParser';
|
||||
import { FourSlashData } from '../harness/fourslash/fourSlashTypes';
|
||||
import { createVfsInfoFromFourSlashData, getMarkerByName } from '../harness/fourslash/testStateUtils';
|
||||
import * as host from '../harness/testHost';
|
||||
import { createFromFileSystem, distlibFolder, libFolder } from '../harness/vfs/factory';
|
||||
import * as vfs from '../harness/vfs/filesystem';
|
||||
import { CustomLSP } from './customLsp';
|
||||
|
||||
// bundled root on test virtual file system.
|
||||
const bundledStubsFolder = combinePaths(vfs.MODULE_PATH, 'bundled', 'stubs');
|
||||
|
||||
// bundled file path on real file system.
|
||||
const bundledStubsFolderPath = resolvePaths(__dirname, '../../bundled/stubs');
|
||||
const bundledStubsFolderPathTestServer = resolvePaths(__dirname, '../bundled/stubs');
|
||||
|
||||
// project root on test virtual file system.
|
||||
export const DEFAULT_WORKSPACE_ROOT = combinePaths('/', 'src');
|
||||
|
||||
export const ERROR_SCRIPT_OUTPUT = 'Error: script failed to run';
|
||||
export const STALL_SCRIPT_OUTPUT = 'Timeout: script never finished running';
|
||||
|
||||
export interface PyrightServerInfo {
|
||||
disposables: Disposable[];
|
||||
registrations: Registration[];
|
||||
logs: LogMessageParams[];
|
||||
connection: Connection;
|
||||
signals: Map<CustomLSP.TestSignalKinds, Deferred<boolean>>;
|
||||
testName: string; // Used for debugging
|
||||
testData: FourSlashData;
|
||||
projectRoots: Uri[];
|
||||
progressReporters: string[];
|
||||
progressReporterStatus: Map<string, number>;
|
||||
progressParts: Map<string, TestProgressPart>;
|
||||
telemetry: any[];
|
||||
diagnostics: PublishDiagnosticsParams[];
|
||||
diagnosticsEvent: Event<PublishDiagnosticsParams>;
|
||||
workspaceEdits: ApplyWorkspaceEditParams[];
|
||||
workspaceEditsEvent: Event<ApplyWorkspaceEditParams>;
|
||||
getInitializeParams(): InitializeParams;
|
||||
dispose(): Promise<void>;
|
||||
convertPathToUri(path: string): Uri;
|
||||
}
|
||||
|
||||
export class TestHostOptions {
|
||||
version: PythonVersion;
|
||||
platform: PythonPlatform;
|
||||
|
||||
// Search path on virtual file system.
|
||||
searchPaths: Uri[];
|
||||
|
||||
// Run script function
|
||||
runScript: (
|
||||
pythonPath: Uri | undefined,
|
||||
scriptPath: Uri,
|
||||
args: string[],
|
||||
cwd: Uri,
|
||||
token: CancellationToken
|
||||
) => Promise<ScriptOutput>;
|
||||
|
||||
constructor({
|
||||
version = PythonVersion.V3_10,
|
||||
platform = PythonPlatform.Linux,
|
||||
searchPaths = [libFolder, distlibFolder],
|
||||
runScript = async (
|
||||
pythonPath: Uri | undefined,
|
||||
scriptPath: Uri,
|
||||
args: string[],
|
||||
cwd: Uri,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
return { stdout: '', stderr: '' };
|
||||
},
|
||||
} = {}) {
|
||||
this.version = version;
|
||||
this.platform = platform;
|
||||
this.searchPaths = searchPaths;
|
||||
this.runScript = runScript;
|
||||
}
|
||||
}
|
||||
// Enable this to log to disk for debugging sync issues.
|
||||
export const logToDisk = (m: string, f: Uri) => {}; // logToDiskImpl
|
||||
export function logToDiskImpl(message: string, fileName: Uri) {
|
||||
const thread = isMainThread ? 'main' : threadId.toString();
|
||||
fs.writeFileSync(fileName.getFilePath(), `${Date.now()} : ${thread} : ${message}\n`, {
|
||||
flag: 'a+',
|
||||
});
|
||||
}
|
||||
|
||||
// Global server worker.
|
||||
let serverWorker: Worker | undefined;
|
||||
let serverWorkerFile: string | undefined;
|
||||
let lastServerFinished: { name: string; finished: boolean } = { name: '', finished: true };
|
||||
|
||||
function removeAllListeners(worker: Worker) {
|
||||
// Only remove the 'message', 'error' and 'close' events
|
||||
worker.rawListeners('message').forEach((listener) => worker.removeListener('message', listener as any));
|
||||
worker.rawListeners('error').forEach((listener) => worker.removeListener('error', listener as any));
|
||||
worker.rawListeners('close').forEach((listener) => worker.removeListener('close', listener as any));
|
||||
}
|
||||
|
||||
function createServerWorker(file: string, testServerData: CustomLSP.TestServerStartOptions) {
|
||||
// Do not terminate the worker if it's the same file. Reuse it.
|
||||
// This makes tests run a lot faster because creating a worker is the same
|
||||
// as starting a new process.
|
||||
if (!serverWorker || serverWorkerFile !== file) {
|
||||
serverWorker?.terminate();
|
||||
serverWorkerFile = file;
|
||||
serverWorker = new Worker(file);
|
||||
logToDisk(`Created new server worker for ${file}`, testServerData.logFile);
|
||||
}
|
||||
// Every time we 'create' the worker, refresh its message handlers. This
|
||||
// is essentially the same thing as creating a new worker.
|
||||
removeAllListeners(serverWorker);
|
||||
logToDisk(
|
||||
`Removed all worker listeners. Test ${testServerData.testName} is starting.\n Last test was ${lastServerFinished.name} and finished: ${lastServerFinished.finished}`,
|
||||
testServerData.logFile
|
||||
);
|
||||
serverWorker.on('error', (e) => {
|
||||
logToDisk(`Worker error: ${e}`, testServerData.logFile);
|
||||
});
|
||||
serverWorker.on('exit', (code) => {
|
||||
logToDisk(`Worker exit: ${code}`, testServerData.logFile);
|
||||
});
|
||||
return serverWorker;
|
||||
}
|
||||
|
||||
export async function cleanupAfterAll() {
|
||||
if (serverWorker) {
|
||||
await serverWorker.terminate();
|
||||
serverWorker = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileLikePath(uri: Uri): string {
|
||||
return FileUri.isFileUri(uri) ? uri.getFilePath() : uri.toString();
|
||||
}
|
||||
|
||||
export function createFileSystem(projectRoot: string, testData: FourSlashData, optionalHost?: host.TestHost) {
|
||||
const mountedPaths = new Map<string, string>();
|
||||
if (fs.existsSync(bundledStubsFolderPath)) {
|
||||
mountedPaths.set(bundledStubsFolder, bundledStubsFolderPath);
|
||||
} else if (fs.existsSync(bundledStubsFolderPathTestServer)) {
|
||||
mountedPaths.set(bundledStubsFolder, bundledStubsFolderPathTestServer);
|
||||
}
|
||||
|
||||
const vfsInfo = createVfsInfoFromFourSlashData(projectRoot, testData);
|
||||
return createFromFileSystem(
|
||||
optionalHost ?? host.HOST,
|
||||
vfsInfo.ignoreCase,
|
||||
{ cwd: vfsInfo.projectRoot, files: vfsInfo.files, meta: testData.globalOptions },
|
||||
mountedPaths
|
||||
);
|
||||
}
|
||||
|
||||
const settingsMap = new Map<PyrightServerInfo, { item: ConfigurationItem; value: any }[]>();
|
||||
|
||||
export function updateSettingsMap(info: PyrightServerInfo, settings: { item: ConfigurationItem; value: any }[]) {
|
||||
// Normalize the URIs for all of the settings.
|
||||
settings.forEach((s) => {
|
||||
if (s.item.scopeUri) {
|
||||
s.item.scopeUri = Uri.parse(s.item.scopeUri, true).toString();
|
||||
}
|
||||
});
|
||||
|
||||
const current = settingsMap.get(info) || [];
|
||||
settingsMap.set(info, [...settings, ...current]);
|
||||
}
|
||||
|
||||
export function getParseResults(fileContents: string, isStubFile = false, ipythonMode: IPythonMode = 0) {
|
||||
const diagSink = new DiagnosticSink();
|
||||
const parseOptions = new ParseOptions();
|
||||
parseOptions.ipythonMode = ipythonMode;
|
||||
parseOptions.isStubFile = isStubFile;
|
||||
parseOptions.pythonVersion = PythonVersion.V3_10;
|
||||
parseOptions.skipFunctionAndClassBody = false;
|
||||
|
||||
// Parse the token stream, building the abstract syntax tree.
|
||||
const parser = new Parser();
|
||||
return parser.parseSourceFile(fileContents, parseOptions, diagSink);
|
||||
}
|
||||
|
||||
function createServerConnection(testServerData: CustomLSP.TestServerStartOptions, disposables: Disposable[]) {
|
||||
// Start a worker with the server running in it.
|
||||
const serverPath = path.join(__dirname, '..', '..', '..', 'out', 'testServer.bundle.js');
|
||||
assert(
|
||||
fs.existsSync(serverPath),
|
||||
`Server bundle does not exist: ${serverPath}. Make sure you ran the build script for test bundle (npm run webpack:testserver).`
|
||||
);
|
||||
const serverWorker = createServerWorker(serverPath, testServerData);
|
||||
const options = {};
|
||||
|
||||
const connection = createConnection(
|
||||
new PortMessageReader(serverWorker),
|
||||
new PortMessageWriter(serverWorker),
|
||||
options
|
||||
);
|
||||
disposables.push(connection);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
export async function waitForDiagnostics(info: PyrightServerInfo, timeout = 10000) {
|
||||
const deferred = createDeferred<void>();
|
||||
const disposable = info.diagnosticsEvent((params) => {
|
||||
if (params.diagnostics.length > 0) {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
const timer = setTimeout(() => deferred.reject('Timed out waiting for diagnostics'), timeout);
|
||||
try {
|
||||
await deferred.promise;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
disposable.dispose();
|
||||
}
|
||||
return info.diagnostics;
|
||||
}
|
||||
|
||||
interface ProgressPart {}
|
||||
|
||||
interface ProgressContext {
|
||||
onProgress<P>(type: ProgressType<P>, token: string | number, handler: NotificationHandler<P>): Disposable;
|
||||
sendNotification<P, RO>(type: ProtocolNotificationType<P, RO>, params?: P): void;
|
||||
}
|
||||
|
||||
class TestProgressPart implements ProgressPart {
|
||||
constructor(
|
||||
private readonly _context: ProgressContext,
|
||||
private readonly _token: ProgressToken,
|
||||
info: PyrightServerInfo,
|
||||
done: () => void
|
||||
) {
|
||||
info.disposables.push(
|
||||
info.connection.onProgress(WorkDoneProgress.type, _token, (params) => {
|
||||
switch (params.kind) {
|
||||
case 'begin':
|
||||
info.progressReporterStatus.set(_token.toString(), 0);
|
||||
break;
|
||||
case 'report':
|
||||
info.progressReporterStatus.set(_token.toString(), params.percentage ?? 0);
|
||||
break;
|
||||
case 'end':
|
||||
done();
|
||||
break;
|
||||
}
|
||||
})
|
||||
);
|
||||
info.progressReporters.push(this._token.toString());
|
||||
info.progressParts.set(this._token.toString(), this);
|
||||
}
|
||||
|
||||
sendCancel() {
|
||||
this._context.sendNotification(WorkDoneProgressCancelNotification.type, { token: this._token });
|
||||
}
|
||||
}
|
||||
|
||||
export async function runPyrightServer(
|
||||
projectRoots: string[] | string,
|
||||
code: string,
|
||||
callInitialize = true,
|
||||
extraSettings?: { item: ConfigurationItem; value: any }[],
|
||||
pythonVersion: PythonVersion = PythonVersion.V3_10,
|
||||
backgroundAnalysis?: boolean
|
||||
): Promise<PyrightServerInfo> {
|
||||
// Normalize the URIs for all of the settings.
|
||||
extraSettings?.forEach((s) => {
|
||||
if (s.item.scopeUri) {
|
||||
s.item.scopeUri = Uri.parse(s.item.scopeUri, true).toString();
|
||||
}
|
||||
});
|
||||
|
||||
// Setup the test data we need to send for Test server startup.
|
||||
const projectRootsArray = Array.isArray(projectRoots) ? projectRoots : [projectRoots];
|
||||
const testServerData: CustomLSP.TestServerStartOptions = {
|
||||
testName: expect.getState().currentTestName ?? 'NoName',
|
||||
code,
|
||||
projectRoots: projectRootsArray.map((p) => (p.includes(':') ? Uri.parse(p, true) : Uri.file(p))),
|
||||
pythonVersion,
|
||||
backgroundAnalysis,
|
||||
logFile: Uri.file(path.join(__dirname, `log${process.pid}.txt`)),
|
||||
pid: process.pid.toString(),
|
||||
};
|
||||
|
||||
logToDisk(`Starting test ${testServerData.testName}`, testServerData.logFile);
|
||||
lastServerFinished = { name: testServerData.testName, finished: false };
|
||||
|
||||
// Parse the test data on this side as well. This allows the use of markers and such.
|
||||
const testData = parseTestData(
|
||||
testServerData.projectRoots.length === 1
|
||||
? getFileLikePath(testServerData.projectRoots[0])
|
||||
: DEFAULT_WORKSPACE_ROOT,
|
||||
testServerData.code,
|
||||
'noname.py'
|
||||
);
|
||||
|
||||
// Start listening to the 'client' side of the connection.
|
||||
const disposables: Disposable[] = [];
|
||||
const connection = createServerConnection(testServerData, disposables);
|
||||
const serverStarted = createDeferred<string>();
|
||||
const diagnosticsEmitter = new Emitter<PublishDiagnosticsParams>();
|
||||
const workspaceEditsEmitter = new Emitter<ApplyWorkspaceEditParams>();
|
||||
|
||||
// Setup the server info.
|
||||
const info: PyrightServerInfo = {
|
||||
disposables,
|
||||
registrations: [],
|
||||
connection,
|
||||
logs: [],
|
||||
progressReporters: [],
|
||||
progressReporterStatus: new Map<string, number>(),
|
||||
progressParts: new Map<string, TestProgressPart>(),
|
||||
signals: new Map(Object.values(CustomLSP.TestSignalKinds).map((v) => [v, createDeferred<boolean>()])),
|
||||
testData,
|
||||
testName: testServerData.testName,
|
||||
telemetry: [],
|
||||
projectRoots: testServerData.projectRoots,
|
||||
diagnostics: [],
|
||||
diagnosticsEvent: diagnosticsEmitter.event,
|
||||
workspaceEdits: [],
|
||||
workspaceEditsEvent: workspaceEditsEmitter.event,
|
||||
getInitializeParams: () => getInitializeParams(testServerData.projectRoots),
|
||||
convertPathToUri: (path: string) => Uri.file(path),
|
||||
dispose: async () => {
|
||||
// Send shutdown. This should disconnect the dispatcher and kill the server.
|
||||
await connection.sendRequest(ShutdownRequest.type, undefined);
|
||||
|
||||
// Now we can dispose the connection.
|
||||
disposables.forEach((d) => d.dispose());
|
||||
|
||||
logToDisk(`Finished test ${testServerData.testName}`, testServerData.logFile);
|
||||
},
|
||||
};
|
||||
info.disposables.push(
|
||||
info.connection.onNotification(CustomLSP.Notifications.TestStartServerResponse, (p) => {
|
||||
serverStarted.resolve(p.testName);
|
||||
}),
|
||||
info.connection.onRequest(RegistrationRequest.type, (p) => {
|
||||
info.registrations.push(...p.registrations);
|
||||
}),
|
||||
info.connection.onNotification(CustomLSP.Notifications.TestSignal, (p: CustomLSP.TestSignal) => {
|
||||
info.signals.get(p.kind)!.resolve(true);
|
||||
}),
|
||||
info.connection.onNotification(LogMessageNotification.type, (p) => {
|
||||
info.logs.push(p);
|
||||
}),
|
||||
info.connection.onRequest(SemanticTokensRefreshRequest.type, () => {
|
||||
// empty. Sliently ignore for now.
|
||||
}),
|
||||
info.connection.onRequest(InlayHintRefreshRequest.type, () => {
|
||||
// empty. Sliently ignore for now.
|
||||
}),
|
||||
info.connection.onRequest(ApplyWorkspaceEditRequest.type, (p) => {
|
||||
info.workspaceEdits.push(p);
|
||||
workspaceEditsEmitter.fire(p);
|
||||
return { applied: true };
|
||||
}),
|
||||
info.connection.onRequest(UnregistrationRequest.type, (p) => {
|
||||
const unregisterIds = p.unregisterations.map((u) => u.id);
|
||||
info.registrations = info.registrations.filter((r) => !unregisterIds.includes(r.id));
|
||||
}),
|
||||
info.connection.onRequest(WorkDoneProgressCreateRequest.type, (p) => {
|
||||
// Save the progress reporter so we can send progress updates.
|
||||
info.progressReporters.push(p.token.toString());
|
||||
info.disposables.push(
|
||||
info.connection.onProgress(WorkDoneProgress.type, p.token, (params) => {
|
||||
switch (params.kind) {
|
||||
case 'begin':
|
||||
info.progressReporterStatus.set(p.token.toString(), 0);
|
||||
break;
|
||||
case 'report':
|
||||
info.progressReporterStatus.set(p.token.toString(), params.percentage ?? 0);
|
||||
break;
|
||||
case 'end':
|
||||
break;
|
||||
}
|
||||
})
|
||||
);
|
||||
}),
|
||||
info.connection.onNotification(PublishDiagnosticsNotification.type, (p) => {
|
||||
info.diagnostics.push(p);
|
||||
diagnosticsEmitter.fire(p);
|
||||
}),
|
||||
info.connection.onNotification(TelemetryEventNotification.type, (p) => {
|
||||
info.telemetry.push(p);
|
||||
})
|
||||
);
|
||||
info.disposables.push(
|
||||
info.connection.onRequest(ConfigurationRequest.type, (p) => {
|
||||
const result = [];
|
||||
const mappedSettings = settingsMap.get(info) || [];
|
||||
for (const item of p.items) {
|
||||
const setting = mappedSettings.find(
|
||||
(s) =>
|
||||
(s.item.scopeUri === item.scopeUri || s.item.scopeUri === undefined) &&
|
||||
s.item.section === item.section
|
||||
);
|
||||
result.push(setting?.value);
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
// Merge the extra settings.
|
||||
const settings: { item: ConfigurationItem; value: any }[] = [];
|
||||
if (extraSettings) {
|
||||
for (const extra of extraSettings) {
|
||||
const existing = settings.find(
|
||||
(s) => s.item.section === extra.item.section && s.item.scopeUri === extra.item.scopeUri
|
||||
);
|
||||
if (existing) {
|
||||
existing.value = { ...existing.value, ...extra.value };
|
||||
} else {
|
||||
settings.push(extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsMap.set(info, settings);
|
||||
|
||||
// Wait for the server to be started.
|
||||
connection.listen();
|
||||
logToDisk(`Sending start notification for ${testServerData.testName}`, testServerData.logFile);
|
||||
CustomLSP.sendNotification(connection, CustomLSP.Notifications.TestStartServer, testServerData);
|
||||
const serverTestName = await serverStarted.promise;
|
||||
assert.equal(serverTestName, testServerData.testName, 'Server started for wrong test');
|
||||
|
||||
logToDisk(`Started test ${testServerData.testName}`, testServerData.logFile);
|
||||
|
||||
// Initialize the server if requested.
|
||||
if (callInitialize) {
|
||||
await initializeLanguageServer(info);
|
||||
logToDisk(`Initialized test ${testServerData.testName}`, testServerData.logFile);
|
||||
}
|
||||
|
||||
if (lastServerFinished.name === testServerData.testName) {
|
||||
lastServerFinished.finished = true;
|
||||
} else {
|
||||
logToDisk(`Last server finished was incorrectly updated to ${lastServerFinished.name}`, testServerData.logFile);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
export async function initializeLanguageServer(info: PyrightServerInfo) {
|
||||
const params = info.getInitializeParams();
|
||||
|
||||
// Send the initialize request.
|
||||
const result = await info.connection.sendRequest(InitializeRequest.type, params, CancellationToken.None);
|
||||
info.connection.sendNotification(InitializedNotification.type, {});
|
||||
|
||||
if (params.workspaceFolders?.length) {
|
||||
info.connection.sendNotification(DidChangeWorkspaceFoldersNotification.type, {
|
||||
event: {
|
||||
added: params.workspaceFolders!,
|
||||
removed: [],
|
||||
},
|
||||
});
|
||||
|
||||
// Wait until workspace initialization is done.
|
||||
// This is required since some tests check status of server directly. In such case, even if the client sent notification,
|
||||
// server might not have processed it and still in the event queue.
|
||||
// This check makes sure server at least processed initialization before test checking server status directly.
|
||||
// If test only uses response from client.sendRequest, then this won't be needed.
|
||||
await info.signals.get(CustomLSP.TestSignalKinds.Initialization)!.promise;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function sleep(timeout: number): Promise<number> {
|
||||
return new Promise<number>((resolve) => {
|
||||
setTimeout(() => resolve(timeout), timeout);
|
||||
});
|
||||
}
|
||||
|
||||
export function openFile(info: PyrightServerInfo, markerName: string, text?: string) {
|
||||
const marker = getMarkerByName(info.testData, markerName);
|
||||
const uri = marker.fileUri.toString();
|
||||
|
||||
text = text ?? info.testData.files.find((f) => f.fileName === marker.fileName)!.content;
|
||||
|
||||
info.connection.sendNotification(DidOpenTextDocumentNotification.type, {
|
||||
textDocument: { uri, languageId: 'python', version: 1, text },
|
||||
});
|
||||
}
|
||||
|
||||
export async function hover(info: PyrightServerInfo, markerName: string) {
|
||||
const marker = info.testData.markerPositions.get('marker')!;
|
||||
const fileUri = marker.fileUri;
|
||||
const text = info.testData.files.find((d) => d.fileName === marker.fileName)!.content;
|
||||
const parseResult = getParseResults(text);
|
||||
const hoverResult = await info.connection.sendRequest(
|
||||
HoverRequest.type,
|
||||
{
|
||||
textDocument: { uri: fileUri.toString() },
|
||||
position: convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines),
|
||||
},
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
return hoverResult;
|
||||
}
|
||||
|
||||
export function getInitializeParams(projectRoots: Uri[]) {
|
||||
// cloned vscode "1.71.0-insider"'s initialize params.
|
||||
const workspaceFolders = projectRoots
|
||||
? projectRoots.map((root, i) => ({ name: root.fileName, uri: projectRoots[i].toString() }))
|
||||
: [];
|
||||
|
||||
const params: InitializeParams = {
|
||||
processId: process.pid,
|
||||
clientInfo: {
|
||||
name: `Pylance Unit Test ${expect.getState().currentTestName}`,
|
||||
version: '1.71.0-insider',
|
||||
},
|
||||
locale: 'en-us',
|
||||
rootPath: null,
|
||||
rootUri: null,
|
||||
capabilities: {
|
||||
workspace: {
|
||||
applyEdit: true,
|
||||
workspaceEdit: {
|
||||
documentChanges: true,
|
||||
resourceOperations: ['create', 'rename', 'delete'],
|
||||
failureHandling: 'textOnlyTransactional',
|
||||
normalizesLineEndings: true,
|
||||
changeAnnotationSupport: {
|
||||
groupsOnLabel: true,
|
||||
},
|
||||
},
|
||||
configuration: true,
|
||||
didChangeWatchedFiles: {
|
||||
dynamicRegistration: true,
|
||||
relativePatternSupport: true,
|
||||
},
|
||||
symbol: {
|
||||
dynamicRegistration: true,
|
||||
symbolKind: {
|
||||
valueSet: [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
||||
26,
|
||||
],
|
||||
},
|
||||
tagSupport: {
|
||||
valueSet: [1],
|
||||
},
|
||||
resolveSupport: {
|
||||
properties: ['location.range'],
|
||||
},
|
||||
},
|
||||
codeLens: {
|
||||
refreshSupport: true,
|
||||
},
|
||||
executeCommand: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
didChangeConfiguration: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
workspaceFolders: true,
|
||||
semanticTokens: {
|
||||
refreshSupport: true,
|
||||
},
|
||||
fileOperations: {
|
||||
dynamicRegistration: true,
|
||||
didCreate: true,
|
||||
didRename: true,
|
||||
didDelete: true,
|
||||
willCreate: true,
|
||||
willRename: true,
|
||||
willDelete: true,
|
||||
},
|
||||
inlineValue: {
|
||||
refreshSupport: true,
|
||||
},
|
||||
inlayHint: {
|
||||
refreshSupport: true,
|
||||
},
|
||||
diagnostics: {
|
||||
refreshSupport: true,
|
||||
},
|
||||
},
|
||||
textDocument: {
|
||||
publishDiagnostics: {
|
||||
relatedInformation: true,
|
||||
versionSupport: false,
|
||||
tagSupport: {
|
||||
valueSet: [1, 2],
|
||||
},
|
||||
codeDescriptionSupport: true,
|
||||
dataSupport: true,
|
||||
},
|
||||
synchronization: {
|
||||
dynamicRegistration: true,
|
||||
willSave: true,
|
||||
willSaveWaitUntil: true,
|
||||
didSave: true,
|
||||
},
|
||||
completion: {
|
||||
dynamicRegistration: true,
|
||||
contextSupport: true,
|
||||
completionItem: {
|
||||
snippetSupport: true,
|
||||
commitCharactersSupport: true,
|
||||
documentationFormat: ['markdown', 'plaintext'],
|
||||
deprecatedSupport: true,
|
||||
preselectSupport: true,
|
||||
tagSupport: {
|
||||
valueSet: [1],
|
||||
},
|
||||
insertReplaceSupport: true,
|
||||
resolveSupport: {
|
||||
properties: ['documentation', 'detail', 'additionalTextEdits'],
|
||||
},
|
||||
insertTextModeSupport: {
|
||||
valueSet: [1, 2],
|
||||
},
|
||||
labelDetailsSupport: true,
|
||||
},
|
||||
insertTextMode: 2,
|
||||
completionItemKind: {
|
||||
valueSet: [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
||||
],
|
||||
},
|
||||
completionList: {
|
||||
itemDefaults: ['commitCharacters', 'editRange', 'insertTextFormat', 'insertTextMode'],
|
||||
},
|
||||
},
|
||||
hover: {
|
||||
dynamicRegistration: true,
|
||||
contentFormat: ['markdown', 'plaintext'],
|
||||
},
|
||||
signatureHelp: {
|
||||
dynamicRegistration: true,
|
||||
signatureInformation: {
|
||||
documentationFormat: ['markdown', 'plaintext'],
|
||||
parameterInformation: {
|
||||
labelOffsetSupport: true,
|
||||
},
|
||||
activeParameterSupport: true,
|
||||
},
|
||||
contextSupport: true,
|
||||
},
|
||||
definition: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
references: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
documentHighlight: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
documentSymbol: {
|
||||
dynamicRegistration: true,
|
||||
symbolKind: {
|
||||
valueSet: [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
|
||||
26,
|
||||
],
|
||||
},
|
||||
hierarchicalDocumentSymbolSupport: true,
|
||||
tagSupport: {
|
||||
valueSet: [1],
|
||||
},
|
||||
labelSupport: true,
|
||||
},
|
||||
codeAction: {
|
||||
dynamicRegistration: true,
|
||||
isPreferredSupport: true,
|
||||
disabledSupport: true,
|
||||
dataSupport: true,
|
||||
resolveSupport: {
|
||||
properties: ['edit'],
|
||||
},
|
||||
codeActionLiteralSupport: {
|
||||
codeActionKind: {
|
||||
valueSet: [
|
||||
'',
|
||||
'quickfix',
|
||||
'refactor',
|
||||
'refactor.extract',
|
||||
'refactor.inline',
|
||||
'refactor.rewrite',
|
||||
'source',
|
||||
'source.organizeImports',
|
||||
],
|
||||
},
|
||||
},
|
||||
honorsChangeAnnotations: false,
|
||||
},
|
||||
codeLens: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
formatting: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
rangeFormatting: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
onTypeFormatting: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
rename: {
|
||||
dynamicRegistration: true,
|
||||
prepareSupport: true,
|
||||
prepareSupportDefaultBehavior: 1,
|
||||
honorsChangeAnnotations: true,
|
||||
},
|
||||
documentLink: {
|
||||
dynamicRegistration: true,
|
||||
tooltipSupport: true,
|
||||
},
|
||||
typeDefinition: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
implementation: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
colorProvider: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
foldingRange: {
|
||||
dynamicRegistration: true,
|
||||
rangeLimit: 5000,
|
||||
lineFoldingOnly: true,
|
||||
foldingRangeKind: {
|
||||
valueSet: ['comment', 'imports', 'region'],
|
||||
},
|
||||
foldingRange: {
|
||||
collapsedText: false,
|
||||
},
|
||||
},
|
||||
declaration: {
|
||||
dynamicRegistration: true,
|
||||
linkSupport: true,
|
||||
},
|
||||
selectionRange: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
callHierarchy: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
semanticTokens: {
|
||||
dynamicRegistration: true,
|
||||
tokenTypes: [
|
||||
'namespace',
|
||||
'type',
|
||||
'class',
|
||||
'enum',
|
||||
'interface',
|
||||
'struct',
|
||||
'typeParameter',
|
||||
'parameter',
|
||||
'variable',
|
||||
'property',
|
||||
'enumMember',
|
||||
'event',
|
||||
'function',
|
||||
'method',
|
||||
'macro',
|
||||
'keyword',
|
||||
'modifier',
|
||||
'comment',
|
||||
'string',
|
||||
'number',
|
||||
'regexp',
|
||||
'operator',
|
||||
'decorator',
|
||||
],
|
||||
tokenModifiers: [
|
||||
'declaration',
|
||||
'definition',
|
||||
'readonly',
|
||||
'static',
|
||||
'deprecated',
|
||||
'abstract',
|
||||
'async',
|
||||
'modification',
|
||||
'documentation',
|
||||
'defaultLibrary',
|
||||
],
|
||||
formats: ['relative'],
|
||||
requests: {
|
||||
range: true,
|
||||
full: {
|
||||
delta: true,
|
||||
},
|
||||
},
|
||||
multilineTokenSupport: false,
|
||||
overlappingTokenSupport: false,
|
||||
serverCancelSupport: true,
|
||||
augmentsSyntaxTokens: true,
|
||||
},
|
||||
linkedEditingRange: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
typeHierarchy: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
inlineValue: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
inlayHint: {
|
||||
dynamicRegistration: true,
|
||||
resolveSupport: {
|
||||
properties: ['tooltip', 'textEdits', 'label.tooltip', 'label.location', 'label.command'],
|
||||
},
|
||||
},
|
||||
diagnostic: {
|
||||
dynamicRegistration: true,
|
||||
relatedDocumentSupport: false,
|
||||
},
|
||||
},
|
||||
window: {
|
||||
showMessage: {
|
||||
messageActionItem: {
|
||||
additionalPropertiesSupport: true,
|
||||
},
|
||||
},
|
||||
showDocument: {
|
||||
support: true,
|
||||
},
|
||||
workDoneProgress: true,
|
||||
},
|
||||
general: {
|
||||
staleRequestSupport: {
|
||||
cancel: true,
|
||||
retryOnContentModified: [
|
||||
'textDocument/semanticTokens/full',
|
||||
'textDocument/semanticTokens/range',
|
||||
'textDocument/semanticTokens/full/delta',
|
||||
],
|
||||
},
|
||||
regularExpressions: {
|
||||
engine: 'ECMAScript',
|
||||
version: 'ES2020',
|
||||
},
|
||||
markdown: {
|
||||
parser: 'marked',
|
||||
version: '1.1.0',
|
||||
},
|
||||
positionEncodings: ['utf-16'],
|
||||
},
|
||||
notebookDocument: {
|
||||
synchronization: {
|
||||
dynamicRegistration: true,
|
||||
executionSummarySupport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
initializationOptions: {
|
||||
autoFormatStrings: true,
|
||||
},
|
||||
workspaceFolders,
|
||||
};
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export class TestHost extends LimitedAccessHost {
|
||||
private readonly _options: TestHostOptions;
|
||||
|
||||
constructor(
|
||||
readonly fs: FileSystem,
|
||||
readonly testFs: vfs.TestFileSystem,
|
||||
readonly testData: FourSlashData,
|
||||
readonly projectRoots: string[],
|
||||
options?: TestHostOptions
|
||||
) {
|
||||
super();
|
||||
|
||||
this._options = options ?? new TestHostOptions();
|
||||
}
|
||||
|
||||
override get kind(): HostKind {
|
||||
return HostKind.FullAccess;
|
||||
}
|
||||
|
||||
override getPythonVersion(pythonPath?: Uri, logInfo?: string[]): PythonVersion | undefined {
|
||||
return this._options.version;
|
||||
}
|
||||
|
||||
override getPythonPlatform(logInfo?: string[]): PythonPlatform | undefined {
|
||||
return this._options.platform;
|
||||
}
|
||||
|
||||
override getPythonSearchPaths(pythonPath?: Uri, logInfo?: string[]): PythonPathResult {
|
||||
return {
|
||||
paths: this._options.searchPaths,
|
||||
prefix: Uri.empty(),
|
||||
};
|
||||
}
|
||||
|
||||
override runScript(
|
||||
pythonPath: Uri | undefined,
|
||||
scriptPath: Uri,
|
||||
args: string[],
|
||||
cwd: Uri,
|
||||
token: CancellationToken
|
||||
): Promise<ScriptOutput> {
|
||||
return this._options.runScript(pythonPath, scriptPath, args, cwd, token);
|
||||
}
|
||||
}
|
11
packages/pyright-internal/src/tests/lsp/main.ts
Normal file
11
packages/pyright-internal/src/tests/lsp/main.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/*
|
||||
* main.ts
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
* Licensed under the MIT license.
|
||||
*
|
||||
* Provides the main entrypoint to the test server when running in Node.
|
||||
*/
|
||||
|
||||
import { run } from './languageServer';
|
||||
|
||||
run();
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* webpack.config-cli.js
|
||||
* Copyright: Microsoft 2018
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const { tsconfigResolveAliases } = require('../../../../../build/lib/webpack');
|
||||
|
||||
const outPath = path.resolve(__dirname, '..', '..', '..', 'out');
|
||||
const typeshedFallback = path.resolve(__dirname, '..', '..', '..', 'typeshed-fallback');
|
||||
|
||||
/**@type {(env: any, argv: { mode: 'production' | 'development' | 'none' }) => import('webpack').Configuration}*/
|
||||
module.exports = (_, { mode }) => {
|
||||
return {
|
||||
context: __dirname,
|
||||
entry: {
|
||||
testServer: './main.ts',
|
||||
},
|
||||
target: 'node',
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
path: outPath,
|
||||
libraryTarget: 'commonjs2',
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
all: false,
|
||||
errors: true,
|
||||
warnings: true,
|
||||
publicPath: true,
|
||||
timings: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
alias: tsconfigResolveAliases('tsconfig.json'),
|
||||
},
|
||||
externals: {
|
||||
fsevents: 'commonjs2 fsevents',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Transform pre-compiled JS files to use syntax available in Node 12+.
|
||||
// esbuild is fast, so let it run on all JS files rather than matching
|
||||
// only known-bad libs.
|
||||
test: /\.js$/,
|
||||
loader: 'esbuild-loader',
|
||||
options: {
|
||||
target: 'node12',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [new CopyPlugin({ patterns: [{ from: typeshedFallback, to: 'typeshed-fallback' }] })],
|
||||
};
|
||||
};
|
@ -1,7 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out"
|
||||
"outDir": "./out",
|
||||
"paths": {
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
|
Loading…
Reference in New Issue
Block a user