diff --git a/package.json b/package.json index 0b2d3f338..f118739a4 100644 --- a/package.json +++ b/package.json @@ -643,6 +643,11 @@ "command": "csharp.listRemoteProcess", "title": "List processes on remote connection for attach", "category": "CSharp" + }, + { + "command": "csharp.reportIssue", + "title": "Start authoring a new issue on GitHub", + "category": "CSharp" } ], "keybindings": [ diff --git a/src/constants/CSharpExtensionId.ts b/src/constants/CSharpExtensionId.ts new file mode 100644 index 000000000..c060aecd0 --- /dev/null +++ b/src/constants/CSharpExtensionId.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const CSharpExtensionId = 'ms-vscode.csharp'; \ No newline at end of file diff --git a/src/constants/IGetDotnetInfo.ts b/src/constants/IGetDotnetInfo.ts new file mode 100644 index 000000000..fd00b8b04 --- /dev/null +++ b/src/constants/IGetDotnetInfo.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IGetDotnetInfo { + (): Promise; +} \ No newline at end of file diff --git a/src/constants/IGetMonoVersion.ts b/src/constants/IGetMonoVersion.ts new file mode 100644 index 000000000..ba491059a --- /dev/null +++ b/src/constants/IGetMonoVersion.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IGetMonoVersion { + (environment: NodeJS.ProcessEnv): Promise; +} \ No newline at end of file diff --git a/src/constants/IMonoResolver.ts b/src/constants/IMonoResolver.ts new file mode 100644 index 000000000..39cc1e93a --- /dev/null +++ b/src/constants/IMonoResolver.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Options } from "../omnisharp/options"; +import { MonoInformation } from "./MonoInformation"; + +export interface IMonoResolver { + getGlobalMonoInfo(options: Options): Promise; +} \ No newline at end of file diff --git a/src/constants/MonoInformation.ts b/src/constants/MonoInformation.ts new file mode 100644 index 000000000..f52cb249c --- /dev/null +++ b/src/constants/MonoInformation.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface MonoInformation { + version: string; + path: string; + env: NodeJS.ProcessEnv; +} diff --git a/src/features/commands.ts b/src/features/commands.ts index 75e203eb3..5f2ca8c2e 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -19,8 +19,11 @@ import { EventStream } from '../EventStream'; import { PlatformInformation } from '../platform'; import CompositeDisposable from '../CompositeDisposable'; import OptionProvider from '../observers/OptionProvider'; +import reportIssue from './reportIssue'; +import { IMonoResolver } from '../constants/IMonoResolver'; +import { getDotnetInfo } from '../utils/getDotnetInfo'; -export default function registerCommands(server: OmniSharpServer, platformInfo: PlatformInformation, eventStream: EventStream, optionProvider: OptionProvider): CompositeDisposable { +export default function registerCommands(server: OmniSharpServer, platformInfo: PlatformInformation, eventStream: EventStream, optionProvider: OptionProvider, monoResolver: IMonoResolver): CompositeDisposable { let disposable = new CompositeDisposable(); disposable.add(vscode.commands.registerCommand('o.restart', () => restartOmniSharp(server))); disposable.add(vscode.commands.registerCommand('o.pickProjectAndStart', async () => pickProjectAndStart(server, optionProvider))); @@ -46,6 +49,7 @@ export default function registerCommands(server: OmniSharpServer, platformInfo: // Register command for adapter executable command. disposable.add(vscode.commands.registerCommand('csharp.coreclrAdapterExecutableCommand', async (args) => getAdapterExecutionCommand(platformInfo, eventStream))); disposable.add(vscode.commands.registerCommand('csharp.clrAdapterExecutableCommand', async (args) => getAdapterExecutionCommand(platformInfo, eventStream))); + disposable.add(vscode.commands.registerCommand('csharp.reportIssue', async () => reportIssue(vscode, eventStream, getDotnetInfo, platformInfo.isValidPlatformForMono(), optionProvider.GetLatestOptions(), monoResolver))); return new CompositeDisposable(disposable); } diff --git a/src/features/reportIssue.ts b/src/features/reportIssue.ts new file mode 100644 index 000000000..f08ec67e9 --- /dev/null +++ b/src/features/reportIssue.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { vscode } from "../vscodeAdapter"; +import { Extension } from "../vscodeAdapter"; +import { CSharpExtensionId } from "../constants/CSharpExtensionId"; +import { EventStream } from "../EventStream"; +import { OpenURL } from "../omnisharp/loggingEvents"; +import { Options } from "../omnisharp/options"; +import { IMonoResolver } from "../constants/IMonoResolver"; +import { IGetDotnetInfo } from "../constants/IGetDotnetInfo"; + +const issuesUrl = "https://github.com/OmniSharp/omnisharp-vscode/issues/new"; + +export default async function reportIssue(vscode: vscode, eventStream: EventStream, getDotnetInfo: IGetDotnetInfo, isValidPlatformForMono: boolean, options: Options, monoResolver: IMonoResolver) { + const dotnetInfo = await getDotnetInfo(); + const monoInfo = await getMonoIfPlatformValid(isValidPlatformForMono, options, monoResolver); + let extensions = getInstalledExtensions(vscode); + let csharpExtVersion = getCsharpExtensionVersion(vscode); + + const body = encodeURIComponent(`## Issue Description ## +## Steps to Reproduce ## + +## Expected Behavior ## + +## Actual Behavior ## + +## Logs ## + +### OmniSharp log ### +
Post the output from Output-->OmniSharp log here
+ +### C# log ### +
Post the output from Output-->C# here
+ +## Environment information ## + +**VSCode version**: ${vscode.version} +**C# Extension**: ${csharpExtVersion} + +${monoInfo} +
Dotnet Information +${dotnetInfo}
+
Visual Studio Code Extensions +${generateExtensionTable(extensions)} +
+`); + + const encodedBody = encodeURIComponent(body); + const queryStringPrefix: string = "?"; + const fullUrl = `${issuesUrl}${queryStringPrefix}body=${encodedBody}`; + eventStream.post(new OpenURL(fullUrl)); +} + +function sortExtensions(a: Extension, b: Extension): number { + + if (a.packageJSON.name.toLowerCase() < b.packageJSON.name.toLowerCase()) { + return -1; + } + if (a.packageJSON.name.toLowerCase() > b.packageJSON.name.toLowerCase()) { + return 1; + } + return 0; +} + +function generateExtensionTable(extensions: Extension[]) { + if (extensions.length <= 0) { + return "none"; + } + + const tableHeader = `|Extension|Author|Version|\n|---|---|---|`; + const table = extensions.map((e) => `|${e.packageJSON.name}|${e.packageJSON.publisher}|${e.packageJSON.version}|`).join("\n"); + + const extensionTable = ` +${tableHeader}\n${table}; +`; + + return extensionTable; +} + +async function getMonoIfPlatformValid(isValidPlatformForMono: boolean, options: Options, monoResolver: IMonoResolver): Promise { + if (isValidPlatformForMono) { + let monoVersion: string; + try { + let globalMonoInfo = await monoResolver.getGlobalMonoInfo(options); + if (globalMonoInfo) { + monoVersion = `OmniSharp using global mono :${globalMonoInfo.version}`; + } + else { + monoVersion = `OmniSharp using built-in mono`; + } + } + catch (error) { + monoVersion = `There is a problem with running OmniSharp on mono: ${error}`; + } + + return `
Mono Information + ${monoVersion}
`; + } + + return ""; +} + + + +function getInstalledExtensions(vscode: vscode) { + let extensions = vscode.extensions.all + .filter(extension => extension.packageJSON.isBuiltin === false); + + return extensions.sort(sortExtensions); +} + +function getCsharpExtensionVersion(vscode: vscode): string { + const extension = vscode.extensions.getExtension(CSharpExtensionId); + return extension.packageJSON.version; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 21a1144c3..cbc8b93f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,10 +35,12 @@ import DotNetTestChannelObserver from './observers/DotnetTestChannelObserver'; import DotNetTestLoggerObserver from './observers/DotnetTestLoggerObserver'; import { ShowOmniSharpConfigChangePrompt } from './observers/OptionChangeObserver'; import createOptionStream from './observables/CreateOptionStream'; +import { CSharpExtensionId } from './constants/CSharpExtensionId'; +import { OpenURLObserver } from './observers/OpenURLObserver'; export async function activate(context: vscode.ExtensionContext): Promise { - const extensionId = 'ms-vscode.csharp'; + const extensionId = CSharpExtensionId; const extension = vscode.extensions.getExtension(extensionId); const extensionVersion = extension.packageJSON.version; const aiKey = extension.packageJSON.contributes.debuggers[0].aiKey; @@ -91,6 +93,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { + switch (event.constructor.name) { + case OpenURL.name: + let url = (event).url; + this.vscode.commands.executeCommand("vscode.open", this.vscode.Uri.parse(url)); + break; + } + } +} \ No newline at end of file diff --git a/src/omnisharp/OmniSharpMonoResolver.ts b/src/omnisharp/OmniSharpMonoResolver.ts new file mode 100644 index 000000000..51622d33e --- /dev/null +++ b/src/omnisharp/OmniSharpMonoResolver.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { satisfies } from 'semver'; +import * as path from 'path'; +import { Options } from './options'; +import { IMonoResolver } from '../constants/IMonoResolver'; +import { MonoInformation } from '../constants/MonoInformation'; +import { IGetMonoVersion } from '../constants/IGetMonoVersion'; + +export class OmniSharpMonoResolver implements IMonoResolver { + private minimumMonoVersion = "5.8.1"; + constructor(private getMonoVersion: IGetMonoVersion) { + } + + private async configureEnvironmentAndGetInfo(options: Options): Promise { + let env = { ...process.env }; + let monoPath: string; + if (options.useGlobalMono !== "never" && options.monoPath !== undefined) { + env['PATH'] = path.join(options.monoPath, 'bin') + path.delimiter + env['PATH']; + env['MONO_GAC_PREFIX'] = options.monoPath; + monoPath = options.monoPath; + } + + let version = await this.getMonoVersion(env); + + return { + version, + path: monoPath, + env + }; + } + + public async getGlobalMonoInfo(options: Options): Promise { + let monoInfo = await this.configureEnvironmentAndGetInfo(options); + let isValid = monoInfo.version && satisfies(monoInfo.version, `>=${this.minimumMonoVersion}`); + + if (options.useGlobalMono === "always") { + if (!isValid) { + throw new Error(`Cannot start OmniSharp because Mono version >=${this.minimumMonoVersion} is required.`); + } + + return monoInfo; + } + else if (options.useGlobalMono === "auto" && isValid) { + return monoInfo; + } + + return undefined; + } +} + diff --git a/src/omnisharp/extension.ts b/src/omnisharp/extension.ts index 38d7a476a..e59545141 100644 --- a/src/omnisharp/extension.ts +++ b/src/omnisharp/extension.ts @@ -36,6 +36,8 @@ import Disposable from '../Disposable'; import OptionProvider from '../observers/OptionProvider'; import trackVirtualDocuments from '../features/virtualDocumentTracker'; import { StructureProvider } from '../features/structureProvider'; +import { OmniSharpMonoResolver } from './OmniSharpMonoResolver'; +import { getMonoVersion } from '../utils/getMonoVersion'; export let omnisharp: OmniSharpServer; @@ -45,7 +47,8 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an }; const options = optionProvider.GetLatestOptions(); - const server = new OmniSharpServer(vscode, provider, packageJSON, platformInfo, eventStream, optionProvider, extensionPath); + let omnisharpMonoResolver = new OmniSharpMonoResolver(getMonoVersion); + const server = new OmniSharpServer(vscode, provider, packageJSON, platformInfo, eventStream, optionProvider, extensionPath, omnisharpMonoResolver); omnisharp = server; const advisor = new Advisor(server); // create before server is started const disposables = new CompositeDisposable(); @@ -93,7 +96,7 @@ export async function activate(context: vscode.ExtensionContext, packageJSON: an localDisposables = null; })); - disposables.add(registerCommands(server, platformInfo, eventStream, optionProvider)); + disposables.add(registerCommands(server, platformInfo, eventStream, optionProvider, omnisharpMonoResolver)); if (!context.workspaceState.get('assetPromptDisabled')) { disposables.add(server.onServerStart(() => { diff --git a/src/omnisharp/launcher.ts b/src/omnisharp/launcher.ts index 21f5f393c..f6fddc3d1 100644 --- a/src/omnisharp/launcher.ts +++ b/src/omnisharp/launcher.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { spawn, ChildProcess } from 'child_process'; -import { satisfies } from 'semver'; + import { PlatformInformation } from '../platform'; import * as path from 'path'; import * as vscode from 'vscode'; import { Options } from './options'; import { LaunchInfo } from './OmnisharpManager'; +import { IMonoResolver } from '../constants/IMonoResolver'; export enum LaunchTargetKind { Solution, @@ -222,9 +223,9 @@ export interface LaunchResult { monoPath?: string; } -export async function launchOmniSharp(cwd: string, args: string[], launchInfo: LaunchInfo, platformInfo: PlatformInformation, options: Options): Promise { +export async function launchOmniSharp(cwd: string, args: string[], launchInfo: LaunchInfo, platformInfo: PlatformInformation, options: Options, monoResolver: IMonoResolver): Promise { return new Promise((resolve, reject) => { - launch(cwd, args, launchInfo, platformInfo, options) + launch(cwd, args, launchInfo, platformInfo, options, monoResolver) .then(result => { // async error - when target not not ENEOT result.process.on('error', err => { @@ -240,7 +241,7 @@ export async function launchOmniSharp(cwd: string, args: string[], launchInfo: L }); } -async function launch(cwd: string, args: string[], launchInfo: LaunchInfo, platformInfo: PlatformInformation, options: Options): Promise { +async function launch(cwd: string, args: string[], launchInfo: LaunchInfo, platformInfo: PlatformInformation, options: Options, monoResolver: IMonoResolver): Promise { if (options.useEditorFormattingSettings) { let globalConfig = vscode.workspace.getConfiguration(); let csharpConfig = vscode.workspace.getConfiguration('[csharp]'); @@ -254,29 +255,16 @@ async function launch(cwd: string, args: string[], launchInfo: LaunchInfo, platf return launchWindows(launchInfo.LaunchPath, cwd, args); } - let childEnv = { ...process.env }; - if (options.useGlobalMono !== "never" && options.monoPath !== undefined) { - childEnv['PATH'] = path.join(options.monoPath, 'bin') + path.delimiter + childEnv['PATH']; - childEnv['MONO_GAC_PREFIX'] = options.monoPath; - } - - let monoVersion = await getMonoVersion(childEnv); - let isValidMonoAvailable = await satisfies(monoVersion, '>=5.8.1'); - - // If the user specifically said that they wanted to launch on Mono, respect their wishes. - if (options.useGlobalMono === "always") { - if (!isValidMonoAvailable) { - throw new Error('Cannot start OmniSharp because Mono version >=5.8.1 is required.'); - } - + let monoInfo = await monoResolver.getGlobalMonoInfo(options); + + if (monoInfo) { const launchPath = launchInfo.MonoLaunchPath || launchInfo.LaunchPath; - - return launchNixMono(launchPath, monoVersion, options.monoPath, cwd, args, childEnv, options.waitForDebugger); - } - - // If we can launch on the global Mono, do so; otherwise, launch directly; - if (options.useGlobalMono === "auto" && isValidMonoAvailable && launchInfo.MonoLaunchPath) { - return launchNixMono(launchInfo.MonoLaunchPath, monoVersion, options.monoPath, cwd, args, childEnv, options.waitForDebugger); + let childEnv = monoInfo.env; + return { + ...launchNixMono(launchPath, cwd, args, childEnv, options.waitForDebugger), + monoVersion: monoInfo.version, + monoPath: monoInfo.path + }; } else { return launchNix(launchInfo.LaunchPath, cwd, args); @@ -333,14 +321,13 @@ function launchNix(launchPath: string, cwd: string, args: string[]): LaunchResul }; } -function launchNixMono(launchPath: string, monoVersion: string, monoPath: string, cwd: string, args: string[], environment: NodeJS.ProcessEnv, useDebugger:boolean): LaunchResult { +function launchNixMono(launchPath: string, cwd: string, args: string[], environment: NodeJS.ProcessEnv, useDebugger:boolean): LaunchResult { let argsCopy = args.slice(0); // create copy of details args argsCopy.unshift(launchPath); argsCopy.unshift("--assembly-loader=strict"); if (useDebugger) { - argsCopy.unshift("--assembly-loader=strict"); argsCopy.unshift("--debug"); argsCopy.unshift("--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555"); } @@ -353,42 +340,7 @@ function launchNixMono(launchPath: string, monoVersion: string, monoPath: string return { process, - command: launchPath, - monoVersion, - monoPath, + command: launchPath }; } -async function getMonoVersion(environment: NodeJS.ProcessEnv): Promise { - const versionRegexp = /(\d+\.\d+\.\d+)/; - - return new Promise((resolve, reject) => { - let childprocess: ChildProcess; - try { - childprocess = spawn('mono', ['--version'], { env: environment }); - } - catch (e) { - return resolve(undefined); - } - - childprocess.on('error', function (err: any) { - resolve(undefined); - }); - - let stdout = ''; - childprocess.stdout.on('data', (data: NodeBuffer) => { - stdout += data.toString(); - }); - - childprocess.stdout.on('close', () => { - let match = versionRegexp.exec(stdout); - - if (match && match.length > 1) { - resolve(match[1]); - } - else { - resolve(undefined); - } - }); - }); -} \ No newline at end of file diff --git a/src/omnisharp/loggingEvents.ts b/src/omnisharp/loggingEvents.ts index 874bf319c..0d93793cf 100644 --- a/src/omnisharp/loggingEvents.ts +++ b/src/omnisharp/loggingEvents.ts @@ -138,7 +138,6 @@ export class DotNetTestDebugProcessStart implements BaseEvent { constructor(public targetProcessId: number) { } } - export class DotNetTestsInClassRunStart implements BaseEvent { constructor(public className: string) { } } @@ -151,6 +150,10 @@ export class DocumentSynchronizationFailure implements BaseEvent { constructor(public documentPath: string, public errorMessage: string) { } } +export class OpenURL { + constructor(public url: string) { } +} + export class DebuggerPrerequisiteFailure extends EventWithMessage { } export class DebuggerPrerequisiteWarning extends EventWithMessage { } export class CommandDotNetRestoreProgress extends EventWithMessage { } diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index 72dca5417..4e6ced6fe 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -29,6 +29,7 @@ import 'rxjs/add/operator/debounceTime'; import CompositeDisposable from '../CompositeDisposable'; import Disposable from '../Disposable'; import OptionProvider from '../observers/OptionProvider'; +import { IMonoResolver } from '../constants/IMonoResolver'; enum ServerState { Starting, @@ -91,7 +92,7 @@ export class OmniSharpServer { private updateProjectDebouncer = new Subject(); private firstUpdateProject: boolean; - constructor(private vscode: vscode, networkSettingsProvider: NetworkSettingsProvider, private packageJSON: any, private platformInfo: PlatformInformation, private eventStream: EventStream, private optionProvider: OptionProvider, extensionPath: string) { + constructor(private vscode: vscode, networkSettingsProvider: NetworkSettingsProvider, private packageJSON: any, private platformInfo: PlatformInformation, private eventStream: EventStream, private optionProvider: OptionProvider, extensionPath: string, private monoResolver: IMonoResolver) { this._requestQueue = new RequestQueueCollection(this.eventStream, 8, request => this._makeRequest(request)); let downloader = new OmnisharpDownloader(networkSettingsProvider, this.eventStream, this.packageJSON, platformInfo, extensionPath); this._omnisharpManager = new OmnisharpManager(downloader, platformInfo); @@ -320,7 +321,7 @@ export class OmniSharpServer { this._fireEvent(Events.BeforeServerStart, solutionPath); try { - let launchResult = await launchOmniSharp(cwd, args, launchInfo, this.platformInfo, options); + let launchResult = await launchOmniSharp(cwd, args, launchInfo, this.platformInfo, options, this.monoResolver); this.eventStream.post(new ObservableEvents.OmnisharpLaunch(launchResult.monoVersion, launchResult.monoPath, launchResult.command, launchResult.process.pid)); this._serverProcess = launchResult.process; diff --git a/src/platform.ts b/src/platform.ts index e05f549a6..00641814d 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -207,4 +207,8 @@ export class PlatformInformation { return null; }); } + + public isValidPlatformForMono(): boolean { + return this.isLinux() || this.isMacOS(); + } } diff --git a/src/utils/getDotnetInfo.ts b/src/utils/getDotnetInfo.ts new file mode 100644 index 000000000..adcc4646f --- /dev/null +++ b/src/utils/getDotnetInfo.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { execChildProcess } from "../common"; + +export async function getDotnetInfo(): Promise { + let dotnetInfo: string; + try { + dotnetInfo = await execChildProcess("dotnet --info", process.cwd()); + } + catch (error) { + dotnetInfo = "A valid dotnet installation could not be found."; + } + + return dotnetInfo; +} \ No newline at end of file diff --git a/src/utils/getMonoVersion.ts b/src/utils/getMonoVersion.ts new file mode 100644 index 000000000..90f0f4fc0 --- /dev/null +++ b/src/utils/getMonoVersion.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChildProcess, spawn } from 'child_process'; +import { IGetMonoVersion } from '../constants/IGetMonoVersion'; + +export const getMonoVersion: IGetMonoVersion = async (environment: NodeJS.ProcessEnv) => { + const versionRegexp = /(\d+\.\d+\.\d+)/; + + return new Promise((resolve, reject) => { + let childprocess: ChildProcess; + try { + childprocess = spawn('mono', ['--version'], { env: environment }); + } + catch (e) { + return resolve(undefined); + } + + childprocess.on('error', function (err: any) { + resolve(undefined); + }); + + let stdout = ''; + childprocess.stdout.on('data', (data: NodeBuffer) => { + stdout += data.toString(); + }); + + childprocess.stdout.on('close', () => { + let match = versionRegexp.exec(stdout); + + if (match && match.length > 1) { + resolve(match[1]); + } + else { + resolve(undefined); + } + }); + }); +}; diff --git a/src/vscodeAdapter.ts b/src/vscodeAdapter.ts index 7f18d91b5..8395b3c27 100644 --- a/src/vscodeAdapter.ts +++ b/src/vscodeAdapter.ts @@ -902,6 +902,11 @@ interface Thenable { then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; } +export interface Extension{ + readonly id: string; + readonly packageJSON: any; +} + export interface vscode { commands: { executeCommand: (command: string, ...rest: any[]) => Thenable; @@ -921,4 +926,13 @@ export interface vscode { createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; onDidChangeConfiguration: Event; }; + extensions: { + getExtension(extensionId: string): Extension | undefined; + all: Extension[]; + }; + Uri: { + parse(value: string): Uri; + }; + + version: string; } \ No newline at end of file diff --git a/test/unitTests/Fakes/FakeGetDotnetInfo.ts b/test/unitTests/Fakes/FakeGetDotnetInfo.ts new file mode 100644 index 000000000..a8d41dc29 --- /dev/null +++ b/test/unitTests/Fakes/FakeGetDotnetInfo.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IGetDotnetInfo } from "../../../src/constants/IGetDotnetInfo"; + +export const fakeDotnetInfo = "myDotnetInfo"; +export const FakeGetDotnetInfo: IGetDotnetInfo = () => Promise.resolve(fakeDotnetInfo); \ No newline at end of file diff --git a/test/unitTests/Fakes/FakeMonoResolver.ts b/test/unitTests/Fakes/FakeMonoResolver.ts new file mode 100644 index 000000000..3a7ec81ad --- /dev/null +++ b/test/unitTests/Fakes/FakeMonoResolver.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMonoResolver } from "../../../src/constants/IMonoResolver"; +import { MonoInformation } from "../../../src/constants/MonoInformation"; + +export const fakeMonoInfo: MonoInformation = { + version: "someMonoVersion", + path: "somePath", + env: undefined +}; + +export class FakeMonoResolver implements IMonoResolver { + public getGlobalMonoCalled: boolean; + + constructor(public willReturnMonoInfo = true) { + this.getGlobalMonoCalled = false; + } + + getGlobalMonoInfo(): Promise { + this.getGlobalMonoCalled = true; + if (this.willReturnMonoInfo) { + return Promise.resolve(fakeMonoInfo); + } + + return Promise.resolve(undefined); + } +} \ No newline at end of file diff --git a/test/unitTests/Fakes/FakeOptions.ts b/test/unitTests/Fakes/FakeOptions.ts new file mode 100644 index 000000000..4985f6b0b --- /dev/null +++ b/test/unitTests/Fakes/FakeOptions.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Options } from "../../../src/omnisharp/options"; + +export function getEmptyOptions(): Options { + return new Options("", "", false, "", false, 0, 0, false, false, false, false, false, false, "", ""); +} diff --git a/test/unitTests/features/reportIssue.test.ts b/test/unitTests/features/reportIssue.test.ts new file mode 100644 index 000000000..f58b33c5b --- /dev/null +++ b/test/unitTests/features/reportIssue.test.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getFakeVsCode } from "../testAssets/Fakes"; +import reportIssue from "../../../src/features/reportIssue"; +import { EventStream } from "../../../src/EventStream"; +import TestEventBus from "../testAssets/TestEventBus"; +import { expect } from "chai"; +import { OpenURL } from "../../../src/omnisharp/loggingEvents"; +import { vscode } from "../../../src/vscodeAdapter"; +import { Options } from "../../../src/omnisharp/options"; +import { FakeGetDotnetInfo, fakeDotnetInfo } from "../Fakes/FakeGetDotnetInfo"; +import { FakeMonoResolver, fakeMonoInfo } from "../Fakes/FakeMonoResolver"; + +suite(`${reportIssue.name}`, () => { + const vscodeVersion = "myVersion"; + const csharpExtVersion = "csharpExtVersion"; + const isValidForMono = true; + let vscode: vscode; + const extension1 = { + packageJSON: { + name: "name1", + publisher: "publisher1", + version: "version1", + isBuiltin: true + }, + id: "id1" + }; + + const extension2 = { + packageJSON: { + name: "name2", + publisher: "publisher2", + version: "version2", + isBuiltin: false + }, + id: "id2" + }; + + let fakeMonoResolver: FakeMonoResolver; + let eventStream: EventStream; + let eventBus: TestEventBus; + let getDotnetInfo = FakeGetDotnetInfo; + let options: Options; + + setup(() => { + vscode = getFakeVsCode(); + vscode.extensions.getExtension = () => { + return { + packageJSON: { + version: csharpExtVersion + }, + id: "" + }; + }; + vscode.version = vscodeVersion; + vscode.extensions.all = [extension1, extension2]; + eventStream = new EventStream(); + eventBus = new TestEventBus(eventStream); + fakeMonoResolver = new FakeMonoResolver(); + }); + + test(`${OpenURL.name} event is created`, async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + let events = eventBus.getEvents(); + expect(events).to.have.length(1); + expect(events[0].constructor.name).to.be.equal(`${OpenURL.name}`); + }); + + test(`${OpenURL.name} event is created with the omnisharp-vscode github repo issues url`, async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, false, options, fakeMonoResolver); + let url = (eventBus.getEvents()[0]).url; + expect(url).to.include("https://github.com/OmniSharp/omnisharp-vscode/issues/new"); + }); + + test("The url contains the vscode version", async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + let url = (eventBus.getEvents()[0]).url; + expect(url).to.include(encodeURIComponent(encodeURIComponent(`**VSCode version**: ${vscodeVersion}`))); + }); + + test("The body contains the csharp extension version", async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + let url = (eventBus.getEvents()[0]).url; + expect(url).to.include(encodeURIComponent(encodeURIComponent(`**C# Extension**: ${csharpExtVersion}`))); + }); + + test("dotnet info is obtained and put into the url", async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + let url = (eventBus.getEvents()[0]).url; + expect(url).to.contain(fakeDotnetInfo); + }); + + test("mono information is obtained when it is a valid mono platform", async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + expect(fakeMonoResolver.getGlobalMonoCalled).to.be.equal(true); + }); + + test("mono version is put in the body when shouldUseGlobalMono returns a monoInfo", async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + expect(fakeMonoResolver.getGlobalMonoCalled).to.be.equal(true); + let url = (eventBus.getEvents()[0]).url; + expect(url).to.contain(fakeMonoInfo.version); + }); + + test("built-in mono usage message is put in the body when shouldUseGlobalMono returns a null", async () => { + fakeMonoResolver = new FakeMonoResolver(false); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + expect(fakeMonoResolver.getGlobalMonoCalled).to.be.equal(true); + let url = (eventBus.getEvents()[0]).url; + expect(url).to.contain(encodeURIComponent(encodeURIComponent(`OmniSharp using built-in mono`))); + }); + + test("mono information is not obtained when it is not a valid mono platform", async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, false, options, fakeMonoResolver); + expect(fakeMonoResolver.getGlobalMonoCalled).to.be.equal(false); + }); + + test("The url contains the name, publisher and version for all the extensions that are not builtin", async () => { + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + let url = (eventBus.getEvents()[0]).url; + expect(url).to.contain(extension2.packageJSON.name); + expect(url).to.contain(extension2.packageJSON.publisher); + expect(url).to.contain(extension2.packageJSON.version); + expect(url).to.not.contain(extension1.packageJSON.name); + expect(url).to.not.contain(extension1.packageJSON.publisher); + expect(url).to.not.contain(extension1.packageJSON.version); + }); +}); \ No newline at end of file diff --git a/test/unitTests/logging/OpenURLObserver.test.ts b/test/unitTests/logging/OpenURLObserver.test.ts new file mode 100644 index 000000000..35b3a59ef --- /dev/null +++ b/test/unitTests/logging/OpenURLObserver.test.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OpenURLObserver } from "../../../src/observers/OpenURLObserver"; +import { vscode } from "../../../src/vscodeAdapter"; +import { getFakeVsCode } from "../testAssets/Fakes"; +import { OpenURL } from "../../../src/omnisharp/loggingEvents"; +import { expect } from "chai"; + +suite(`${OpenURLObserver.name}`, () => { + let observer: OpenURLObserver; + let vscode: vscode; + let commands: Array; + let valueToBeParsed: string; + const url = "someUrl"; + + setup(() => { + vscode = getFakeVsCode(); + commands = []; + valueToBeParsed = undefined; + vscode.commands.executeCommand = (command: string, ...rest: any[]) => { + commands.push(command); + return undefined; + }; + vscode.Uri.parse = (value: string) => { + valueToBeParsed = value; + return undefined; + }; + observer = new OpenURLObserver(vscode); + }); + + test("Execute command is called with the vscode.open command", () => { + let event = new OpenURL(url); + observer.post(event); + expect(commands).to.be.deep.equal(["vscode.open"]); + }); + + test("url is passed to the rest parameter in executeCommand via vscode.uri.parse ", () => { + let event = new OpenURL(url); + observer.post(event); + expect(valueToBeParsed).to.be.equal(url); + }); +}); \ No newline at end of file diff --git a/test/unitTests/omnisharp/OmniSharpMonoResolver.test.ts b/test/unitTests/omnisharp/OmniSharpMonoResolver.test.ts new file mode 100644 index 000000000..ffab14606 --- /dev/null +++ b/test/unitTests/omnisharp/OmniSharpMonoResolver.test.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OmniSharpMonoResolver } from "../../../src/omnisharp/OmniSharpMonoResolver"; + +import { Options } from "../../../src/omnisharp/options"; +import { use as chaiUse, expect } from "chai"; +import { join } from "path"; +import { getEmptyOptions } from "../Fakes/FakeOptions"; + +chaiUse(require('chai-as-promised')); + +suite(`${OmniSharpMonoResolver.name}`, () => { + let getMonoCalled: boolean; + let environment: NodeJS.ProcessEnv; + let options: Options; + const monoPath = "monoPath"; + const getMono = (version: string) => (env: NodeJS.ProcessEnv) => { + getMonoCalled = true; + environment = env; + return Promise.resolve(version); + }; + + setup(() => { + getMonoCalled = false; + options = getEmptyOptions(); + }); + + test("it returns undefined if the version is less than 5.8.1 and useGlobalMono is auto", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.0.0")); + let monoInfo = await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "auto", + monoPath: monoPath + }); + expect(monoInfo).to.be.undefined; + }); + + test("it returns undefined if useGlobalMono is never", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.2")); + let monoInfo = await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "never", + monoPath: monoPath + }); + expect(monoInfo).to.be.undefined; + }); + + test("it returns the path and version if the version is greater than or equal to 5.8.1 and getGlobalMonoInfo is always", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.1")); + let monoInfo = await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "always", + monoPath: monoPath + }); + + expect(monoInfo.version).to.be.equal("5.8.1"); + expect(monoInfo.path).to.be.equal(monoPath); + }); + + test("it returns the path and version if the version is greater than or equal to 5.8.1 and getGlobalMonoInfo is auto", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.2")); + let monoInfo = await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "auto", + monoPath: monoPath + }); + + expect(monoInfo.version).to.be.equal("5.8.2"); + expect(monoInfo.path).to.be.equal(monoPath); + }); + + test("it throws exception if getGlobalMonoInfo is always and version<5.8.1", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.0")); + + await expect(monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "always", + monoPath: monoPath + })).to.be.rejected; + }); + + test("sets the environment with the monoPath id useGlobalMono is auto", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.1")); + let monoInfo = await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "auto", + monoPath: monoPath + }); + + expect(monoInfo.env["PATH"]).to.contain(join(monoPath, 'bin')); + expect(monoInfo.env["MONO_GAC_PREFIX"]).to.be.equal(monoPath); + }); + + test("sets the environment with the monoPath id useGlobalMono is always", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.1")); + let monoInfo = await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "auto", + monoPath: monoPath + }); + + expect(monoInfo.env["PATH"]).to.contain(join(monoPath, 'bin')); + expect(monoInfo.env["MONO_GAC_PREFIX"]).to.be.equal(monoPath); + }); + + test("doesn't set the environment with the monoPath if useGlobalMono is never", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.1")); + await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "never", + monoPath: monoPath + }); + + expect(getMonoCalled).to.be.equal(true); + expect(environment["PATH"]).to.not.contain(join(monoPath, 'bin')); + expect(environment["MONO_GAC_PREFIX"]).to.be.undefined; + }); + + test("getMono is called with the environment that includes the monoPath if the useGlobalMono is auto or always", async () => { + let monoResolver = new OmniSharpMonoResolver(getMono("5.8.1")); + await monoResolver.getGlobalMonoInfo({ + ...options, + useGlobalMono: "auto", + monoPath: monoPath + }); + + expect(getMonoCalled).to.be.equal(true); + expect(environment["PATH"]).to.contain(join(monoPath, 'bin')); + expect(environment["MONO_GAC_PREFIX"]).to.be.equal(monoPath); + }); +}); \ No newline at end of file diff --git a/test/unitTests/testAssets/Fakes.ts b/test/unitTests/testAssets/Fakes.ts index bd0aa03ee..706d184f0 100644 --- a/test/unitTests/testAssets/Fakes.ts +++ b/test/unitTests/testAssets/Fakes.ts @@ -133,7 +133,19 @@ export function getFakeVsCode(): vscode.vscode { onDidChangeConfiguration: (listener: (e: ConfigurationChangeEvent) => any, thisArgs?: any, disposables?: Disposable[]): Disposable => { throw new Error("Not Implemented"); } - } + }, + extensions: { + getExtension: () => { + throw new Error("Not Implemented"); + }, + all: [] + }, + Uri: { + parse: () => { + throw new Error("Not Implemented"); + } + }, + version: "" }; } @@ -179,4 +191,4 @@ export function getVSCodeWithConfig() { export function updateConfig(vscode: vscode.vscode, section: string, config: string, value: any) { let workspaceConfig = vscode.workspace.getConfiguration(section); workspaceConfig.update(config, value); -} +} \ No newline at end of file