runtimes/testing/TestFeatures.ts (204 lines of code) (raw):

import { Server, CredentialsProvider, Logging, Lsp, Telemetry, Workspace, Chat, Runtime, Notification, SDKClientConstructorV2, SDKClientConstructorV3, SDKInitializator, Agent, } from '../server-interface' import { StubbedInstance, stubInterface } from 'ts-sinon' import { CancellationToken, CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, DocumentFormattingParams, ExecuteCommandParams, HoverParams, InlineCompletionParams, SemanticTokensParams, TextDocument, SignatureHelpParams, UpdateConfigurationParams, InitializeParams, } from '../protocol' import { IdentityManagement } from '../server-interface/identity-management' import { Service } from 'aws-sdk' import { ServiceConfigurationOptions } from 'aws-sdk/lib/service' /** * A test helper package to test Server implementations. Accepts a single callback * registration for each LSP event. You can use this helper to trigger LSP events * as many times as you want, calling the first registered callback with the event * params. * * You could instrument the stubs for all Features, but this is discouraged over * testing the effects and responses. */ export class TestFeatures { chat: StubbedInstance<Chat> credentialsProvider: StubbedInstance<CredentialsProvider> // TODO: This needs to improve, somehow sinon doesn't stub nested objects lsp: StubbedInstance<Lsp> & { workspace: StubbedInstance<Lsp['workspace']> } & { extensions: StubbedInstance<Lsp['extensions']> } workspace: StubbedInstance<Workspace> logging: StubbedInstance<Logging> telemetry: StubbedInstance<Telemetry> documents: { [uri: string]: TextDocument } runtime: StubbedInstance<Runtime> identityManagement: StubbedInstance<IdentityManagement> notification: StubbedInstance<Notification> sdkInitializator: SDKInitializator agent: Agent private disposables: (() => void)[] = [] constructor() { this.chat = stubInterface<Chat>() this.credentialsProvider = stubInterface<CredentialsProvider>() this.lsp = stubInterface< Lsp & { workspace: StubbedInstance<Lsp['workspace']> } & { extensions: StubbedInstance<Lsp['extensions']> } >() this.lsp.workspace = stubInterface<typeof this.lsp.workspace>() this.lsp.extensions = stubInterface<typeof this.lsp.extensions>() this.workspace = stubInterface<Workspace>() this.logging = stubInterface<Logging>() this.telemetry = stubInterface<Telemetry>() this.documents = {} this.runtime = stubInterface<Runtime>() this.identityManagement = stubInterface<IdentityManagement>() this.notification = stubInterface<Notification>() this.sdkInitializator = Object.assign( // Default callable function for v3 clients <T, P>(Ctor: SDKClientConstructorV3<T, P>, current_config: P): T => new Ctor({ ...current_config }), // Property for v2 clients { v2: <T extends Service, P extends ServiceConfigurationOptions>( Ctor: SDKClientConstructorV2<T, P>, current_config: P ): T => new Ctor({ ...current_config }), } ) this.workspace.getTextDocument.callsFake(async uri => this.documents[uri]) this.workspace.getAllTextDocuments.callsFake(async () => Object.values(this.documents)) this.agent = stubInterface<Agent>() } /** * Instantiates the server with `this` (`TestFeatures`) and simulates starting it by * invoking the initialized-notification handler. * * @remarks * * For also triggering the initializer handler, and simulating a full LSP handshake, * use`initialize` instead. */ async start(server: Server) { this.disposables.push(server(this)) return Promise.resolve(this).then(f => { this.doSendInitializedNotification() return f }) } /** * Instantiates the server with `this` (`TestFeatures`) and simulates the LSP handshake * by invoking the initializer handler followed by the initialized-notification handler. * * In case of no prior call to `setClientParams`and no `clientParams` are passed as argument, * the clientParams default to `{}`. * * If `clientParams` are passed, they take precedence over any previously configured params * and override them. */ async initialize(server: Server, clientParams?: InitializeParams, token?: CancellationToken) { this.disposables.push(server(this)) const params = clientParams ?? (this.lsp.getClientInitializeParams() || ({} as InitializeParams)) this.setClientParams(params) return Promise.resolve(this).then(f => { this.doSendInitializeRequest(params, token || ({} as CancellationToken)) this.doSendInitializedNotification() return f }) } async doInlineCompletion(params: InlineCompletionParams, token: CancellationToken) { return this.lsp.onInlineCompletion.args[0]?.[0](params, token) } async doCompletion(params: CompletionParams, token: CancellationToken) { return this.lsp.onCompletion.args[0]?.[0](params, token) } async doSemanticTokens(params: SemanticTokensParams, token: CancellationToken) { return this.lsp.onSemanticTokens.args[0]?.[0](params, token) } async doFormat(params: DocumentFormattingParams, token: CancellationToken) { return this.lsp.onDidFormatDocument.args[0]?.[0](params, token) } async doHover(params: HoverParams, token: CancellationToken) { return this.lsp.onHover.args[0]?.[0](params, token) } async doSignatureHelp(params: SignatureHelpParams, token: CancellationToken) { return this.lsp.onSignatureHelp.args[0]?.[0](params, token) } async doInlineCompletionWithReferences( ...args: Parameters<Parameters<Lsp['extensions']['onInlineCompletionWithReferences']>[0]> ) { return this.lsp.extensions.onInlineCompletionWithReferences.args[0]?.[0](...args) } async doLogInlineCompletionSessionResults( ...args: Parameters<Parameters<Lsp['extensions']['onLogInlineCompletionSessionResults']>[0]> ) { return this.lsp.extensions.onLogInlineCompletionSessionResults.args[0]?.[0](...args) } openDocument(document: TextDocument) { this.documents[document.uri] = document return this } async doChangeConfiguration() { // Force the call to handle after the current task completes await undefined this.lsp.didChangeConfiguration.args[0]?.[0]({ settings: undefined }) return this } async doChangeTextDocument(params: DidChangeTextDocumentParams) { // Force the call to handle after the current task completes await undefined this.lsp.onDidChangeTextDocument.args[0]?.[0](params) return this } async doOpenTextDocument(params: DidOpenTextDocumentParams) { // Force the call to handle after the current task completes await undefined this.lsp.onDidOpenTextDocument.args[0]?.[0](params) return this } async doExecuteCommand(params: ExecuteCommandParams, token: CancellationToken) { return this.lsp.onExecuteCommand.args[0]?.[0](params, token) } async doUpdateConfiguration(params: UpdateConfigurationParams, token: CancellationToken) { return this.lsp.workspace.onUpdateConfiguration.args[0]?.[0](params, token) } async simulateTyping(uri: string, text: string) { let remainder = text while (remainder.length > 0) { const document = this.documents[uri]! const contentChange = remainder.substring(0, 1) remainder = remainder.substring(1) const newDocument = TextDocument.create( document.uri, document.languageId, document.version + 1, document.getText() + contentChange ) this.documents[uri] = newDocument const endPosition = document.positionAt(document.getText().length) const range = { start: endPosition, end: endPosition, } // Force the call to handle after the current task completes await undefined this.lsp.onDidChangeTextDocument.args[0]?.[0]({ textDocument: { uri, version: document.version, }, contentChanges: [ { range, text: contentChange, }, ], }) } return this } doSendInitializeRequest(clientParams: InitializeParams, token: CancellationToken) { this.lsp.addInitializer.args[0]?.[0](clientParams, token) } doSendInitializedNotification() { this.lsp.onInitialized.args[0]?.[0]({}) } setClientParams(clientParams: InitializeParams) { this.lsp.getClientInitializeParams.returns(clientParams) } resetClientParams() { this.lsp.getClientInitializeParams.returns(undefined) } dispose() { this.disposables.forEach(d => d()) } }