Skip to content

Add integration test for restore of file-based programs #8470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ To debug unit tests locally, press <kbd>F5</kbd> in VS Code with the "Launch Tes
To debug integration tests
1. Import the `csharp-test-profile.code-profile` in VSCode to setup a clean profile in which to run integration tests. This must be imported at least once to use the launch configurations (ensure the extensions are updated in the profile).
2. Open any integration test file and <kbd>F5</kbd> launch with the correct launch configuration selected.
- For integration tests inside `test/lsptoolshost`, use either `Launch Current File slnWithCsproj Integration Tests` or `[DevKit] Launch Current File slnWithCsproj Integration Tests` (to run tests using C# + C# Dev Kit)
- For integration tests inside `test/lsptoolshost`, use either `[Roslyn] Run Current File Integration Test` or `[DevKit] Launch Current File Integration Tests` (to run tests using C# + C# Dev Kit)
- For integration tests inside `test/razor`, use `[Razor] Run Current File Integration Test`
- For integration tests inside `test/omnisharp`, use one of the `Omnisharp:` current file profiles
- For integration tests inside `test/omnisharp`, use one of the `[O#] Run Current File Integration Test` current file profiles

These will allow you to actually debug the test, but the 'Razor integration tests' configuration does not.

Expand Down
10 changes: 8 additions & 2 deletions src/lsptoolshost/projectRestore/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,14 @@ export async function restore(
);

await responsePromise.then(
(result) => result.forEach((r) => writeOutput(r)),
(err) => outputChannel.error(`[.NET Restore] ${err}`)
(result) => {
result.forEach((r) => writeOutput(r))
languageServer.fireProjectsRestoredEvent({ success: true });
},
(err) => {
outputChannel.error(`[.NET Restore] ${err}`)
languageServer.fireProjectsRestoredEvent({ success: false });
}
);
}
)
Expand Down
31 changes: 31 additions & 0 deletions src/lsptoolshost/server/languageServerEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,25 @@ export interface ServerStateChangeEvent {
workspaceLabel: string;
}

export interface ProjectsRestoredEvent {
success: boolean;
}

export interface ProjectReloadStartedEvent {
}

export interface ProjectReloadCompletedEvent {
}

/**
* Defines events that are fired by the language server.
* These events can be consumed to wait for the server to reach a certain state.
*/
export interface LanguageServerEvents {
readonly onServerStateChange: vscode.Event<ServerStateChangeEvent>;
readonly onProjectsRestored: vscode.Event<ProjectsRestoredEvent>;
readonly onProjectReloadStarted: vscode.Event<ProjectReloadStartedEvent>;
readonly onProjectReloadCompleted: vscode.Event<ProjectReloadCompletedEvent>;
}

/**
Expand All @@ -33,12 +46,30 @@ export interface LanguageServerEvents {
*/
export class RoslynLanguageServerEvents implements LanguageServerEvents, IDisposable {
public readonly onServerStateChangeEmitter = new vscode.EventEmitter<ServerStateChangeEvent>();
public readonly onProjectsRestoredEmitter = new vscode.EventEmitter<ProjectsRestoredEvent>();
public readonly onProjectReloadStartedEmitter = new vscode.EventEmitter<ProjectReloadStartedEvent>();
public readonly onProjectReloadCompletedEmitter = new vscode.EventEmitter<ProjectReloadCompletedEvent>();

public get onServerStateChange(): vscode.Event<ServerStateChangeEvent> {
return this.onServerStateChangeEmitter.event;
}

public get onProjectsRestored(): vscode.Event<ProjectsRestoredEvent> {
return this.onProjectsRestoredEmitter.event;
}

public get onProjectReloadStarted(): vscode.Event<ProjectReloadStartedEvent> {
return this.onProjectReloadStartedEmitter.event;
}

public get onProjectReloadCompleted(): vscode.Event<ProjectReloadCompletedEvent> {
return this.onProjectReloadCompletedEmitter.event;
}

dispose(): void {
this.onServerStateChangeEmitter.dispose();
this.onProjectsRestoredEmitter.dispose();
this.onProjectReloadStartedEmitter.dispose();
this.onProjectReloadCompletedEmitter.dispose();
}
}
20 changes: 20 additions & 0 deletions src/lsptoolshost/server/roslynLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export class RoslynLanguageServer {
this.registerSetTrace();
this.registerSendOpenSolution();
this.registerProjectInitialization();
this.registerProjectReloadStarted();
this.registerProjectReloadCompleted();
this.registerServerStateChanged();
this.registerServerStateTracking();
this.registerReportProjectConfiguration();
Expand Down Expand Up @@ -208,6 +210,10 @@ export class RoslynLanguageServer {
});
}

public fireProjectsRestoredEvent(eventArgs: { success: boolean }) {
this._languageServerEvents.onProjectsRestoredEmitter.fire(eventArgs);
}

