diff --git a/package.json b/package.json index a088c51551..8bc0ba5984 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "extensionDependencies": [], "dependencies": { + "deep-equal": "^2.0.2", "diff": "^4.0.2", "json-rpc2": "^1.0.2", "moment": "^2.24.0", @@ -49,8 +50,7 @@ "vscode-debugprotocol": "^1.40.0", "vscode-extension-telemetry": "^0.1.2", "vscode-languageclient": "6.1.0", - "web-request": "^1.0.7", - "deep-equal": "^2.0.2" + "web-request": "^1.0.7" }, "devDependencies": { "@types/fs-extra": "^8.1.0", diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts index 687b61bdbb..1a1e64d963 100644 --- a/src/debugAdapter/goDebug.ts +++ b/src/debugAdapter/goDebug.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------*/ import { ChildProcess, execFile, execSync, spawn, spawnSync } from 'child_process'; +import { EventEmitter } from 'events'; import * as fs from 'fs'; import { existsSync, lstatSync } from 'fs'; +import * as glob from 'glob'; import { Client, RPCConnection } from 'json-rpc2'; import * as os from 'os'; import * as path from 'path'; @@ -88,6 +90,20 @@ interface DebuggerState { Running: boolean; } +export interface PackageBuildInfo { + ImportPath: string; + DirectoryPath: string; + Files: string[]; +} + +export interface ListPackagesBuildInfoOut { + List: PackageBuildInfo[]; +} + +export interface ListSourcesOut { + Sources: string[]; +} + interface CreateBreakpointOut { Breakpoint: DebugBreakpoint; } @@ -308,6 +324,10 @@ function findPathSeparator(filePath: string) { return filePath.includes('/') ? '/' : '\\'; } +export function escapeGoModPath(filePath: string) { + return filePath.replace(/[A-Z]/g, (match: string) => `!${match.toLocaleLowerCase()}`); +} + function normalizePath(filePath: string) { if (process.platform === 'win32') { const pathSeparator = findPathSeparator(filePath); @@ -319,7 +339,11 @@ function normalizePath(filePath: string) { return filePath; } -class Delve { +function getBaseName(filePath: string) { + return filePath.includes('/') ? path.basename(filePath) : path.win32.basename(filePath); +} + +export class Delve { public program: string; public remotePath: string; public loadConfig: LoadConfig; @@ -715,7 +739,7 @@ class Delve { } } -class GoDebugSession extends LoggingDebugSession { +export class GoDebugSession extends LoggingDebugSession { private variableHandles: Handles; private breakpoints: Map; // Editing breakpoints requires halting delve, skip sending Stop Event to VS Code in such cases @@ -729,12 +753,18 @@ class GoDebugSession extends LoggingDebugSession { private stopOnEntry: boolean; private logLevel: Logger.LogLevel = Logger.LogLevel.Error; private readonly initdone = 'initdoneĀ·'; + private remoteSourcesAndPackages = new RemoteSourcesAndPackages(); + private localToRemotePathMapping = new Map(); + private remoteToLocalPathMapping = new Map(); private showGlobalVariables: boolean = false; private continueEpoch = 0; private continueRequestRunning = false; - public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { + public constructor( + debuggerLinesStartAt1: boolean, + isServer: boolean = false, + readonly fileSystem = fs) { super('', debuggerLinesStartAt1, isServer); this.variableHandles = new Handles(); this.skipStopEventOnce = false; @@ -827,10 +857,80 @@ class GoDebugSession extends LoggingDebugSession { } } - protected toDebuggerPath(filePath: string): string { + /** + * Given a potential list of paths in potentialPaths array, we will + * find the path that has the longest suffix matching filePath. + * For example, if filePath is /usr/local/foo/bar/main.go + * and potentialPaths are abc/xyz/main.go, bar/main.go + * then bar/main.go will be the result. + * NOTE: This function assumes that potentialPaths array only contains + * files with the same base names as filePath. + */ + protected findPathWithBestMatchingSuffix(filePath: string, potentialPaths: string[]): string|undefined { + if (!potentialPaths.length) { + return; + } + + if (potentialPaths.length === 1) { + return potentialPaths[0]; + } + + const filePathSegments = filePath.split(/\/|\\/).reverse(); + let bestPathSoFar = potentialPaths[0]; + let bestSegmentsCount = 0; + for (const potentialPath of potentialPaths) { + const potentialPathSegments = potentialPath.split(/\/|\\/).reverse(); + let i = 0; + for (; i < filePathSegments.length + && i < potentialPathSegments.length + && filePathSegments[i] === potentialPathSegments[i]; i++) { + if (i > bestSegmentsCount) { + bestSegmentsCount = i; + bestPathSoFar = potentialPath; + } + } + } + return bestPathSoFar; + } + + /** + * Given a local path, try to find matching file in the remote machine + * using remote sources and remote packages info that we get from Delve. + * The result would be cached in localToRemotePathMapping. + */ + protected inferRemotePathFromLocalPath(localPath: string): string|undefined { + if (this.localToRemotePathMapping.has(localPath)) { + return this.localToRemotePathMapping.get(localPath); + } + + const fileName = getBaseName(localPath); + const potentialMatchingRemoteFiles = this.remoteSourcesAndPackages.remoteSourceFilesNameGrouping.get(fileName); + const bestMatchingRemoteFile = this.findPathWithBestMatchingSuffix(localPath, potentialMatchingRemoteFiles); + if (!bestMatchingRemoteFile) { + return; + } + + this.localToRemotePathMapping.set(localPath, bestMatchingRemoteFile); + return bestMatchingRemoteFile; + } + + protected async toDebuggerPath(filePath: string): Promise { if (this.delve.remotePath.length === 0) { + if (this.delve.isRemoteDebugging) { + // The user trusts us to infer the remote path mapping! + try { + await this.initializeRemotePackagesAndSources(); + const matchedRemoteFile = this.inferRemotePathFromLocalPath(filePath); + if (matchedRemoteFile) { + return matchedRemoteFile; + } + } catch (error) { + log(`Failing to initialize remote sources: ${error}`); + } + } return this.convertClientPathToDebugger(filePath); } + // The filePath may have a different path separator than the localPath // So, update it to use the same separator as the remote path to ease // in replacing the local path in it with remote path @@ -838,8 +938,158 @@ class GoDebugSession extends LoggingDebugSession { return filePath.replace(this.delve.program.replace(/\/|\\/g, this.remotePathSeparator), this.delve.remotePath); } + /** + * Given a remote path, try to infer the matching local path. + * We attempt to find the path in local Go packages as well as workspaceFolder. + * Cache the result in remoteToLocalPathMapping. + */ + protected inferLocalPathFromRemotePath(remotePath: string): string|undefined { + if (this.remoteToLocalPathMapping.has(remotePath)) { + return this.remoteToLocalPathMapping.get(remotePath); + } + + const convertedLocalPackageFile = this.inferLocalPathFromRemoteGoPackage(remotePath); + if (convertedLocalPackageFile) { + this.remoteToLocalPathMapping.set(remotePath, convertedLocalPackageFile); + return convertedLocalPackageFile; + } + + // If we cannot find the path in packages, most likely it will be in the current directory. + const fileName = getBaseName(remotePath); + const globSync = glob.sync(fileName, {matchBase: true, + cwd: this.delve.program }); + const bestMatchingLocalPath = this.findPathWithBestMatchingSuffix(remotePath, globSync); + if (bestMatchingLocalPath) { + const fullLocalPath = path.join(this.delve.program, bestMatchingLocalPath); + this.remoteToLocalPathMapping.set(remotePath, fullLocalPath); + return fullLocalPath; + } + } + + /** + * Given a remote path, we attempt to infer the local path by first checking + * if it is in any remote packages. If so, then we attempt to find the matching + * local package and find the local path from there. + */ + protected inferLocalPathFromRemoteGoPackage(remotePath: string): string|undefined { + const remotePackage = this.remoteSourcesAndPackages.remotePackagesBuildInfo.find( + (buildInfo) => remotePath.startsWith(buildInfo.DirectoryPath)); + // Since we know pathToConvert exists in a remote package, we can try to find + // that same package in the local client. We can use import path to search for the package. + if (!remotePackage) { + return; + } + + if (!this.remotePathSeparator) { + this.remotePathSeparator = findPathSeparator(remotePackage.DirectoryPath); + } + + // Escaping package path. + // It seems like sometimes Delve don't escape the path properly + // so we should do it. + remotePath = escapeGoModPath(remotePath); + const escapedImportPath = escapeGoModPath(remotePackage.ImportPath); + + // The remotePackage.DirectoryPath should be something like + // //xyz... + // Directory Path can be like "/go/pkg/mod/github.com/google/go-cmp@v0.4.0/cmp" + // and Import Path can be like "github.com/google/go-cmp/cmp" + // and Remote Path "/go/pkg/mod/github.com/google/go-cmp@v0.4.0/cmp/blah.go" + const importPathIndex = remotePath.replace(/@v\d+\.\d+\.\d+[^\/]*/, '') + .indexOf(escapedImportPath); + if (importPathIndex < 0) { + return; + } + + const relativeRemotePath = remotePath + .substr(importPathIndex) + .split(this.remotePathSeparator) + .join(this.localPathSeparator); + const pathToConvertWithLocalSeparator = remotePath.split(this.remotePathSeparator).join(this.localPathSeparator); + + // Scenario 1: The package is inside the current working directory. + const localWorkspacePath = path.join(this.delve.program, relativeRemotePath); + if (this.fileSystem.existsSync(localWorkspacePath)) { + return localWorkspacePath; + } + + // Scenario 2: The package is inside GOPATH. + const localGoPathImportPath = this.inferLocalPathInGoPathFromRemoteGoPackage( + pathToConvertWithLocalSeparator, relativeRemotePath); + if (localGoPathImportPath) { + return localGoPathImportPath; + } + + // Scenario 3: The package is inside GOROOT. + return this.inferLocalPathInGoRootFromRemoteGoPackage(pathToConvertWithLocalSeparator, relativeRemotePath); + } + + /** + * Given a remotePath, check whether the file path exists in $GOROOT/src. + * Return the path if it exists. + * We are assuming that remotePath is of the form /src/. + */ + protected inferLocalPathInGoRootFromRemoteGoPackage( + remotePathWithLocalSeparator: string, relativeRemotePath: string): string|undefined { + const srcIndex = remotePathWithLocalSeparator.indexOf(`${this.localPathSeparator}src${this.localPathSeparator}`); + const goroot = process.env['GOROOT'] || ''; + const localGoRootImportPath = path.join( + goroot, + srcIndex >= 0 + ? remotePathWithLocalSeparator.substr(srcIndex) + : path.join('src', relativeRemotePath)); + if (this.fileSystem.existsSync(localGoRootImportPath)) { + return localGoRootImportPath; + } + } + + /** + * Given a remotePath, check whether the file path exists in $GOPATH. + * This can be either in $GOPATH/pkg/mod or $GOPATH/src. If so, return that path. + * remotePath can be something like /usr/local/gopath/src/hello-world/main.go + * and relativeRemotePath should be hello-world/main.go. In other words, + * relativeRemotePath is a relative version of remotePath starting + * from the import path of the module. + */ + protected inferLocalPathInGoPathFromRemoteGoPackage( + remotePathWithLocalSeparator: string, relativeRemotePath: string): string|undefined { + // Scenario 1: The package is inside $GOPATH/pkg/mod. + const gopath = (process.env['GOPATH'] || '').split(path.delimiter)[0]; + + const indexGoModCache = remotePathWithLocalSeparator.indexOf( + `${this.localPathSeparator}pkg${this.localPathSeparator}mod${this.localPathSeparator}` + ); + const localGoPathImportPath = path.join( + gopath, + indexGoModCache >= 0 + ? remotePathWithLocalSeparator.substr(indexGoModCache) + : path.join('pkg', 'mod', relativeRemotePath)); + if (this.fileSystem.existsSync(localGoPathImportPath)) { + return localGoPathImportPath; + } + + // Scenario 2: The file is in a package in $GOPATH/src. + const localGoPathSrcPath = path.join( + gopath, 'src', + relativeRemotePath.split(this.remotePathSeparator).join(this.localPathSeparator)); + if (this.fileSystem.existsSync(localGoPathSrcPath)) { + return localGoPathSrcPath; + } + } + + /** + * This functions assumes that remote packages and paths information + * have been initialized. + */ protected toLocalPath(pathToConvert: string): string { if (this.delve.remotePath.length === 0) { + // User trusts use to infer the path + if (this.delve.isRemoteDebugging) { + const inferredPath = this.inferLocalPathFromRemotePath(pathToConvert); + if (inferredPath) { + return inferredPath; + } + } return this.convertDebuggerPathToClient(pathToConvert); } @@ -968,7 +1218,7 @@ class GoDebugSession extends LoggingDebugSession { this.delve.call( this.delve.isApiV1 ? 'StacktraceGoroutine' : 'Stacktrace', [stackTraceIn], - (err, out) => { + async (err, out) => { if (err) { this.logDelveError(err, 'Failed to produce stacktrace'); return this.sendErrorResponse(response, 2004, 'Unable to produce stack trace: "{e}"', { @@ -977,6 +1227,9 @@ class GoDebugSession extends LoggingDebugSession { } const locations = this.delve.isApiV1 ? out : (out).Locations; log('locations', locations); + + await this.initializeRemotePackagesAndSources(); + let stackFrames = locations.map((location, frameId) => { const uniqueStackFrameId = this.stackFrameHandles.create([goroutineId, frameId]); return new StackFrame( @@ -1400,8 +1653,8 @@ class GoDebugSession extends LoggingDebugSession { args.remotePath = ''; } + this.localPathSeparator = findPathSeparator(localPath); if (args.remotePath.length > 0) { - this.localPathSeparator = findPathSeparator(localPath); this.remotePathSeparator = findPathSeparator(args.remotePath); const llist = localPath.split(/\/|\\/).reverse(); @@ -1476,15 +1729,38 @@ class GoDebugSession extends LoggingDebugSession { ); } - private setBreakPoints( + /** + * Initializing remote packages and sources. + * We use event model to prevent race conditions. + */ + private async initializeRemotePackagesAndSources(): Promise { + if (this.remoteSourcesAndPackages.initializedRemoteSourceFiles) { + return; + } + + if (!this.remoteSourcesAndPackages.initializingRemoteSourceFiles) { + await this.remoteSourcesAndPackages.initializeRemotePackagesAndSources(this.delve); + return; + } + + if (this.remoteSourcesAndPackages.initializingRemoteSourceFiles) { + await new Promise((resolve) => { + this.remoteSourcesAndPackages.on(RemoteSourcesAndPackages.INITIALIZED, () => { + resolve(); + }); + }); + } + } + + private async setBreakPoints( response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments - ): Thenable { + ): Promise { const file = normalizePath(args.source.path); if (!this.breakpoints.get(file)) { this.breakpoints.set(file, []); } - const remoteFile = this.toDebuggerPath(file); + const remoteFile = await this.toDebuggerPath(file); return Promise.all( this.breakpoints.get(file).map((existingBP) => { @@ -1591,12 +1867,13 @@ class GoDebugSession extends LoggingDebugSession { ); } - private getPackageInfo(debugState: DebuggerState): Thenable { + private async getPackageInfo(debugState: DebuggerState): Promise { if (!debugState.currentThread || !debugState.currentThread.file) { return Promise.resolve(null); } + await this.initializeRemotePackagesAndSources(); const dir = path.dirname( - this.delve.remotePath.length + this.delve.remotePath.length || this.delve.isRemoteDebugging ? this.toLocalPath(debugState.currentThread.file) : debugState.currentThread.file ); @@ -1909,6 +2186,65 @@ class GoDebugSession extends LoggingDebugSession { } } +// Class for fetching remote sources and packages +// in the remote program using Delve. +// tslint:disable-next-line:max-classes-per-file +export class RemoteSourcesAndPackages extends EventEmitter { + public static readonly INITIALIZED = 'INITIALIZED'; + + public initializingRemoteSourceFiles = false; + public initializedRemoteSourceFiles = false; + + public remotePackagesBuildInfo: PackageBuildInfo[] = []; + public remoteSourceFiles: string[] = []; + public remoteSourceFilesNameGrouping = new Map(); + + /** + * Initialize and fill out remote packages build info and remote source files. + * Emits the INITIALIZED event once initialization is complete. + */ + public async initializeRemotePackagesAndSources(delve: Delve): Promise { + this.initializingRemoteSourceFiles = true; + + try { + // ListPackagesBuildInfo is not available on V1. + if (!delve.isApiV1 && this.remotePackagesBuildInfo.length === 0) { + const packagesBuildInfoResponse: ListPackagesBuildInfoOut = await delve.callPromise( + 'ListPackagesBuildInfo', [{IncludeFiles: true}] + ); + if (packagesBuildInfoResponse && packagesBuildInfoResponse.List) { + this.remotePackagesBuildInfo = packagesBuildInfoResponse.List; + } + } + + // List sources will return all the source files used by Delve. + if (delve.isApiV1) { + this.remoteSourceFiles = await delve.callPromise('ListSources', []); + } else { + const listSourcesResponse: ListSourcesOut = await delve.callPromise('ListSources', [{}]); + if (listSourcesResponse && listSourcesResponse.Sources) { + this.remoteSourceFiles = listSourcesResponse.Sources; + } + } + + // Group the source files by name for easy searching later. + this.remoteSourceFiles = this.remoteSourceFiles.filter((sourceFile) => !sourceFile.startsWith('<')); + this.remoteSourceFiles.forEach((sourceFile) => { + const fileName = getBaseName(sourceFile); + if (!this.remoteSourceFilesNameGrouping.has(fileName)) { + this.remoteSourceFilesNameGrouping.set(fileName, []); + } + this.remoteSourceFilesNameGrouping.get(fileName).push(sourceFile); + }); + } catch (error) { + logError(`Failed to initialize remote sources and packages: ${error && error.message}`); + } finally { + this.emit(RemoteSourcesAndPackages.INITIALIZED); + this.initializedRemoteSourceFiles = true; + } + } +} + function random(low: number, high: number): number { return Math.floor(Math.random() * (high - low) + low); } diff --git a/test/integration/goDebug.test.ts b/test/integration/goDebug.test.ts new file mode 100644 index 0000000000..3fae9105fe --- /dev/null +++ b/test/integration/goDebug.test.ts @@ -0,0 +1,263 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Delve, escapeGoModPath, GoDebugSession, + PackageBuildInfo, RemoteSourcesAndPackages } from '../../src/debugAdapter/goDebug'; + +suite('Path Manipulation Tests', () => { + test('escapeGoModPath works', () => { + assert.strictEqual(escapeGoModPath('BurnSushi/test.go'), '!burn!sushi/test.go'); + }); +}); + +suite('GoDebugSession Tests', () => { + const workspaceFolder = '/usr/workspacefolder'; + const delve: Delve = {} as Delve; + const previousGoPath = process.env.GOPATH; + const previousGoRoot = process.env.GOROOT; + + let goDebugSession: GoDebugSession; + let remoteSourcesAndPackages: RemoteSourcesAndPackages; + let fileSystem: typeof fs; + setup(() => { + process.env.GOPATH = '/usr/gopath'; + process.env.GOROOT = '/usr/goroot'; + remoteSourcesAndPackages = new RemoteSourcesAndPackages(); + fileSystem = { existsSync: () => false } as unknown as typeof fs; + delve.program = workspaceFolder; + delve.isApiV1 = false; + goDebugSession = new GoDebugSession(true, false, fileSystem); + goDebugSession['delve'] = delve; + goDebugSession['remoteSourcesAndPackages'] = remoteSourcesAndPackages; + }); + + teardown(() => { + process.env.GOPATH = previousGoPath; + process.env.GOROOT = previousGoRoot; + sinon.restore(); + }); + + test('inferRemotePathFromLocalPath works', () => { + const sourceFileMapping = new Map(); + sourceFileMapping.set('main.go', ['/app/hello-world/main.go', '/app/main.go']); + sourceFileMapping.set('blah.go', ['/app/blah.go']); + + remoteSourcesAndPackages.remoteSourceFilesNameGrouping = sourceFileMapping; + + const inferredPath = goDebugSession['inferRemotePathFromLocalPath']( + 'C:\\Users\\Documents\\src\\hello-world\\main.go'); + assert.strictEqual(inferredPath, '/app/hello-world/main.go'); + }); + + test('inferLocalPathFromRemoteGoPackage works for package in workspaceFolder', () => { + const remotePath = '/src/hello-world/morestrings/morestrings.go'; + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world/morestrings', + DirectoryPath: '/src/hello-world/morestrings', + Files: ['/src/hello-world/morestrings/lessstrings.go', '/src/hello-world/morestrings/morestrings.go'] + }; + + const testPackage: PackageBuildInfo = { + ImportPath: 'FooBar/test', + DirectoryPath: 'remote/pkg/mod/!foo!bar/test@v1.0.2', + Files: ['remote/pkg/mod/!foo!bar/test@v1.0.2/test.go'] + }; + + const localPath = path.join(workspaceFolder, 'hello-world/morestrings/morestrings.go'); + const existsSyncStub = sinon.stub(fileSystem, 'existsSync'); + existsSyncStub.withArgs(localPath).returns(true); + + remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage]; + + goDebugSession['localPathSeparator'] = '/'; + const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath); + assert.strictEqual(inferredLocalPath, localPath); + }); + + test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/pkg/mod', () => { + const remotePath = 'remote/pkg/mod/!foo!bar/test@v1.0.2/test.go'; + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world', + DirectoryPath: '/src/hello-world', + Files: ['src/hello-world/hello.go', 'src/hello-world/world.go'] + }; + + const testPackage: PackageBuildInfo = { + ImportPath: 'FooBar/test', + DirectoryPath: 'remote/pkg/mod/!foo!bar/test@v1.0.2', + Files: ['remote/pkg/mod/!foo!bar/test@v1.0.2/test.go'] + }; + + const localPath = path.join(process.env.GOPATH, 'pkg/mod/!foo!bar/test@v1.0.2/test.go'); + const existsSyncStub = sinon.stub(fileSystem, 'existsSync'); + existsSyncStub.withArgs(localPath).returns(true); + + remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage]; + + goDebugSession['localPathSeparator'] = '/'; + const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath); + assert.strictEqual(inferredLocalPath, localPath); + }); + + test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/pkg/mod with relative path', () => { + const remotePath = '!foo!bar/test@v1.0.2/test.go'; + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world', + DirectoryPath: '/src/hello-world', + Files: ['src/hello-world/hello.go', 'src/hello-world/world.go'] + }; + + const testPackage: PackageBuildInfo = { + ImportPath: 'FooBar/test', + DirectoryPath: '!foo!bar/test@v1.0.2', + Files: ['!foo!bar/test@v1.0.2/test.go'] + }; + + const localPath = path.join(process.env.GOPATH, 'pkg/mod/!foo!bar/test@v1.0.2/test.go'); + const existsSyncStub = sinon.stub(fileSystem, 'existsSync'); + existsSyncStub.withArgs(localPath).returns(true); + + remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage]; + + goDebugSession['localPathSeparator'] = '/'; + const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath); + assert.strictEqual(inferredLocalPath, localPath); + }); + + test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/src', () => { + const remotePath = 'remote/gopath/src/foobar/test@v1.0.2-abcde-34/test.go'; + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world', + DirectoryPath: '/src/hello-world', + Files: ['src/hello-world/hello.go', 'src/hello-world/world.go'] + }; + + const testPackage: PackageBuildInfo = { + ImportPath: 'foobar/test', + DirectoryPath: 'remote/gopath/src/foobar/test@v1.0.2-abcde-34', + Files: ['remote/gopath/src/foobar/test@v1.0.2-abcde-34/test.go'] + }; + + const localPath = path.join(process.env.GOPATH, 'src', 'foobar/test@v1.0.2-abcde-34/test.go'); + const existsSyncStub = sinon.stub(fileSystem, 'existsSync'); + existsSyncStub.withArgs(localPath).returns(true); + + remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage]; + + goDebugSession['localPathSeparator'] = '/'; + const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath); + assert.strictEqual(inferredLocalPath, localPath); + }); + + test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/src with relative path', () => { + const remotePath = 'foobar/test@v1.0.2/test.go'; + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world', + DirectoryPath: '/src/hello-world', + Files: ['src/hello-world/hello.go', 'src/hello-world/world.go'] + }; + + const testPackage: PackageBuildInfo = { + ImportPath: 'foobar/test', + DirectoryPath: 'foobar/test@v1.0.2', + Files: ['foobar/test@v1.0.2/test.go'] + }; + + const localPath = path.join(process.env.GOPATH, 'src', 'foobar/test@v1.0.2/test.go'); + const existsSyncStub = sinon.stub(fileSystem, 'existsSync'); + existsSyncStub.withArgs(localPath).returns(true); + + remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage]; + + goDebugSession['localPathSeparator'] = '/'; + const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath); + assert.strictEqual(inferredLocalPath, localPath); + }); + + test('inferLocalPathFromRemoteGoPackage works for package in GOROOT/src', () => { + const remotePath = 'remote/goroot/src/foobar/test@v1.0.2/test.go'; + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world', + DirectoryPath: '/src/hello-world', + Files: ['src/hello-world/hello.go', 'src/hello-world/world.go'] + }; + + const testPackage: PackageBuildInfo = { + ImportPath: 'foobar/test', + DirectoryPath: 'remote/goroot/src/foobar/test@v1.0.2', + Files: ['remote/goroot/src/foobar/test@v1.0.2/test.go'] + }; + + const localPath = path.join(process.env.GOROOT, 'src', 'foobar/test@v1.0.2/test.go'); + const existsSyncStub = sinon.stub(fileSystem, 'existsSync'); + existsSyncStub.withArgs(localPath).returns(true); + + remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage]; + + goDebugSession['localPathSeparator'] = '/'; + const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath); + assert.strictEqual(inferredLocalPath, localPath); + }); + + test('inferLocalPathFromRemoteGoPackage works for package in GOROOT/src with relative path', () => { + const remotePath = 'foobar/test@v1.0.2/test.go'; + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world', + DirectoryPath: '/src/hello-world', + Files: ['src/hello-world/hello.go', 'src/hello-world/world.go'] + }; + + const testPackage: PackageBuildInfo = { + ImportPath: 'foobar/test', + DirectoryPath: 'foobar/test@v1.0.2', + Files: ['foobar/test@v1.0.2/test.go'] + }; + + const localPath = path.join(process.env.GOROOT, 'src', 'foobar/test@v1.0.2/test.go'); + const existsSyncStub = sinon.stub(fileSystem, 'existsSync'); + existsSyncStub.withArgs(localPath).returns(true); + + remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage]; + + goDebugSession['localPathSeparator'] = '/'; + const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath); + assert.strictEqual(inferredLocalPath, localPath); + }); +}); + +suite('RemoteSourcesAndPackages Tests', () => { + const helloPackage: PackageBuildInfo = { + ImportPath: 'hello-world', + DirectoryPath: '/src/hello-world', + Files: ['src/hello-world/hello.go', 'src/hello-world/world.go'] + }; + const testPackage: PackageBuildInfo = { + ImportPath: 'test', + DirectoryPath: '/src/test', + Files: ['src/test/test.go'] + }; + const sources = ['src/hello-world/hello.go', 'src/hello-world/world.go', 'src/test/test.go']; + let remoteSourcesAndPackages: RemoteSourcesAndPackages; + let delve: Delve; + setup(() => { + delve = {callPromise: () => ({}), isApiV1: false} as unknown as Delve; + remoteSourcesAndPackages = new RemoteSourcesAndPackages(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('initializeRemotePackagesAndSources retrieves remote packages and sources', async () => { + const stub = sinon.stub(delve, 'callPromise'); + stub.withArgs('ListPackagesBuildInfo', [{IncludeFiles: true}]) + .returns(Promise.resolve({List: [helloPackage, testPackage]})); + stub.withArgs('ListSources', [{}]) + .returns(Promise.resolve({Sources: sources})); + + await remoteSourcesAndPackages.initializeRemotePackagesAndSources(delve); + assert.deepEqual(remoteSourcesAndPackages.remoteSourceFiles, sources); + assert.deepEqual(remoteSourcesAndPackages.remotePackagesBuildInfo, [helloPackage, testPackage]); + }); +});