Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copilot Instructions for vscode-python-debugger

## Learnings

- Always use `run.executable` (the actual Python binary path) instead of `activatedRun.executable` for interpreter identification in `getInterpreterDetails`, `getSettingsPythonPath`, and `getExecutableCommand`. `activatedRun.executable` may be a wrapper command (e.g. `pixi run python`) set by environment managers like pixi or conda, which breaks the debugger if used as a replacement for the binary. (1)
24 changes: 24 additions & 0 deletions src/test/unittest/adapter/factory.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import * as telemetryReporter from '../../../extension/telemetry/reporter';
import * as vscodeApi from '../../../extension/common/vscodeapi';
import { DebugConfigStrings } from '../../../extension/common/utils/localize';
import { PythonEnvironment } from '../../../extension/envExtApi';
import { buildPythonEnvironmentWithActivatedRun } from '../common/helpers';

use(chaiAsPromised);

Expand Down Expand Up @@ -340,4 +341,27 @@ suite('Debugging - Adapter Factory', () => {

assert.deepStrictEqual(descriptor, debugExecutable);
});

test('Use run.executable rather than activatedRun.executable for interpreter identification', async () => {
// Simulates environment managers like pixi/conda that set activatedRun to a wrapper
// command (e.g. "pixi run python") while run.executable is the actual Python binary.
const actualPythonPath = 'path/to/actual/python3';
const wrapperCommand = 'pixi';
const interpreterWithWrapper = buildPythonEnvironmentWithActivatedRun(
actualPythonPath,
wrapperCommand,
'3.10.0',
['run', 'python'],
);
const session = createSession({});
// The debug adapter should use the actual Python binary, not the wrapper
const debugExecutable = new DebugAdapterExecutable(interpreterWithWrapper.execInfo.run.executable, [
debugAdapterPath,
]);
getInterpreterDetailsStub.resolves({ path: [interpreterWithWrapper.execInfo.run.executable] });
resolveEnvironmentStub.resolves(interpreterWithWrapper);
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);

assert.deepStrictEqual(descriptor, debugExecutable);
});
});
42 changes: 42 additions & 0 deletions src/test/unittest/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,45 @@ export function buildPythonEnvironment(execPath: string, version: string, sysPre
sysPrefix,
} as PythonEnvironment;
}

/**
* Helper to build a PythonEnvironment where activatedRun differs from run.
* This simulates environment managers like pixi or conda that set activatedRun
* to a wrapper command (e.g. 'pixi run python') while run.executable points to
* the actual Python binary.
*
* @param execPath string - path to the actual python executable (run.executable)
* @param activatedRunExecutable string - path/command for the wrapper (activatedRun.executable)
* @param version string - python version string (e.g. '3.9.0')
* @param activatedRunArgs string[] - optional args for activatedRun
*/
export function buildPythonEnvironmentWithActivatedRun(
execPath: string,
activatedRunExecutable: string,
version: string,
activatedRunArgs: string[] = [],
): PythonEnvironment {
const execUri = Uri.file(execPath);
return {
envId: {
id: execUri.fsPath,
managerId: 'Venv',
},
name: `Python ${version}`,
displayName: `Python ${version}`,
displayPath: execUri.fsPath,
version: version,
environmentPath: execUri,
execInfo: {
run: {
executable: execUri.fsPath,
args: [],
},
activatedRun: {
executable: activatedRunExecutable,
args: activatedRunArgs,
},
},
sysPrefix: '',
} as PythonEnvironment;
}
40 changes: 39 additions & 1 deletion src/test/unittest/common/pythonTrue.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Uri, Disposable, Extension, extensions } from 'vscode';
import * as path from 'path';
import * as pythonApi from '../../../extension/common/python';
import * as utilities from '../../../extension/common/utilities';
import { buildPythonEnvironment } from './helpers';
import { buildPythonEnvironment, buildPythonEnvironmentWithActivatedRun } from './helpers';

// Platform-specific path constants using path.join so tests assert using native separators.
// Leading root '/' preserved; on Windows this yields a leading backslash (e.g. '\\usr\\bin').
Expand Down Expand Up @@ -166,6 +166,24 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => {

expect(result).to.be.undefined;
});

test('Should use run.executable instead of activatedRun.executable when they differ', async () => {
// Simulates environment managers like pixi/conda that set activatedRun to a wrapper command
const actualPythonPath = PYTHON_PATH;
const wrapperCommand = path.join('/', 'usr', 'local', 'bin', 'pixi');
const mockPythonEnv = buildPythonEnvironmentWithActivatedRun(actualPythonPath, wrapperCommand, '3.9.0', [
'run',
'python',
]);
(mockEnvsExtension as any).exports = mockPythonEnvApi;
mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv);
mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv);

const result = await pythonApi.getSettingsPythonPath();

// Should return the actual Python binary path, not the wrapper command
expect(result).to.deep.equal([actualPythonPath]);
});
});

suite('getEnvironmentVariables', () => {
Expand Down Expand Up @@ -374,6 +392,26 @@ suite('Python API Tests- useEnvironmentsExtension:true', () => {

expect(result.path).to.be.undefined;
});

test('Should use run.executable instead of activatedRun.executable when they differ', async () => {
// Simulates environment managers like pixi/conda that set activatedRun to a wrapper command
const actualPythonPath = PYTHON_PATH;
const wrapperCommand = path.join('/', 'usr', 'local', 'bin', 'pixi');
const mockEnv = buildPythonEnvironmentWithActivatedRun(actualPythonPath, wrapperCommand, '3.9.0', [
'run',
'python',
]);

(mockEnvsExtension as any).exports = mockPythonEnvApi;
mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(actualPythonPath) });
mockPythonEnvApi.resolveEnvironment.resolves(mockEnv);

const result = await pythonApi.getInterpreterDetails();

// Should return the actual Python binary path, not the wrapper command
expect(result.path).to.deep.equal([actualPythonPath]);
expect(result.resource).to.be.undefined;
});
});

suite('onDidChangePythonInterpreter event', () => {
Expand Down
Loading