Skip to content

Commit

Permalink
Add integration test for SemanticTokensProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeRobich committed May 10, 2020
1 parent 38bf6ee commit 83ecd67
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
16 changes: 16 additions & 0 deletions src/features/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions src/omnisharp/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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()));
}));
Expand Down
8 changes: 6 additions & 2 deletions test/integrationTests/launchConfiguration.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions test/integrationTests/poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function assertWithPoll<T>(
}

function defaultPollExpression<T>(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<T>(
Expand All @@ -68,7 +68,7 @@ export async function pollDoesNotHappen<T>(
}

export async function poll<T>(
getValue: () => T,
getValue: () => Promise<T> | T,
duration: number,
step: number,
expression: (input: T) => boolean = defaultPollExpression): Promise<T> {
Expand Down
135 changes: 135 additions & 0 deletions test/integrationTests/semanticTokensProvider.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> {

const legend = <vscode.SemanticTokensLegend>await vscode.commands.executeCommand("csharp.private.getSemanticTokensLegend");
const actual = <vscode.SemanticTokens>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");
11 changes: 11 additions & 0 deletions test/integrationTests/testAssets/singleCsproj/semantictokens.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Test
{
public class TestProgram
{
public static int TestMain(string[] args)
{
System.Console.WriteLine(string.Join(',', args));
return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Test
{
public class TestProgram
{
public static int TestMain(string[] args)
{
System.Console.WriteLine(string.Join(',', args));
return 0;
}
}
}
24 changes: 10 additions & 14 deletions test/integrationTests/virtualDocumentTracker.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 () => {
Expand All @@ -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 = <vscode.CompletionList>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");
});
});

Expand Down

0 comments on commit 83ecd67

Please sign in to comment.