From 753a7ad46cc0c1397f86e89b12438283284d8374 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:38:52 +0000 Subject: [PATCH 1/6] Initial plan From 1f246e66f67d74bf9d93e784931c65ff40f9142c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:48:39 +0000 Subject: [PATCH 2/6] Add comprehensive tests for resolveAndUpdatePythonPath prioritization logic Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- .../configuration/resolvers/base.unit.test.ts | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) diff --git a/src/test/unittest/configuration/resolvers/base.unit.test.ts b/src/test/unittest/configuration/resolvers/base.unit.test.ts index ae63c4b9..34c80bf4 100644 --- a/src/test/unittest/configuration/resolvers/base.unit.test.ts +++ b/src/test/unittest/configuration/resolvers/base.unit.test.ts @@ -15,6 +15,7 @@ import * as helper from '../../../../extension/debugger/configuration/resolvers/ import * as vscodeapi from '../../../../extension/common/vscodeapi'; import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../../extension/types'; import { PythonEnvironment } from '../../../../extension/debugger/adapter/types'; +import { PythonPathSource } from '../../../../extension/debugger/types'; import * as pythonApi from '../../../../extension/common/python'; suite('Debugging - Config Resolver', () => { @@ -61,6 +62,10 @@ suite('Debugging - Config Resolver', () => { public isDebuggingFlask(debugConfiguration: Partial) { return BaseConfigurationResolver.isDebuggingFlask(debugConfiguration); } + + public getPythonPathSource() { + return this.pythonPathSource; + } } let resolver: BaseResolver; let getWorkspaceFoldersStub: sinon.SinonStub; @@ -265,6 +270,356 @@ suite('Debugging - Config Resolver', () => { expect(config).to.have.property('debugLauncherPython', pythonPath); }); + // Tests for prioritization of python path configuration + suite('resolveAndUpdatePythonPath prioritization tests', () => { + test('When pythonPath is a concrete path and python is undefined, python should be set to pythonPath value', async () => { + const expectedPath = path.join('path', 'to', 'custom', 'python'); + const config = { + pythonPath: expectedPath, + python: undefined, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', expectedPath); + }); + + test('When pythonPath is a concrete path and python is a different concrete path, python should take precedence', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + }); + + test('When pythonPath is ${command:python.interpreterPath} and python is a concrete path, python should take precedence', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: pythonValue, + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + }); + + test('When pythonPath is a concrete path and python is ${command:python.interpreterPath}, python should resolve from interpreter', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: pythonPathValue, + python: '${command:python.interpreterPath}', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); + + test('When both pythonPath and python are ${command:python.interpreterPath}, both should resolve to interpreter path', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: '${command:python.interpreterPath}', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); + + test('When pythonPath is not set and python is not set, both should resolve from interpreter', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = {}; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); + + test('debugAdapterPython should use pythonPath when neither debugAdapterPython nor python are set', async () => { + const pythonPathValue = path.join('path', 'to', 'custom', 'python'); + const config = { + pythonPath: pythonPathValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugAdapterPython', pythonPathValue); + }); + + test('debugAdapterPython should use python when pythonPath is set but python has different value', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', pythonValue); + }); + + test('debugAdapterPython should prefer explicitly set debugAdapterPython over pythonPath', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); + const config = { + pythonPath: pythonPathValue, + debugAdapterPython: debugAdapterValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugAdapterPython', debugAdapterValue); + }); + + test('debugLauncherPython should use pythonPath when neither debugLauncherPython nor python are set', async () => { + const pythonPathValue = path.join('path', 'to', 'custom', 'python'); + const config = { + pythonPath: pythonPathValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugLauncherPython', pythonPathValue); + }); + + test('debugLauncherPython should use python when pythonPath is set but python has different value', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugLauncherPython', pythonValue); + }); + + test('debugLauncherPython should prefer explicitly set debugLauncherPython over pythonPath', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); + const config = { + pythonPath: pythonPathValue, + debugLauncherPython: debugLauncherValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugLauncherPython', debugLauncherValue); + }); + + test('All three debug python fields can have different values when explicitly set', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); + const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); + const config = { + python: pythonValue, + debugAdapterPython: debugAdapterValue, + debugLauncherPython: debugLauncherValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', debugAdapterValue); + expect(config).to.have.property('debugLauncherPython', debugLauncherValue); + }); + + test('When debugAdapterPython is ${command:python.interpreterPath}, it should fallback to resolved pythonPath', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + debugAdapterPython: '${command:python.interpreterPath}', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + expect(config).to.have.property('debugAdapterPython', interpreterPath); + }); + + test('When debugLauncherPython is ${command:python.interpreterPath}, it should fallback to resolved pythonPath', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + debugLauncherPython: '${command:python.interpreterPath}', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + expect(config).to.have.property('debugLauncherPython', interpreterPath); + }); + + test('Complex scenario: pythonPath set, python differs, debugAdapterPython and debugLauncherPython both set differently', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); + const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + debugAdapterPython: debugAdapterValue, + debugLauncherPython: debugLauncherValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', debugAdapterValue); + expect(config).to.have.property('debugLauncherPython', debugLauncherValue); + }); + + test('When pythonPath is undefined and python is concrete path, debugAdapter and debugLauncher should use python', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + python: pythonValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', pythonValue); + expect(config).to.have.property('debugLauncherPython', pythonValue); + }); + + test('When pythonPath is empty string, it should be treated as not set and resolve from interpreter', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); + + // Tests for pythonPathSource field + test('pythonPathSource should be settingsJson when python is ${command:python.interpreterPath}', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + python: '${command:python.interpreterPath}', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); + }); + + test('pythonPathSource should be settingsJson when python is undefined', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: interpreterPath, + python: undefined, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); + }); + + test('pythonPathSource should be launchJson when python is explicitly set to a concrete path', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + python: pythonValue, + }; + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.launchJson); + }); + + test('pythonPathSource should be launchJson when python is a concrete path even if pythonPath is ${command:python.interpreterPath}', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: pythonValue, + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.launchJson); + }); + + test('pythonPathSource should be settingsJson when both pythonPath and python are ${command:python.interpreterPath}', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: '${command:python.interpreterPath}', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); + }); + + test('pythonPathSource should be settingsJson when neither pythonPath nor python are set', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = {}; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); + }); + }); + const localHostTestMatrix: Record = { localhost: true, '127.0.0.1': true, From 3b64f0c369c7930e4ceac879e3d2e41ce5443d71 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:45:37 -0700 Subject: [PATCH 3/6] add tests for resolveAndUpdatePythonPath --- .vscode/launch.json | 6 +- .../configuration/resolvers/base.unit.test.ts | 664 ++++++++++-------- 2 files changed, 357 insertions(+), 313 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index da993863..0b406f92 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,9 +27,9 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test/unittest/index" ], - "env": { - "TEST_GREP": "Python API Tests" - }, + // "env": { + // "TEST_GREP": "Python API Tests" + // }, "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "tasks: watch-tests" } diff --git a/src/test/unittest/configuration/resolvers/base.unit.test.ts b/src/test/unittest/configuration/resolvers/base.unit.test.ts index 34c80bf4..14765f6d 100644 --- a/src/test/unittest/configuration/resolvers/base.unit.test.ts +++ b/src/test/unittest/configuration/resolvers/base.unit.test.ts @@ -270,399 +270,443 @@ suite('Debugging - Config Resolver', () => { expect(config).to.have.property('debugLauncherPython', pythonPath); }); - // Tests for prioritization of python path configuration - suite('resolveAndUpdatePythonPath prioritization tests', () => { - test('When pythonPath is a concrete path and python is undefined, python should be set to pythonPath value', async () => { - const expectedPath = path.join('path', 'to', 'custom', 'python'); - const config = { - pythonPath: expectedPath, - python: undefined, - }; - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', expectedPath); + const localHostTestMatrix: Record = { + localhost: true, + '127.0.0.1': true, + '::1': true, + '127.0.0.2': false, + '156.1.2.3': false, + '::2': false, + }; + Object.keys(localHostTestMatrix).forEach((key) => { + test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { + const isLocalHost = resolver.isLocalHost(key); + + expect(isLocalHost).to.equal(localHostTestMatrix[key]); }); + }); + test('Is debugging fastapi=true', () => { + const config = { module: 'fastapi' }; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(true, 'not fastapi'); + }); + test('Is debugging fastapi=false', () => { + const config = { module: 'fastapi2' }; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(false, 'fastapi'); + }); + test('Is debugging fastapi=false when not defined', () => { + const config = {}; + const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); + expect(isFastAPI).to.equal(false, 'fastapi'); + }); + test('Is debugging flask=true', () => { + const config = { module: 'flask' }; + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); + expect(isFlask).to.equal(true, 'not flask'); + }); + test('Is debugging flask=false', () => { + const config = { module: 'flask2' }; + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); + expect(isFlask).to.equal(false, 'flask'); + }); + test('Is debugging flask=false when not defined', () => { + const config = {}; + const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); + expect(isFlask).to.equal(false, 'flask'); + }); +}); - test('When pythonPath is a concrete path and python is a different concrete path, python should take precedence', async () => { - const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); - const pythonValue = path.join('path', 'to', 'python', 'python'); - const config = { - pythonPath: pythonPathValue, - python: pythonValue, - }; +// Tests for prioritization of python path configuration +suite('resolveAndUpdatePythonPath prioritization tests', () => { + class BaseResolver2 extends BaseConfigurationResolver { + public resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { + throw new Error('Not Implemented'); + } - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + public resolveDebugConfigurationWithSubstitutedVariables( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { + throw new Error('Not Implemented'); + } - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonValue); - }); + public resolveAndUpdatePythonPath( + workspaceFolderUri: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ) { + return super.resolveAndUpdatePythonPath(workspaceFolderUri, debugConfiguration); + } - test('When pythonPath is ${command:python.interpreterPath} and python is a concrete path, python should take precedence', async () => { - const pythonValue = path.join('path', 'to', 'python', 'python'); - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: '${command:python.interpreterPath}', - python: pythonValue, - }; + public getPythonPathSource() { + return this.pythonPathSource; + } + } - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + let resolver: BaseResolver2; + let getInterpreterDetailsStub: sinon.SinonStub; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + setup(() => { + resolver = new BaseResolver2(); + getInterpreterDetailsStub = sinon.stub(pythonApi, 'getInterpreterDetails'); + }); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonValue); - }); + teardown(() => { + sinon.restore(); + }); - test('When pythonPath is a concrete path and python is ${command:python.interpreterPath}, python should resolve from interpreter', async () => { - const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: pythonPathValue, - python: '${command:python.interpreterPath}', - }; + test('When pythonPath is a concrete path and python is undefined, python should be set to pythonPath value', async () => { + const expectedPath = path.join('path', 'to', 'custom', 'python'); + const config = { + pythonPath: expectedPath, + python: undefined, + }; - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', expectedPath); + }); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', interpreterPath); - }); + test('When pythonPath is a concrete path and python is a different concrete path, python should take precedence', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + }; - test('When both pythonPath and python are ${command:python.interpreterPath}, both should resolve to interpreter path', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: '${command:python.interpreterPath}', - python: '${command:python.interpreterPath}', - }; + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + }); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + test('When pythonPath is ${command:python.interpreterPath} and python is a concrete path, python should take precedence', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: pythonValue, + }; - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', interpreterPath); - }); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - test('When pythonPath is not set and python is not set, both should resolve from interpreter', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = {}; + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + }); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + test('When pythonPath is a concrete path and python is ${command:python.interpreterPath}, python should resolve from interpreter', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: pythonPathValue, + python: '${command:python.interpreterPath}', + }; - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', interpreterPath); - }); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - test('debugAdapterPython should use pythonPath when neither debugAdapterPython nor python are set', async () => { - const pythonPathValue = path.join('path', 'to', 'custom', 'python'); - const config = { - pythonPath: pythonPathValue, - }; + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPathValue); - expect(config).to.have.property('debugAdapterPython', pythonPathValue); - }); + test('When both pythonPath and python are ${command:python.interpreterPath}, both should resolve to interpreter path', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: '${command:python.interpreterPath}', + }; - test('debugAdapterPython should use python when pythonPath is set but python has different value', async () => { - const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); - const pythonValue = path.join('path', 'to', 'python', 'python'); - const config = { - pythonPath: pythonPathValue, - python: pythonValue, - }; + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonValue); - expect(config).to.have.property('debugAdapterPython', pythonValue); - }); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); - test('debugAdapterPython should prefer explicitly set debugAdapterPython over pythonPath', async () => { - const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); - const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); - const config = { - pythonPath: pythonPathValue, - debugAdapterPython: debugAdapterValue, - }; + test('When pythonPath is not set and python is not set, both should resolve from interpreter', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = {}; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPathValue); - expect(config).to.have.property('debugAdapterPython', debugAdapterValue); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('debugLauncherPython should use pythonPath when neither debugLauncherPython nor python are set', async () => { - const pythonPathValue = path.join('path', 'to', 'custom', 'python'); - const config = { - pythonPath: pythonPathValue, - }; + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + test('debugAdapterPython should use pythonPath when neither debugAdapterPython nor python are set', async () => { + const pythonPathValue = path.join('path', 'to', 'custom', 'python'); + const config = { + pythonPath: pythonPathValue, + }; - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPathValue); - expect(config).to.have.property('debugLauncherPython', pythonPathValue); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('debugLauncherPython should use python when pythonPath is set but python has different value', async () => { - const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); - const pythonValue = path.join('path', 'to', 'python', 'python'); - const config = { - pythonPath: pythonPathValue, - python: pythonValue, - }; + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugAdapterPython', pythonPathValue); + }); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + test('debugAdapterPython should use pythonPath when pythonPath is set but python has different value', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + }; - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonValue); - expect(config).to.have.property('debugLauncherPython', pythonValue); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('debugLauncherPython should prefer explicitly set debugLauncherPython over pythonPath', async () => { - const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); - const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); - const config = { - pythonPath: pythonPathValue, - debugLauncherPython: debugLauncherValue, - }; + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', pythonPathValue); + }); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + test('debugAdapterPython should prefer explicitly set debugAdapterPython over pythonPath', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); + const config = { + pythonPath: pythonPathValue, + debugAdapterPython: debugAdapterValue, + }; - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonPathValue); - expect(config).to.have.property('debugLauncherPython', debugLauncherValue); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('All three debug python fields can have different values when explicitly set', async () => { - const pythonValue = path.join('path', 'to', 'python', 'python'); - const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); - const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); - const config = { - python: pythonValue, - debugAdapterPython: debugAdapterValue, - debugLauncherPython: debugLauncherValue, - }; - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonValue); - expect(config).to.have.property('debugAdapterPython', debugAdapterValue); - expect(config).to.have.property('debugLauncherPython', debugLauncherValue); - }); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugAdapterPython', debugAdapterValue); + }); - test('When debugAdapterPython is ${command:python.interpreterPath}, it should fallback to resolved pythonPath', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: '${command:python.interpreterPath}', - debugAdapterPython: '${command:python.interpreterPath}', - }; + test('debugLauncherPython should use pythonPath when neither debugLauncherPython nor python are set', async () => { + const pythonPathValue = path.join('path', 'to', 'custom', 'python'); + const config = { + pythonPath: pythonPathValue, + }; - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugLauncherPython', pythonPathValue); + }); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', interpreterPath); - expect(config).to.have.property('debugAdapterPython', interpreterPath); - }); + test('debugLauncherPython should use pythonPath when pythonPath is set but python has different value', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + }; - test('When debugLauncherPython is ${command:python.interpreterPath}, it should fallback to resolved pythonPath', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: '${command:python.interpreterPath}', - debugLauncherPython: '${command:python.interpreterPath}', - }; + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugLauncherPython', pythonPathValue); + }); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + test('debugLauncherPython should prefer explicitly set debugLauncherPython over pythonPath', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); + const config = { + pythonPath: pythonPathValue, + debugLauncherPython: debugLauncherValue, + }; - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', interpreterPath); - expect(config).to.have.property('debugLauncherPython', interpreterPath); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('Complex scenario: pythonPath set, python differs, debugAdapterPython and debugLauncherPython both set differently', async () => { - const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); - const pythonValue = path.join('path', 'to', 'python', 'python'); - const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); - const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); - const config = { - pythonPath: pythonPathValue, - python: pythonValue, - debugAdapterPython: debugAdapterValue, - debugLauncherPython: debugLauncherValue, - }; - - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonValue); - expect(config).to.have.property('debugAdapterPython', debugAdapterValue); - expect(config).to.have.property('debugLauncherPython', debugLauncherValue); - }); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonPathValue); + expect(config).to.have.property('debugLauncherPython', debugLauncherValue); + }); - test('When pythonPath is undefined and python is concrete path, debugAdapter and debugLauncher should use python', async () => { - const pythonValue = path.join('path', 'to', 'python', 'python'); - const config = { - python: pythonValue, - }; + test('All three debug python fields can have different values when explicitly set', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); + const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); + const config = { + python: pythonValue, + debugAdapterPython: debugAdapterValue, + debugLauncherPython: debugLauncherValue, + }; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', pythonValue); - expect(config).to.have.property('debugAdapterPython', pythonValue); - expect(config).to.have.property('debugLauncherPython', pythonValue); - }); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', debugAdapterValue); + expect(config).to.have.property('debugLauncherPython', debugLauncherValue); + }); - test('When pythonPath is empty string, it should be treated as not set and resolve from interpreter', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: '', - }; + test('When debugAdapterPython is ${command:python.interpreterPath}, it should fallback to resolved pythonPath', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + debugAdapterPython: '${command:python.interpreterPath}', + }; - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(config).to.not.have.property('pythonPath'); - expect(config).to.have.property('python', interpreterPath); - }); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + expect(config).to.have.property('debugAdapterPython', interpreterPath); + }); - // Tests for pythonPathSource field - test('pythonPathSource should be settingsJson when python is ${command:python.interpreterPath}', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - python: '${command:python.interpreterPath}', - }; + test('When debugLauncherPython is ${command:python.interpreterPath}, it should fallback to resolved pythonPath', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + debugLauncherPython: '${command:python.interpreterPath}', + }; - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); - }); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + expect(config).to.have.property('debugLauncherPython', interpreterPath); + }); - test('pythonPathSource should be settingsJson when python is undefined', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: interpreterPath, - python: undefined, - }; + test('Complex scenario: pythonPath set, python differs, debugAdapterPython and debugLauncherPython both set differently', async () => { + const pythonPathValue = path.join('path', 'to', 'pythonPath', 'python'); + const pythonValue = path.join('path', 'to', 'python', 'python'); + const debugAdapterValue = path.join('path', 'to', 'debugAdapter', 'python'); + const debugLauncherValue = path.join('path', 'to', 'debugLauncher', 'python'); + const config = { + pythonPath: pythonPathValue, + python: pythonValue, + debugAdapterPython: debugAdapterValue, + debugLauncherPython: debugLauncherValue, + }; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); - }); + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', debugAdapterValue); + expect(config).to.have.property('debugLauncherPython', debugLauncherValue); + }); - test('pythonPathSource should be launchJson when python is explicitly set to a concrete path', async () => { - const pythonValue = path.join('path', 'to', 'python', 'python'); - const config = { - python: pythonValue, - }; + test('When pythonPath is undefined and python is concrete path, debugAdapter and debugLauncher should use resolved pythonPath from interpreter', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + python: pythonValue, + }; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.launchJson); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('pythonPathSource should be launchJson when python is a concrete path even if pythonPath is ${command:python.interpreterPath}', async () => { - const pythonValue = path.join('path', 'to', 'python', 'python'); - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: '${command:python.interpreterPath}', - python: pythonValue, - }; + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', pythonValue); + expect(config).to.have.property('debugAdapterPython', interpreterPath); + expect(config).to.have.property('debugLauncherPython', interpreterPath); + }); - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + test('When pythonPath is empty string, it should be treated as not set and resolve from interpreter', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '', + }; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.launchJson); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('pythonPathSource should be settingsJson when both pythonPath and python are ${command:python.interpreterPath}', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = { - pythonPath: '${command:python.interpreterPath}', - python: '${command:python.interpreterPath}', - }; + expect(config).to.not.have.property('pythonPath'); + expect(config).to.have.property('python', interpreterPath); + }); - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + // Tests for pythonPathSource field + test('pythonPathSource should be settingsJson when python is ${command:python.interpreterPath}', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + python: '${command:python.interpreterPath}', + }; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); - expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); - }); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - test('pythonPathSource should be settingsJson when neither pythonPath nor python are set', async () => { - const interpreterPath = path.join('path', 'from', 'interpreter'); - const config = {}; + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); + }); - getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + test('pythonPathSource should be settingsJson when python is undefined', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: interpreterPath, + python: undefined, + }; - await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); - expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); - }); + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); }); - const localHostTestMatrix: Record = { - localhost: true, - '127.0.0.1': true, - '::1': true, - '127.0.0.2': false, - '156.1.2.3': false, - '::2': false, - }; - Object.keys(localHostTestMatrix).forEach((key) => { - test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { - const isLocalHost = resolver.isLocalHost(key); + test('pythonPathSource should be launchJson when python is explicitly set to a concrete path', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const config = { + python: pythonValue, + }; - expect(isLocalHost).to.equal(localHostTestMatrix[key]); - }); - }); - test('Is debugging fastapi=true', () => { - const config = { module: 'fastapi' }; - const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); - expect(isFastAPI).to.equal(true, 'not fastapi'); - }); - test('Is debugging fastapi=false', () => { - const config = { module: 'fastapi2' }; - const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); - expect(isFastAPI).to.equal(false, 'fastapi'); - }); - test('Is debugging fastapi=false when not defined', () => { - const config = {}; - const isFastAPI = resolver.isDebuggingFastAPI(config as LaunchRequestArguments); - expect(isFastAPI).to.equal(false, 'fastapi'); + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.launchJson); }); - test('Is debugging flask=true', () => { - const config = { module: 'flask' }; - const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); - expect(isFlask).to.equal(true, 'not flask'); + + test('pythonPathSource should be launchJson when python is a concrete path even if pythonPath is ${command:python.interpreterPath}', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: pythonValue, + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.launchJson); }); - test('Is debugging flask=false', () => { - const config = { module: 'flask2' }; - const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); - expect(isFlask).to.equal(false, 'flask'); + + test('pythonPathSource should be settingsJson when both pythonPath and python are ${command:python.interpreterPath}', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + pythonPath: '${command:python.interpreterPath}', + python: '${command:python.interpreterPath}', + }; + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); }); - test('Is debugging flask=false when not defined', () => { + + test('pythonPathSource should be settingsJson when neither pythonPath nor python are set', async () => { + const interpreterPath = path.join('path', 'from', 'interpreter'); const config = {}; - const isFlask = resolver.isDebuggingFlask(config as LaunchRequestArguments); - expect(isFlask).to.equal(false, 'flask'); + + getInterpreterDetailsStub.resolves({ path: [interpreterPath] } as unknown as PythonEnvironment); + + await resolver.resolveAndUpdatePythonPath(undefined, config as LaunchRequestArguments); + + expect(resolver.getPythonPathSource()).to.equal(PythonPathSource.settingsJson); }); }); From beae7cbc9bb6cafb68705b8659350c7b24b6388d Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:04:53 -0700 Subject: [PATCH 4/6] refactor resolvingWorkspaceVars and implement deprecation telemetry tracker --- .../configuration/providers/pyramidLaunch.ts | 4 +- .../debugger/configuration/resolvers/base.ts | 10 +-- .../configuration/resolvers/launch.ts | 4 +- .../debugger/configuration/utils/common.ts | 67 +++++++++++++------ src/extension/telemetry/constants.ts | 1 + src/extension/telemetry/index.ts | 17 +++++ .../providers/pyramidLaunch.unit.test.ts | 4 +- 7 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/extension/debugger/configuration/providers/pyramidLaunch.ts b/src/extension/debugger/configuration/providers/pyramidLaunch.ts index 47476be9..39aee600 100644 --- a/src/extension/debugger/configuration/providers/pyramidLaunch.ts +++ b/src/extension/debugger/configuration/providers/pyramidLaunch.ts @@ -13,7 +13,7 @@ import { EventName } from '../../../telemetry/constants'; import { DebuggerTypeName } from '../../../constants'; import { LaunchRequestArguments } from '../../../types'; import { DebugConfigurationState, DebugConfigurationType } from '../../types'; -import { resolveVariables } from '../utils/common'; +import { resolveWorkspaceVariables } from '../utils/common'; const workspaceFolderToken = '${workspaceFolder}'; @@ -73,7 +73,7 @@ export async function validateIniPath( if (!selected || selected.trim().length === 0) { return error; } - const resolvedPath = resolveVariables(selected, undefined, folder); + const resolvedPath = resolveWorkspaceVariables(selected, undefined, folder); if (resolvedPath) { if (selected !== defaultValue && !fs.pathExists(resolvedPath)) { return error; diff --git a/src/extension/debugger/configuration/resolvers/base.ts b/src/extension/debugger/configuration/resolvers/base.ts index 68025da8..1fef6f1e 100644 --- a/src/extension/debugger/configuration/resolvers/base.ts +++ b/src/extension/debugger/configuration/resolvers/base.ts @@ -12,7 +12,7 @@ import { getWorkspaceFolders, getWorkspaceFolder as getVSCodeWorkspaceFolder } f import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; import { PythonPathSource } from '../../types'; import { IDebugConfigurationResolver } from '../types'; -import { resolveVariables } from '../utils/common'; +import { resolveWorkspaceVariables } from '../utils/common'; import { getProgram } from './helper'; import { getSettingsPythonPath, getInterpreterDetails } from '../../../common/python'; import { getOSType, OSType } from '../../../common/platform'; @@ -96,7 +96,7 @@ export abstract class BaseConfigurationResolver return; } if (debugConfiguration.envFile && (workspaceFolder || debugConfiguration.cwd)) { - debugConfiguration.envFile = resolveVariables( + debugConfiguration.envFile = resolveWorkspaceVariables( debugConfiguration.envFile, (workspaceFolder ? workspaceFolder.fsPath : undefined) || debugConfiguration.cwd, undefined, @@ -118,7 +118,7 @@ export abstract class BaseConfigurationResolver : await getSettingsPythonPath(workspaceFolder); debugConfiguration.pythonPath = interpreterPath ? interpreterPath[0] : interpreterPath; } else { - debugConfiguration.pythonPath = resolveVariables( + debugConfiguration.pythonPath = resolveWorkspaceVariables( debugConfiguration.pythonPath ? debugConfiguration.pythonPath : undefined, workspaceFolder?.fsPath, undefined, @@ -137,7 +137,7 @@ export abstract class BaseConfigurationResolver debugConfiguration.python = debugConfiguration.pythonPath; } else { this.pythonPathSource = PythonPathSource.launchJson; - debugConfiguration.python = resolveVariables( + debugConfiguration.python = resolveWorkspaceVariables( debugConfiguration.python ?? debugConfiguration.pythonPath, workspaceFolder?.fsPath, undefined, @@ -194,7 +194,7 @@ export abstract class BaseConfigurationResolver } else { // Expand ${workspaceFolder} variable first if necessary. pathMappings = pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => { - const resolvedLocalRoot = resolveVariables(mappedLocalRoot, defaultLocalRoot, undefined); + const resolvedLocalRoot = resolveWorkspaceVariables(mappedLocalRoot, defaultLocalRoot, undefined); return { localRoot: resolvedLocalRoot || '', // TODO: Apply to remoteRoot too? diff --git a/src/extension/debugger/configuration/resolvers/launch.ts b/src/extension/debugger/configuration/resolvers/launch.ts index 85e89208..89efe76a 100644 --- a/src/extension/debugger/configuration/resolvers/launch.ts +++ b/src/extension/debugger/configuration/resolvers/launch.ts @@ -8,7 +8,7 @@ import { getOSType, OSType } from '../../../common/platform'; import { getEnvFile } from '../../../common/settings'; import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, DebugPurpose, LaunchRequestArguments } from '../../../types'; -import { resolveVariables } from '../utils/common'; +import { resolveWorkspaceVariables } from '../utils/common'; import { BaseConfigurationResolver } from './base'; import { getDebugEnvironmentVariables, getProgram } from './helper'; import { getConfiguration } from '../../../common/vscodeapi'; @@ -83,7 +83,7 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver { - const newValue = variablesObject[name]; - if (isString(newValue)) { - return newValue; - } - return match && (match.indexOf('env.') > 0 || match.indexOf('env:') > 0) ? '' : match; - }); + if (!value) { + return value; } - return value; + + // opt for folder with fallback to rootFolder + const workspaceFolder = folder ? getWorkspaceFolder(folder.uri) : undefined; + const workspaceFolderPath = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder; + + // Replace all ${variable} patterns + return value.replace(/\$\{([^}]+)\}/g, (match: string, variableName: string) => { + // Handle workspaceFolder variable + if (variableName === 'workspaceFolder' && isString(workspaceFolderPath)) { + // Track usage of this potentially deprecated code path + sendTelemetryEvent(EventName.DEPRECATED_CODE_PATH_USAGE, undefined, { + codePath: 'workspaceFolder_substitution', + }); + return workspaceFolderPath; + } + + // Replace environment variables with empty string + if (variableName.startsWith('env.') || variableName.startsWith('env:')) { + // Track usage of this potentially deprecated code path + sendTelemetryEvent(EventName.DEPRECATED_CODE_PATH_USAGE, undefined, { + codePath: 'env_variable_substitution', + }); + return ''; + } + + // Unknown variables are left unchanged + return match; + }); } diff --git a/src/extension/telemetry/constants.ts b/src/extension/telemetry/constants.ts index 94ff6643..d75bb898 100644 --- a/src/extension/telemetry/constants.ts +++ b/src/extension/telemetry/constants.ts @@ -24,4 +24,5 @@ export enum EventName { USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', DEBUGGER_PYTHON_37_DEPRECATED = 'DEBUGGER_PYTHON_37_DEPRECATED', DEBUGGER_SHOW_PYTHON_INLINE_VALUES = 'DEBUGGER_SHOW_PYTHON_INLINE_VALUES', + DEPRECATED_CODE_PATH_USAGE = 'DEPRECATED_CODE_PATH_USAGE', } diff --git a/src/extension/telemetry/index.ts b/src/extension/telemetry/index.ts index 5be026fe..ef647b4e 100644 --- a/src/extension/telemetry/index.ts +++ b/src/extension/telemetry/index.ts @@ -691,4 +691,21 @@ export interface IEventNamePropertyMapping { "DEBUGGER_SHOW_PYTHON_INLINE_VALUES" : { "owner": "eleanorjboyd" } */ [EventName.DEBUGGER_SHOW_PYTHON_INLINE_VALUES]: never | undefined; + /** + * Telemetry event sent when potentially deprecated code paths are executed. + */ + /* __GDPR__ + "deprecated_code_path_usage" : { + "codepath" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventName.DEPRECATED_CODE_PATH_USAGE]: { + /** + * Identifier for the specific deprecated code path that was executed. + * Examples: 'workspaceFolder_substitution', 'env_variable_substitution' + * + * @type {string} + */ + codePath: string; + }; } diff --git a/src/test/unittest/configuration/providers/pyramidLaunch.unit.test.ts b/src/test/unittest/configuration/providers/pyramidLaunch.unit.test.ts index e04acada..8aaad1a9 100644 --- a/src/test/unittest/configuration/providers/pyramidLaunch.unit.test.ts +++ b/src/test/unittest/configuration/providers/pyramidLaunch.unit.test.ts @@ -12,10 +12,10 @@ import { Uri } from 'vscode'; import { DebugConfigStrings } from '../../../../extension/common/utils/localize'; import { MultiStepInput } from '../../../../extension/common/multiStepInput'; import { DebuggerTypeName } from '../../../../extension/constants'; -import { resolveVariables } from '../../../../extension/debugger/configuration/utils/common'; import * as pyramidLaunch from '../../../../extension/debugger/configuration/providers/pyramidLaunch'; import { DebugConfigurationState } from '../../../../extension/debugger/types'; import * as vscodeapi from '../../../../extension/common/vscodeapi'; +import { resolveWorkspaceVariables } from '../../../../extension/debugger/configuration/utils/common'; suite('Debugging - Configuration Provider Pyramid', () => { let input: MultiStepInput; @@ -52,7 +52,7 @@ suite('Debugging - Configuration Provider Pyramid', () => { test('Resolve variables (with resource)', async () => { const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; workspaceStub.returns(folder); - const resolvedPath = resolveVariables('${workspaceFolder}/one.py', undefined, folder); + const resolvedPath = resolveWorkspaceVariables('${workspaceFolder}/one.py', undefined, folder); expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); }); From 112738b7e0996e0805ddf28c45373e34e5b0ce74 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:48:37 -0700 Subject: [PATCH 5/6] refactor resolveAndUpdatePythonPath --- .../debugger/configuration/resolvers/base.ts | 126 +++++++++++++----- 1 file changed, 95 insertions(+), 31 deletions(-) diff --git a/src/extension/debugger/configuration/resolvers/base.ts b/src/extension/debugger/configuration/resolvers/base.ts index 1fef6f1e..ed3bff04 100644 --- a/src/extension/debugger/configuration/resolvers/base.ts +++ b/src/extension/debugger/configuration/resolvers/base.ts @@ -80,6 +80,16 @@ export abstract class BaseConfigurationResolver return undefined; } + /** + * Resolves and updates file paths and Python interpreter paths in the debug configuration. + * + * This method performs two main operations: + * 1. Resolves workspace variables in the envFile path (if specified) + * 2. Resolves and updates Python interpreter paths, handling legacy pythonPath deprecation + * + * @param workspaceFolder The workspace folder URI for variable resolution + * @param debugConfiguration The launch configuration to update + */ protected async resolveAndUpdatePaths( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments, @@ -88,22 +98,38 @@ export abstract class BaseConfigurationResolver await this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); } + /** + * Resolves workspace variables in the envFile path. + * + * Expands variables like ${workspaceFolder} in the envFile configuration using the + * workspace folder path or current working directory as the base for resolution. + * + * @param workspaceFolder The workspace folder URI for variable resolution + * @param debugConfiguration The launch configuration containing the envFile path + */ protected static resolveAndUpdateEnvFilePath( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments, ): void { - if (!debugConfiguration) { + // Early exit if no configuration or no envFile to resolve + if (!debugConfiguration?.envFile) { return; } - if (debugConfiguration.envFile && (workspaceFolder || debugConfiguration.cwd)) { - debugConfiguration.envFile = resolveWorkspaceVariables( - debugConfiguration.envFile, - (workspaceFolder ? workspaceFolder.fsPath : undefined) || debugConfiguration.cwd, - undefined, - ); + + const basePath = workspaceFolder?.fsPath || debugConfiguration.cwd; + + if (basePath) { + // update envFile with resolved variables + debugConfiguration.envFile = resolveWorkspaceVariables(debugConfiguration.envFile, basePath, undefined); } } + /** + * Resolves Python interpreter paths and handles the legacy pythonPath deprecation. + * + * @param workspaceFolder The workspace folder URI for variable resolution and interpreter detection + * @param debugConfiguration The launch configuration to update with resolved Python paths + */ protected async resolveAndUpdatePythonPath( workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments, @@ -111,53 +137,91 @@ export abstract class BaseConfigurationResolver if (!debugConfiguration) { return; } + + // get the interpreter details in the context of the workspace folder + const interpreterDetail = await getInterpreterDetails(workspaceFolder); + const interpreterPath = interpreterDetail?.path ?? (await getSettingsPythonPath(workspaceFolder)); + const resolvedInterpreterPath = interpreterPath ? interpreterPath[0] : interpreterPath; + + traceLog( + `resolveAndUpdatePythonPath - Initial state: ` + + `pythonPath='${debugConfiguration.pythonPath}', ` + + `python='${debugConfiguration.python}', ` + + `debugAdapterPython='${debugConfiguration.debugAdapterPython}', ` + + `debugLauncherPython='${debugConfiguration.debugLauncherPython}', ` + + `workspaceFolder='${workspaceFolder?.fsPath}'` + + `resolvedInterpreterPath='${resolvedInterpreterPath}'`, + ); + + // STEP 1: Resolve legacy pythonPath property (DEPRECATED) + // pythonPath will equal user set value, or getInterpreterDetails if undefined or set to command if (debugConfiguration.pythonPath === '${command:python.interpreterPath}' || !debugConfiguration.pythonPath) { - const interpreterDetail = await getInterpreterDetails(workspaceFolder); - const interpreterPath = interpreterDetail - ? interpreterDetail.path - : await getSettingsPythonPath(workspaceFolder); - debugConfiguration.pythonPath = interpreterPath ? interpreterPath[0] : interpreterPath; + this.pythonPathSource = PythonPathSource.settingsJson; + debugConfiguration.pythonPath = resolvedInterpreterPath; } else { + // User provided explicit pythonPath in launch.json debugConfiguration.pythonPath = resolveWorkspaceVariables( - debugConfiguration.pythonPath ? debugConfiguration.pythonPath : undefined, + debugConfiguration.pythonPath, workspaceFolder?.fsPath, undefined, ); } + // STEP 2: Resolve current python property (CURRENT STANDARD) if (debugConfiguration.python === '${command:python.interpreterPath}') { + // if python is set to the command, resolve it this.pythonPathSource = PythonPathSource.settingsJson; - const interpreterDetail = await getInterpreterDetails(workspaceFolder); - const interpreterPath = interpreterDetail.path - ? interpreterDetail.path - : await getSettingsPythonPath(workspaceFolder); - debugConfiguration.python = interpreterPath ? interpreterPath[0] : interpreterPath; - } else if (debugConfiguration.python === undefined) { - this.pythonPathSource = PythonPathSource.settingsJson; + debugConfiguration.python = resolvedInterpreterPath; + } else if (!debugConfiguration.python) { + // fallback to pythonPath if python undefined debugConfiguration.python = debugConfiguration.pythonPath; } else { + // User provided explicit python path in launch.json this.pythonPathSource = PythonPathSource.launchJson; debugConfiguration.python = resolveWorkspaceVariables( - debugConfiguration.python ?? debugConfiguration.pythonPath, + debugConfiguration.python, workspaceFolder?.fsPath, undefined, ); } - if ( + // STEP 3: Set debug adapter and launcher Python paths (backwards compatible) + this.setDebugComponentPythonPaths(debugConfiguration); + + // STEP 4: Clean up - remove the deprecated pythonPath property + delete debugConfiguration.pythonPath; + } + + /** + * Sets debugAdapterPython and debugLauncherPython with backwards compatibility. + * Prefers pythonPath over python for these internal properties. + * + * @param debugConfiguration The debug configuration to update + */ + private setDebugComponentPythonPaths(debugConfiguration: LaunchRequestArguments): void { + const shouldSetDebugAdapter = debugConfiguration.debugAdapterPython === '${command:python.interpreterPath}' || - debugConfiguration.debugAdapterPython === undefined - ) { - debugConfiguration.debugAdapterPython = debugConfiguration.pythonPath ?? debugConfiguration.python; - } - if ( + debugConfiguration.debugAdapterPython === undefined; + + const shouldSetDebugLauncher = debugConfiguration.debugLauncherPython === '${command:python.interpreterPath}' || - debugConfiguration.debugLauncherPython === undefined - ) { - debugConfiguration.debugLauncherPython = debugConfiguration.pythonPath ?? debugConfiguration.python; + debugConfiguration.debugLauncherPython === undefined; + + // Default fallback path (prefer pythonPath for backwards compatibility) + const fallbackPath = debugConfiguration.pythonPath ?? debugConfiguration.python; + + if (debugConfiguration.pythonPath !== debugConfiguration.python) { + sendTelemetryEvent(EventName.DEPRECATED_CODE_PATH_USAGE, undefined, { + codePath: 'different_python_paths_in_debug_config', + }); } - delete debugConfiguration.pythonPath; + if (shouldSetDebugAdapter) { + debugConfiguration.debugAdapterPython = fallbackPath; + } + if (shouldSetDebugLauncher) { + debugConfiguration.debugLauncherPython = fallbackPath; + } } protected static debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions): void { From a1d6e790f4b66187d2b5f4caaf13069c57e43af0 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:52:06 -0700 Subject: [PATCH 6/6] fix item --- src/extension/debugger/configuration/resolvers/base.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension/debugger/configuration/resolvers/base.ts b/src/extension/debugger/configuration/resolvers/base.ts index ed3bff04..8b887107 100644 --- a/src/extension/debugger/configuration/resolvers/base.ts +++ b/src/extension/debugger/configuration/resolvers/base.ts @@ -174,6 +174,7 @@ export abstract class BaseConfigurationResolver debugConfiguration.python = resolvedInterpreterPath; } else if (!debugConfiguration.python) { // fallback to pythonPath if python undefined + this.pythonPathSource = PythonPathSource.settingsJson; debugConfiguration.python = debugConfiguration.pythonPath; } else { // User provided explicit python path in launch.json