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 ae63c4b9..14765f6d 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; @@ -311,3 +316,397 @@ suite('Debugging - Config Resolver', () => { expect(isFlask).to.equal(false, 'flask'); }); }); + +// 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'); + } + + public resolveDebugConfigurationWithSubstitutedVariables( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { + throw new Error('Not Implemented'); + } + + public resolveAndUpdatePythonPath( + workspaceFolderUri: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ) { + return super.resolveAndUpdatePythonPath(workspaceFolderUri, debugConfiguration); + } + + public getPythonPathSource() { + return this.pythonPathSource; + } + } + + let resolver: BaseResolver2; + let getInterpreterDetailsStub: sinon.SinonStub; + + setup(() => { + resolver = new BaseResolver2(); + getInterpreterDetailsStub = sinon.stub(pythonApi, 'getInterpreterDetails'); + }); + + teardown(() => { + sinon.restore(); + }); + + 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 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, + }; + + 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', pythonPathValue); + }); + + 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 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, + }; + + 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', pythonPathValue); + }); + + 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 resolved pythonPath from interpreter', async () => { + const pythonValue = path.join('path', 'to', 'python', 'python'); + const interpreterPath = path.join('path', 'from', 'interpreter'); + const config = { + 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); + expect(config).to.have.property('debugAdapterPython', interpreterPath); + expect(config).to.have.property('debugLauncherPython', interpreterPath); + }); + + 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); + }); +});