diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2aa05bbe2..a2ade83006 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,9 +64,9 @@ To debug unit tests locally, press F5 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 F5 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. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 28ad63bfeb..bee7392a24 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -72,6 +72,8 @@ stages: - stage: displayName: Test Linux (.NET 8) dependsOn: [] + variables: + ROSLYN_SKIP_TEST_FILE_BASED_PROGRAMS: 'true' jobs: - template: azure-pipelines/test-matrix.yml parameters: @@ -83,11 +85,13 @@ stages: pool: name: NetCore-Public demands: ImageOverride -equals 1es-ubuntu-2004-open - containerName: mcr.microsoft.com/dotnet/sdk:8.0 + containerName: mcr.microsoft.com/dotnet/sdk:8.0-noble - stage: displayName: Test Linux (.NET 9) dependsOn: [] + variables: + ROSLYN_SKIP_TEST_FILE_BASED_PROGRAMS: 'true' jobs: - template: azure-pipelines/test-matrix.yml parameters: @@ -99,11 +103,29 @@ stages: pool: name: NetCore-Public demands: ImageOverride -equals 1es-ubuntu-2004-open - containerName: mcr.microsoft.com/dotnet/sdk:9.0 + containerName: mcr.microsoft.com/dotnet/sdk:9.0-noble + +- stage: + displayName: Test Linux (.NET 10) + dependsOn: [] + jobs: + - template: azure-pipelines/test-matrix.yml + parameters: + os: linux + # Prefer the dotnet from the container. + installDotNet: false + testVSCodeVersion: $(testVSCodeVersion) + installAdditionalLinuxDependencies: true + pool: + name: NetCore-Public + demands: ImageOverride -equals 1es-ubuntu-2004-open + containerName: mcr.microsoft.com/dotnet/sdk:10.0.100-rc.2-noble - stage: Test_Windows_Stage displayName: Test Windows dependsOn: [] + variables: + ROSLYN_SKIP_TEST_FILE_BASED_PROGRAMS: 'true' jobs: - template: azure-pipelines/test-matrix.yml parameters: @@ -117,6 +139,8 @@ stages: - stage: Test_MacOS_Stage displayName: Test MacOS dependsOn: [] + variables: + ROSLYN_SKIP_TEST_FILE_BASED_PROGRAMS: 'true' jobs: - template: azure-pipelines/test-matrix.yml parameters: diff --git a/azure-pipelines/test-linux-docker-prereqs.yml b/azure-pipelines/test-linux-docker-prereqs.yml index e89c6bdcfc..70a1a81a6b 100644 --- a/azure-pipelines/test-linux-docker-prereqs.yml +++ b/azure-pipelines/test-linux-docker-prereqs.yml @@ -13,7 +13,7 @@ steps: # Installing the dependencies requires root, but the docker image we're using doesn't have root permissions (nor sudo) # We can exec as root from outside the container from the host machine. # Really we should create our own image with these pre-installed, but we'll need to figure out how to publish the image. -- script: docker exec --user root $(containerId) bash -c 'apt-get update -y && apt-get install -y libglib2.0-0 libnss3 libatk-bridge2.0-dev libdrm2 libgtk-3-0 libgbm-dev libasound2 xvfb' +- script: docker exec --user root $(containerId) bash -c 'apt-get update -y && apt-get install -y libglib2.0-0 libnss3 libatk-bridge2.0-dev libdrm2 libgtk-3-0 libgbm-dev libasound2t64 xvfb' displayName: 'Install additional Linux dependencies' target: host condition: eq(variables['Agent.OS'], 'Linux') \ No newline at end of file diff --git a/test/lsptoolshost/integrationTests/completion.integration.test.ts b/test/lsptoolshost/integrationTests/completion.integration.test.ts index fa3a150082..4924a9a99a 100644 --- a/test/lsptoolshost/integrationTests/completion.integration.test.ts +++ b/test/lsptoolshost/integrationTests/completion.integration.test.ts @@ -7,7 +7,12 @@ 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 () => { @@ -70,23 +75,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 { - 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 - ); - } }); diff --git a/test/lsptoolshost/integrationTests/fileBasedPrograms.integration.test.ts b/test/lsptoolshost/integrationTests/fileBasedPrograms.integration.test.ts new file mode 100644 index 0000000000..3f87d61e9b --- /dev/null +++ b/test/lsptoolshost/integrationTests/fileBasedPrograms.integration.test.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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, + waitForAllAsyncOperationsAsync, + waitForExpectedResult, + describeIfFileBasedPrograms, +} from './integrationHelpers'; +import { beforeAll, beforeEach, afterAll, test, expect, afterEach } from '@jest/globals'; +import { CSharpExtensionExports } from '../../../src/csharpExtensionExports'; + +describeIfFileBasedPrograms(`File-based Programs Tests`, () => { + let exports: CSharpExtensionExports; + + beforeAll(async () => { + process.env.RoslynWaiterEnabled = 'true'; + 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 vscode.window.activeTextEditor!.edit((editBuilder) => { + editBuilder.insert(new vscode.Position(0, 0), '#:package Newtonsoft.Json@13.0.3'); + editBuilder.insert(new vscode.Position(1, 0), 'using Newton'); + }); + await vscode.window.activeTextEditor!.document.save(); + await waitForAllAsyncOperationsAsync(exports); + + const position = new vscode.Position(1, 'using Newton'.length); + await waitForExpectedResult( + async () => getCompletionsAsync(position, undefined, 10), + 10 * 1000, + 100, + (completionItems) => expect(completionItems.items.map((item) => item.label)).toContain('Newtonsoft') + ); + }); +}); diff --git a/test/lsptoolshost/integrationTests/integrationHelpers.ts b/test/lsptoolshost/integrationTests/integrationHelpers.ts index b4e2e8ea90..4f87296490 100644 --- a/test/lsptoolshost/integrationTests/integrationHelpers.ts +++ b/test/lsptoolshost/integrationTests/integrationHelpers.ts @@ -12,8 +12,9 @@ import { ServerState } from '../../../src/lsptoolshost/server/languageServerEven import testAssetWorkspace from './testAssets/testAssetWorkspace'; import { EOL, platform } from 'os'; import { describe, expect, test } from '@jest/globals'; +import { WaitForAsyncOperationsRequest } from './testHooks'; -export async function activateCSharpExtension(): Promise { +export async function activateCSharpExtension(): Promise { const csharpExtension = vscode.extensions.getExtension('ms-dotnettools.csharp'); if (!csharpExtension) { throw new Error('Failed to find installation of ms-dotnettools.csharp'); @@ -53,6 +54,8 @@ export async function activateCSharpExtension(): Promise { if (shouldRestart) { await restartLanguageServer(); } + + return csharpExtension.exports; } export function usingDevKit(): boolean { @@ -113,6 +116,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 { + 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 { const activeEditor = vscode.window.activeTextEditor; if (!activeEditor) { @@ -278,6 +300,10 @@ export const testIfDevKit = testIf(usingDevKit()); export const testIfNotMacOS = testIf(!isMacOS()); export const testIfWindows = testIf(isWindows()); +const runFileBasedProgramsTests = process.env['ROSLYN_SKIP_TEST_FILE_BASED_PROGRAMS'] !== 'true'; +console.log(`process.env.ROSLYN_SKIP_TEST_FILE_BASED_PROGRAMS: ${process.env.ROSLYN_SKIP_TEST_FILE_BASED_PROGRAMS}`); +export const describeIfFileBasedPrograms = describeIf(runFileBasedProgramsTests); + function describeIf(condition: boolean) { return condition ? describe : describe.skip; } @@ -299,3 +325,8 @@ function isWindows() { function isLinux() { return !(isMacOS() || isWindows()); } + +export async function waitForAllAsyncOperationsAsync(exports: CSharpExtensionExports): Promise { + const source = new vscode.CancellationTokenSource(); + await exports.experimental.sendServerRequest(WaitForAsyncOperationsRequest.type, { operations: [] }, source.token); +} diff --git a/test/lsptoolshost/integrationTests/testAssets/slnWithCsproj/src/scripts/Directory.Build.props b/test/lsptoolshost/integrationTests/testAssets/slnWithCsproj/src/scripts/Directory.Build.props new file mode 100644 index 0000000000..028c8dee38 --- /dev/null +++ b/test/lsptoolshost/integrationTests/testAssets/slnWithCsproj/src/scripts/Directory.Build.props @@ -0,0 +1,6 @@ + + + + $(MSBuildThisFileDirectory) + + \ No newline at end of file diff --git a/test/lsptoolshost/integrationTests/testAssets/slnWithCsproj/src/scripts/app1.cs b/test/lsptoolshost/integrationTests/testAssets/slnWithCsproj/src/scripts/app1.cs new file mode 100644 index 0000000000..0655ddff83 --- /dev/null +++ b/test/lsptoolshost/integrationTests/testAssets/slnWithCsproj/src/scripts/app1.cs @@ -0,0 +1,4 @@ + + + +Console.WriteLine("Hello World!"); diff --git a/test/lsptoolshost/integrationTests/testHooks.ts b/test/lsptoolshost/integrationTests/testHooks.ts new file mode 100644 index 0000000000..3fd6936114 --- /dev/null +++ b/test/lsptoolshost/integrationTests/testHooks.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as lsp from 'vscode-languageserver-protocol'; + +export interface WaitForAsyncOperationsParams { + /** + * The operations to wait for. + */ + operations: string[]; +} + +export interface WaitForAsyncOperationsResponse {} // eslint-disable-line @typescript-eslint/no-empty-object-type + +export namespace WaitForAsyncOperationsRequest { + export const method = 'workspace/waitForAsyncOperations'; + export const messageDirection: lsp.MessageDirection = lsp.MessageDirection.clientToServer; + export const type = new lsp.RequestType(method); +}