From 83ecd67947721dacd49b89908e9ba4779dd4e5bb Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Sat, 9 May 2020 18:55:30 -0700 Subject: [PATCH] Add integration test for SemanticTokensProvider --- .travis.yml | 2 +- package.json | 3 +- src/features/commands.ts | 16 +++ src/omnisharp/extension.ts | 10 ++ .../launchConfiguration.integration.test.ts | 8 +- test/integrationTests/poll.ts | 4 +- .../semanticTokensProvider.test.ts | 135 ++++++++++++++++++ .../testAssets/singleCsproj/semantictokens.cs | 11 ++ .../slnWithCsproj/src/app/semantictokens.cs | 11 ++ ...virtualDocumentTracker.integration.test.ts | 24 ++-- 10 files changed, 204 insertions(+), 20 deletions(-) create mode 100644 test/integrationTests/semanticTokensProvider.test.ts create mode 100644 test/integrationTests/testAssets/singleCsproj/semantictokens.cs create mode 100644 test/integrationTests/testAssets/slnWithCsproj/src/app/semantictokens.cs diff --git a/.travis.yml b/.travis.yml index 373055190a..e8f945f325 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ mono: - latest env: -- CODE_VERSION=1.36.0 +- CODE_VERSION=1.45.0 before_install: - if [ $TRAVIS_OS_NAME == "linux" ]; then diff --git a/package.json b/package.json index 7d365a2992..b4b58e0f76 100644 --- a/package.json +++ b/package.json @@ -646,7 +646,8 @@ "csharp.semanticHighlighting.enabled": { "type": "boolean", "default": false, - "description": "Enable/disable Semantic Highlighting for C# files (Razor files currently unsupported). Defaults to false. Close open files for changes to take effect." + "description": "Enable/disable Semantic Highlighting for C# files (Razor files currently unsupported). Defaults to false. Close open files for changes to take effect.", + "scope": "window" }, "omnisharp.path": { "type": [ diff --git a/src/features/commands.ts b/src/features/commands.ts index 02050de9b2..1e0d4db075 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -22,6 +22,7 @@ import OptionProvider from '../observers/OptionProvider'; import reportIssue from './reportIssue'; import { IMonoResolver } from '../constants/IMonoResolver'; import { getDotnetInfo } from '../utils/getDotnetInfo'; +import { getSemanticTokensProvider } from '../omnisharp/extension'; export default function registerCommands(server: OmniSharpServer, platformInfo: PlatformInformation, eventStream: EventStream, optionProvider: OptionProvider, monoResolver: IMonoResolver, packageJSON: any, extensionPath: string): CompositeDisposable { let disposable = new CompositeDisposable(); @@ -54,9 +55,24 @@ export default function registerCommands(server: OmniSharpServer, platformInfo: disposable.add(vscode.commands.registerCommand('csharp.clrAdapterExecutableCommand', async (args) => getAdapterExecutionCommand(platformInfo, eventStream, packageJSON, extensionPath))); disposable.add(vscode.commands.registerCommand('csharp.reportIssue', async () => reportIssue(vscode, eventStream, getDotnetInfo, platformInfo.isValidPlatformForMono(), optionProvider.GetLatestOptions(), monoResolver))); + if (process.env.OSVC_SUITE !== undefined) { + // Register commands used for integration tests. + disposable.add(vscode.commands.registerCommand('csharp.private.getSemanticTokensLegend', async () => getSemanticTokensLegend())); + disposable.add(vscode.commands.registerCommand('csharp.private.getSemanticTokens', async (fileUri) => await getSemanticTokens(fileUri))); + } + return new CompositeDisposable(disposable); } +function getSemanticTokensLegend() { + return getSemanticTokensProvider().getLegend(); +} + +async function getSemanticTokens(fileUri: vscode.Uri) { + const document = await vscode.workspace.openTextDocument(fileUri); + return await getSemanticTokensProvider().provideDocumentSemanticTokens(document, null); +} + function restartOmniSharp(server: OmniSharpServer) { if (server.isRunning()) { server.restart(); diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index f62498bf0f..33a719a1d4 100644 --- a/src/omnisharp/extension.ts +++ b/src/omnisharp/extension.ts @@ -46,6 +46,11 @@ export interface ActivationResult { readonly advisor: Advisor; } +let _semanticTokensProvider: SemanticTokensProvider; +export function getSemanticTokensProvider() { + return _semanticTokensProvider; +} + export async function activate(context: vscode.ExtensionContext, packageJSON: any, platformInfo: PlatformInformation, provider: NetworkSettingsProvider, eventStream: EventStream, optionProvider: OptionProvider, extensionPath: string) { const documentSelector: vscode.DocumentSelector = { language: 'csharp', @@ -95,6 +100,11 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an localDisposables.add(vscode.languages.registerFoldingRangeProvider(documentSelector, new StructureProvider(server, languageMiddlewareFeature))); const semanticTokensProvider = new SemanticTokensProvider(server, optionProvider, languageMiddlewareFeature); + // Make the semantic token provider available for testing + if (process.env.OSVC_SUITE !== undefined) { + _semanticTokensProvider = semanticTokensProvider; + } + localDisposables.add(vscode.languages.registerDocumentSemanticTokensProvider(documentSelector, semanticTokensProvider, semanticTokensProvider.getLegend())); localDisposables.add(vscode.languages.registerDocumentRangeSemanticTokensProvider(documentSelector, semanticTokensProvider, semanticTokensProvider.getLegend())); })); diff --git a/test/integrationTests/launchConfiguration.integration.test.ts b/test/integrationTests/launchConfiguration.integration.test.ts index 366d84ee80..e712ceaf3b 100644 --- a/test/integrationTests/launchConfiguration.integration.test.ts +++ b/test/integrationTests/launchConfiguration.integration.test.ts @@ -38,7 +38,8 @@ suite(`Tasks generation: ${testAssetWorkspace.description}`, function () { test("Starting .NET Core Launch (console) from the workspace root should create an Active Debug Session", async () => { - vscode.debug.onDidChangeActiveDebugSession((e) => { + const onChangeSubscription = vscode.debug.onDidChangeActiveDebugSession((e) => { + onChangeSubscription.dispose(); expect(vscode.debug.activeDebugSession).not.to.be.undefined; expect(vscode.debug.activeDebugSession.type).to.equal("coreclr"); }); @@ -47,7 +48,10 @@ suite(`Tasks generation: ${testAssetWorkspace.description}`, function () { expect(result, "Debugger could not be started."); let debugSessionTerminated = new Promise(resolve => { - vscode.debug.onDidTerminateDebugSession((e) => resolve()); + const onTerminateSubscription = vscode.debug.onDidTerminateDebugSession((e) => { + onTerminateSubscription.dispose(); + resolve(); + }); }); await debugSessionTerminated; diff --git a/test/integrationTests/poll.ts b/test/integrationTests/poll.ts index 6bc021b03b..97f005af75 100644 --- a/test/integrationTests/poll.ts +++ b/test/integrationTests/poll.ts @@ -44,7 +44,7 @@ export async function assertWithPoll( } function defaultPollExpression(value: T): boolean { - return value !== undefined && ((Array.isArray(value) && value.length > 0) || !Array.isArray(value)); + return value !== undefined && ((Array.isArray(value) && value.length > 0) || (!Array.isArray(value) && !!value)); } export async function pollDoesNotHappen( @@ -68,7 +68,7 @@ export async function pollDoesNotHappen( } export async function poll( - getValue: () => T, + getValue: () => Promise | T, duration: number, step: number, expression: (input: T) => boolean = defaultPollExpression): Promise { diff --git a/test/integrationTests/semanticTokensProvider.test.ts b/test/integrationTests/semanticTokensProvider.test.ts new file mode 100644 index 0000000000..0bfcef7c8d --- /dev/null +++ b/test/integrationTests/semanticTokensProvider.test.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { should, assert } from 'chai'; +import { activateCSharpExtension, isRazorWorkspace } from './integrationHelpers'; +import testAssetWorkspace from './testAssets/testAssetWorkspace'; +import { EventType } from '../../src/omnisharp/EventType'; +import { poll } from './poll'; + +const chai = require('chai'); +chai.use(require('chai-arrays')); +chai.use(require('chai-fs')); + +interface ExpectedToken { + startLine: number; + character: number; + length: number; + tokenClassifiction: string; +} + +async function assertTokens(fileUri: vscode.Uri, expected: ExpectedToken[] | null, message?: string): Promise { + + const legend = await vscode.commands.executeCommand("csharp.private.getSemanticTokensLegend"); + const actual = await vscode.commands.executeCommand("csharp.private.getSemanticTokens", fileUri); + + if (actual === null) { + assert.isNull(expected, message); + return; + } + + let actualRanges = []; + let lastLine = 0; + let lastCharacter = 0; + for (let i = 0; i < actual.data.length; i += 5) { + const lineDelta = actual.data[i], charDelta = actual.data[i + 1], len = actual.data[i + 2], typeIdx = actual.data[i + 3], modSet = actual.data[i + 4]; + const line = lastLine + lineDelta; + const character = lineDelta === 0 ? lastCharacter + charDelta : charDelta; + const tokenClassifiction = [legend.tokenTypes[typeIdx], ...legend.tokenModifiers.filter((_, i) => modSet & 1 << i)].join('.'); + actualRanges.push(t(line, character, len, tokenClassifiction)); + lastLine = line; + lastCharacter = character; + } + assert.deepEqual(actualRanges, expected, message); +} + +suite(`SemanticTokensProvider: ${testAssetWorkspace.description}`, function () { + let fileUri: vscode.Uri; + + suiteSetup(async function () { + should(); + + // These tests don't run on the BasicRazorApp2_1 solution + if (isRazorWorkspace(vscode.workspace)) { + this.skip(); + } + + const activation = await activateCSharpExtension(); + await testAssetWorkspace.restore(); + + // Wait for workspace information to be returned + let isWorkspaceLoaded = false; + + const subscription = activation.eventStream.subscribe(event => { + if (event.type === EventType.WorkspaceInformationUpdated) { + isWorkspaceLoaded = true; + subscription.unsubscribe(); + } + }); + + await poll(() => isWorkspaceLoaded, 25000, 500); + + const fileName = 'semantictokens.cs'; + const projectDirectory = testAssetWorkspace.projects[0].projectDirectoryPath; + + fileUri = vscode.Uri.file(path.join(projectDirectory, fileName)); + }); + + test('Semantic Highlighting returns null when disabled', async () => { + let csharpConfig = vscode.workspace.getConfiguration('csharp'); + await csharpConfig.update('semanticHighlighting.enabled', false, vscode.ConfigurationTarget.Global); + + await assertTokens(fileUri, /*expected*/ null); + }); + + test('Semantic Highlighting returns classified tokens when enabled', async () => { + let csharpConfig = vscode.workspace.getConfiguration('csharp'); + await csharpConfig.update('semanticHighlighting.enabled', true, vscode.ConfigurationTarget.Global); + + await assertTokens(fileUri, [ + // 0:namespace Test + _keyword("namespace", 0, 0), _namespace("Test", 0, 10), + // 1:{ + _punctuation("{", 1, 0), + // 2: public class TestProgram + _keyword("public", 2, 4), _keyword("class", 2, 11), _class("TestProgram", 2, 17), + // 3: { + _punctuation("{", 3, 4), + // 4: public static int TestMain(string[] args) + _keyword("public", 4, 8), _keyword("static", 4, 15), _keyword("int", 4, 22), _staticMethod("TestMain", 4, 26), _punctuation("(", 4, 34), _keyword("string", 4, 35), _punctuation("[", 4, 41), _punctuation("]", 4, 42), _parameter("args", 4, 44), _punctuation(")", 4, 48), + // 5: { + _punctuation("{", 5, 8), + // 6: System.Console.WriteLine(string.Join(',', args)); + _namespace("System", 6, 12), _operator(".", 6, 18), _staticClass("Console", 6, 19), _operator(".", 6, 26), _staticMethod("WriteLine", 6, 27), _punctuation("(", 6, 36), _keyword("string", 6, 37), _operator(".", 6, 43), _staticMethod("Join", 6, 44), _punctuation("(", 6, 48), _string("','", 6, 49), _punctuation(")", 6, 52), _parameter("args", 6, 54), _punctuation(")", 6, 58), _punctuation(")", 6, 59), _punctuation(";", 6, 60), + // 7: return 0; + _controlKeyword("return", 7, 12), _number("0", 7, 19), _punctuation(";", 7, 20), + // 8: } + _punctuation("}", 8, 8), + // 9: } + _punctuation("}", 9, 4), + //10: } + _punctuation("}", 10, 0), + ]); + }); +}); + +function t(startLine: number, character: number, length: number, tokenClassifiction: string): ExpectedToken { + return { startLine, character, length, tokenClassifiction }; +} + +const _keyword = (text: string, line: number, col: number) => t(line, col, text.length, "plainKeyword"); +const _controlKeyword = (text: string, line: number, col: number) => t(line, col, text.length, "controlKeyword"); +const _punctuation = (text: string, line: number, col: number) => t(line, col, text.length, "punctuation"); +const _operator = (text: string, line: number, col: number) => t(line, col, text.length, "operator"); +const _number = (text: string, line: number, col: number) => t(line, col, text.length, "number"); +const _string = (text: string, line: number, col: number) => t(line, col, text.length, "string"); +const _namespace = (text: string, line: number, col: number) => t(line, col, text.length, "namespace"); +const _class = (text: string, line: number, col: number) => t(line, col, text.length, "class"); +const _staticClass = (text: string, line: number, col: number) => t(line, col, text.length, "class.static"); +const _staticMethod = (text: string, line: number, col: number) => t(line, col, text.length, "member.static"); +const _parameter = (text: string, line: number, col: number) => t(line, col, text.length, "parameter"); diff --git a/test/integrationTests/testAssets/singleCsproj/semantictokens.cs b/test/integrationTests/testAssets/singleCsproj/semantictokens.cs new file mode 100644 index 0000000000..055b674b53 --- /dev/null +++ b/test/integrationTests/testAssets/singleCsproj/semantictokens.cs @@ -0,0 +1,11 @@ +namespace Test +{ + public class TestProgram + { + public static int TestMain(string[] args) + { + System.Console.WriteLine(string.Join(',', args)); + return 0; + } + } +} diff --git a/test/integrationTests/testAssets/slnWithCsproj/src/app/semantictokens.cs b/test/integrationTests/testAssets/slnWithCsproj/src/app/semantictokens.cs new file mode 100644 index 0000000000..055b674b53 --- /dev/null +++ b/test/integrationTests/testAssets/slnWithCsproj/src/app/semantictokens.cs @@ -0,0 +1,11 @@ +namespace Test +{ + public class TestProgram + { + public static int TestMain(string[] args) + { + System.Console.WriteLine(string.Join(',', args)); + return 0; + } + } +} diff --git a/test/integrationTests/virtualDocumentTracker.integration.test.ts b/test/integrationTests/virtualDocumentTracker.integration.test.ts index fc587d9e15..a2a231d421 100644 --- a/test/integrationTests/virtualDocumentTracker.integration.test.ts +++ b/test/integrationTests/virtualDocumentTracker.integration.test.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; -import { should, expect } from 'chai'; +import { should, assert } from 'chai'; import { activateCSharpExtension } from './integrationHelpers'; import testAssetWorkspace from './testAssets/testAssetWorkspace'; import { IDisposable } from '../../src/Disposable'; @@ -15,16 +15,19 @@ chai.use(require('chai-arrays')); chai.use(require('chai-fs')); suite(`Virtual Document Tracking ${testAssetWorkspace.description}`, function () { - let virtualScheme: string = "virtual"; + const virtualScheme = "virtual"; let virtualDocumentRegistration: IDisposable; + let virtualUri: vscode.Uri; suiteSetup(async function () { should(); - await activateCSharpExtension(); - await testAssetWorkspace.restore(); - let virtualCSharpDocumentProvider = new VirtualCSharpDocumentProvider(); + const virtualCSharpDocumentProvider = new VirtualCSharpDocumentProvider(); virtualDocumentRegistration = vscode.workspace.registerTextDocumentContentProvider(virtualScheme, virtualCSharpDocumentProvider); + virtualUri = vscode.Uri.parse(`${virtualScheme}://${testAssetWorkspace.projects[0].projectDirectoryPath}/_virtualFile.cs`); + + await activateCSharpExtension(); + await testAssetWorkspace.restore(); }); suiteTeardown(async () => { @@ -33,19 +36,12 @@ suite(`Virtual Document Tracking ${testAssetWorkspace.description}`, function () }); test("Virtual documents are operated on.", async () => { - let virtualUri = vscode.Uri.parse(`${virtualScheme}://${testAssetWorkspace.projects[0].projectDirectoryPath}/_virtualFile.cs`); await vscode.workspace.openTextDocument(virtualUri); - let position = new vscode.Position(2, 4); + let position = new vscode.Position(2, 0); let completionItems = await vscode.commands.executeCommand("vscode.executeCompletionItemProvider", virtualUri, position); - expect(completionItems.items).to.satisfy(() => { - let item = completionItems.items.find(item => { - return item.label === "while"; - }); - - return item; - }); + assert.include(completionItems.items.map(({ label }) => label), "while"); }); });