Skip to content

Commit 96a1935

Browse files
authored
feat: add tests and helper for using run.executable for interpreter not activatedRun (#955)
1 parent 9fc2797 commit 96a1935

File tree

4 files changed

+110
-1
lines changed

4 files changed

+110
-1
lines changed

.github/copilot-instructions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copilot Instructions for vscode-python-debugger
2+
3+
## Learnings
4+
5+
- 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)

src/test/unittest/adapter/factory.unit.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import * as telemetryReporter from '../../../extension/telemetry/reporter';
3232
import * as vscodeApi from '../../../extension/common/vscodeapi';
3333
import { DebugConfigStrings } from '../../../extension/common/utils/localize';
3434
import { PythonEnvironment } from '../../../extension/envExtApi';
35+
import { buildPythonEnvironmentWithActivatedRun } from '../common/helpers';
3536

3637
use(chaiAsPromised);
3738

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

341342
assert.deepStrictEqual(descriptor, debugExecutable);
342343
});
344+
345+
test('Use run.executable rather than activatedRun.executable for interpreter identification', async () => {
346+
// Simulates environment managers like pixi/conda that set activatedRun to a wrapper
347+
// command (e.g. "pixi run python") while run.executable is the actual Python binary.
348+
const actualPythonPath = 'path/to/actual/python3';
349+
const wrapperCommand = 'pixi';
350+
const interpreterWithWrapper = buildPythonEnvironmentWithActivatedRun(
351+
actualPythonPath,
352+
wrapperCommand,
353+
'3.10.0',
354+
['run', 'python'],
355+
);
356+
const session = createSession({});
357+
// The debug adapter should use the actual Python binary, not the wrapper
358+
const debugExecutable = new DebugAdapterExecutable(interpreterWithWrapper.execInfo.run.executable, [
359+
debugAdapterPath,
360+
]);
361+
getInterpreterDetailsStub.resolves({ path: [interpreterWithWrapper.execInfo.run.executable] });
362+
resolveEnvironmentStub.resolves(interpreterWithWrapper);
363+
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
364+
365+
assert.deepStrictEqual(descriptor, debugExecutable);
366+
});
343367
});

src/test/unittest/common/helpers.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,45 @@ export function buildPythonEnvironment(execPath: string, version: string, sysPre
2828
sysPrefix,
2929
} as PythonEnvironment;
3030
}
31+
32+
/**
33+
* Helper to build a PythonEnvironment where activatedRun differs from run.
34+
* This simulates environment managers like pixi or conda that set activatedRun
35+
* to a wrapper command (e.g. 'pixi run python') while run.executable points to
36+
* the actual Python binary.
37+
*
38+
* @param execPath string - path to the actual python executable (run.executable)
39+
* @param activatedRunExecutable string - path/command for the wrapper (activatedRun.executable)
40+
* @param version string - python version string (e.g. '3.9.0')
41+
* @param activatedRunArgs string[] - optional args for activatedRun
42+
*/
43+
export function buildPythonEnvironmentWithActivatedRun(
44+
execPath: string,
45+
activatedRunExecutable: string,
46+
version: string,
47+
activatedRunArgs: string[] = [],
48+
): PythonEnvironment {
49+
const execUri = Uri.file(execPath);
50+
return {
51+
envId: {
52+
id: execUri.fsPath,
53+
managerId: 'Venv',
54+
},
55+
name: `Python ${version}`,
56+
displayName: `Python ${version}`,
57+
displayPath: execUri.fsPath,
58+
version: version,
59+
environmentPath: execUri,
60+
execInfo: {
61+
run: {
62+
executable: execUri.fsPath,
63+
args: [],
64+
},
65+
activatedRun: {
66+
executable: activatedRunExecutable,
67+
args: activatedRunArgs,
68+
},
69+
},
70+
sysPrefix: '',
71+
} as PythonEnvironment;
72+
}

src/test/unittest/common/pythonTrue.unit.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Uri, Disposable, Extension, extensions } from 'vscode';
99
import * as path from 'path';
1010
import * as pythonApi from '../../../extension/common/python';
1111
import * as utilities from '../../../extension/common/utilities';
12-
import { buildPythonEnvironment } from './helpers';
12+
import { buildPythonEnvironment, buildPythonEnvironmentWithActivatedRun } from './helpers';
1313

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

167167
expect(result).to.be.undefined;
168168
});
169+
170+
test('Should use run.executable instead of activatedRun.executable when they differ', async () => {
171+
// Simulates environment managers like pixi/conda that set activatedRun to a wrapper command
172+
const actualPythonPath = PYTHON_PATH;
173+
const wrapperCommand = path.join('/', 'usr', 'local', 'bin', 'pixi');
174+
const mockPythonEnv = buildPythonEnvironmentWithActivatedRun(actualPythonPath, wrapperCommand, '3.9.0', [
175+
'run',
176+
'python',
177+
]);
178+
(mockEnvsExtension as any).exports = mockPythonEnvApi;
179+
mockPythonEnvApi.getEnvironment.resolves(mockPythonEnv);
180+
mockPythonEnvApi.resolveEnvironment.resolves(mockPythonEnv);
181+
182+
const result = await pythonApi.getSettingsPythonPath();
183+
184+
// Should return the actual Python binary path, not the wrapper command
185+
expect(result).to.deep.equal([actualPythonPath]);
186+
});
169187
});
170188

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

375393
expect(result.path).to.be.undefined;
376394
});
395+
396+
test('Should use run.executable instead of activatedRun.executable when they differ', async () => {
397+
// Simulates environment managers like pixi/conda that set activatedRun to a wrapper command
398+
const actualPythonPath = PYTHON_PATH;
399+
const wrapperCommand = path.join('/', 'usr', 'local', 'bin', 'pixi');
400+
const mockEnv = buildPythonEnvironmentWithActivatedRun(actualPythonPath, wrapperCommand, '3.9.0', [
401+
'run',
402+
'python',
403+
]);
404+
405+
(mockEnvsExtension as any).exports = mockPythonEnvApi;
406+
mockPythonEnvApi.getEnvironment.returns({ environmentPath: Uri.file(actualPythonPath) });
407+
mockPythonEnvApi.resolveEnvironment.resolves(mockEnv);
408+
409+
const result = await pythonApi.getInterpreterDetails();
410+
411+
// Should return the actual Python binary path, not the wrapper command
412+
expect(result.path).to.deep.equal([actualPythonPath]);
413+
expect(result.resource).to.be.undefined;
414+
});
377415
});
378416

379417
suite('onDidChangePythonInterpreter event', () => {

0 commit comments

Comments
 (0)