private registerServerStateTracking() {
this._languageServerEvents.onServerStateChange((e) => {
this._state = e.state;
Expand Down Expand Up @@ -236,6 +242,20 @@ export class RoslynLanguageServer {
});
}

private registerProjectReloadStarted() {
this._languageClient.onNotification(RoslynProtocol.ProjectReloadStartedNotification.type, () => {
this._languageServerEvents.onProjectReloadStartedEmitter.fire({
});
});
}

private registerProjectReloadCompleted() {
this._languageClient.onNotification(RoslynProtocol.ProjectReloadCompletedNotification.type, () => {
this._languageServerEvents.onProjectReloadCompletedEmitter.fire({
});
});
}

private registerReportProjectConfiguration() {
// Store the dotnet info outside of the notification so we're not running dotnet --info every time the project changes.
let dotnetInfo: DotnetInfo | undefined = undefined;
Expand Down
12 changes: 12 additions & 0 deletions src/lsptoolshost/server/roslynProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,18 @@ export namespace ProjectInitializationCompleteNotification {
export const type = new NotificationType(method);
}

export namespace ProjectReloadStartedNotification {
export const method = 'workspace/projectReloadStarted';
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
export const type = new NotificationType(method);
}

export namespace ProjectReloadCompletedNotification {
export const method = 'workspace/projectReloadCompleted';
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
export const type = new NotificationType(method);
}

export namespace ProjectConfigurationNotification {
export const method = 'workspace/projectConfigurationTelemetry';
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import * as path from 'path';
import { describe, beforeAll, beforeEach, afterAll, test, expect, afterEach } from '@jest/globals';
import testAssetWorkspace from './testAssets/testAssetWorkspace';
import { activateCSharpExtension, closeAllEditorsAsync, openFileInWorkspaceAsync } from './integrationHelpers';
import { activateCSharpExtension, closeAllEditorsAsync, getCompletionsAsync, openFileInWorkspaceAsync } from './integrationHelpers';

describe(`Completion Tests`, () => {
beforeAll(async () => {
Expand Down Expand Up @@ -70,23 +70,4 @@ describe(`Completion Tests`, () => {
expect(methodOverrideLine).toContain('override void Method(NeedsImport n)');
expect(methodOverrideImplLine).toContain('base.Method(n);');
});

async function getCompletionsAsync(
position: vscode.Position,
triggerCharacter: string | undefined,
completionsToResolve: number
): Promise<vscode.CompletionList> {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
throw new Error('No active editor');
}

return await vscode.commands.executeCommand(
'vscode.executeCompletionItemProvider',
activeEditor.document.uri,
position,
triggerCharacter,
completionsToResolve
);
}
});
23 changes: 22 additions & 1 deletion test/lsptoolshost/integrationTests/integrationHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import testAssetWorkspace from './testAssets/testAssetWorkspace';
import { EOL, platform } from 'os';
import { describe, expect, test } from '@jest/globals';

export async function activateCSharpExtension(): Promise<void> {
export async function activateCSharpExtension(): Promise<CSharpExtensionExports> {
const csharpExtension = vscode.extensions.getExtension<CSharpExtensionExports>('ms-dotnettools.csharp');
if (!csharpExtension) {
throw new Error('Failed to find installation of ms-dotnettools.csharp');
Expand Down Expand Up @@ -53,6 +53,8 @@ export async function activateCSharpExtension(): Promise<void> {
if (shouldRestart) {
await restartLanguageServer();
}

return csharpExtension.exports;
}

export function usingDevKit(): boolean {
Expand Down Expand Up @@ -113,6 +115,25 @@ export function isSlnWithGenerator(workspace: typeof vscode.workspace) {
return isGivenSln(workspace, 'slnWithGenerator');
}

export async function getCompletionsAsync(
position: vscode.Position,
triggerCharacter: string | undefined,
completionsToResolve: number
): Promise<vscode.CompletionList> {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
throw new Error('No active editor');
}

return await vscode.commands.executeCommand(
'vscode.executeCompletionItemProvider',
activeEditor.document.uri,
position,
triggerCharacter,
completionsToResolve
);
}

export async function getCodeLensesAsync(): Promise<vscode.CodeLens[]> {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
Expand Down
71 changes: 71 additions & 0 deletions test/lsptoolshost/integrationTests/restore.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as path from 'path';
import testAssetWorkspace from './testAssets/testAssetWorkspace';
import {
activateCSharpExtension,
closeAllEditorsAsync,
getCompletionsAsync,
openFileInWorkspaceAsync,
revertActiveFile,
sleep,
waitForExpectedResult,
} from './integrationHelpers';
import { describe, beforeAll, beforeEach, afterAll, test, expect, afterEach } from '@jest/globals';
import { CSharpExtensionExports } from '../../../src/csharpExtensionExports';

const timeout = 15*60*1000;

describe(`Restore Tests`, () => {
let exports: CSharpExtensionExports;

beforeAll(async () => {
exports = await activateCSharpExtension();
});

beforeEach(async () => {
await openFileInWorkspaceAsync(path.join('src', 'scripts', 'app1.cs'));
});

afterEach(async () => {
await revertActiveFile();
await closeAllEditorsAsync();
});

afterAll(async () => {
await testAssetWorkspace.cleanupWorkspace();
});

test('Inserting package directive triggers a restore', async () => {
await sleep(1);
await vscode.window.activeTextEditor!.edit((editBuilder) => {
editBuilder.insert(new vscode.Position(0, 0), '#:package [email protected]');
});
await vscode.window.activeTextEditor!.document.save();

let designTimeBuildFinished = false;
exports.experimental.languageServerEvents.onProjectsRestored(() => {
// Restore has finished. Now subscribe to the next design time build.
// TODO: seems racy.
exports.experimental.languageServerEvents.onProjectReloadCompleted(() => {
designTimeBuildFinished = true;
});
});

await waitForExpectedResult<boolean>(
() => designTimeBuildFinished,
timeout,
100,
(designTimeBuildFinished) => expect(designTimeBuildFinished).toBe(true)
);

// we restored, then completed a design-time build.
const completionItems = await getCompletionsAsync(new vscode.Position(1, "using Newton".length), undefined, 10);

expect(completionItems.items.map(item => item.label)).toContain("Newtonsoft");
}, timeout);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

using Newton;

Console.WriteLine("Hello World!");
Loading