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
126 changes: 126 additions & 0 deletions packages/adapter-python/tests/unit/python-utils.comprehensive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,3 +1015,129 @@ describe('Verbose discovery logging', () => {
}
});
});

describe('WhichCommandFinder internals (coverage sprint)', () => {
beforeEach(() => {
// CI runners (setup-python) export pythonLocation/PYTHON_PATH, which
// short-circuit discovery before WhichCommandFinder is ever consulted —
// neutralize them so these tests exercise the finder on every machine.
vi.stubEnv('pythonLocation', undefined);
vi.stubEnv('PythonLocation', undefined);
vi.stubEnv('PYTHON_PATH', undefined);
vi.stubEnv('PYTHON_EXECUTABLE', undefined);
spawnMock.mockReset();
whichMock.mockReset();
});

afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

const validPythonSpawn = () =>
spawnMock.mockImplementation(() => createSpawn({ exitCode: 0, stdout: 'Python 3.12.0\n' }));

it('installs a ComSpec fallback on Windows when neither ComSpec nor COMSPEC is set', async () => {
vi.stubEnv('ComSpec', '');
vi.stubEnv('COMSPEC', '');
vi.stubEnv('SystemRoot', 'C:\\WinTest');
whichMock.mockResolvedValue(['C:\\py\\python.exe']);
validPythonSpawn();

await expect(findPythonExecutable(undefined, undefined, undefined, 'win32'))
.resolves.toBe('C:\\py\\python.exe');

expect(process.env.ComSpec).toBe(path.join('C:\\WinTest', 'System32', 'cmd.exe'));
expect(process.env.COMSPEC).toBe(path.join('C:\\WinTest', 'System32', 'cmd.exe'));
});

it('copies Path into PATH on Windows when only Path is defined', async () => {
vi.stubEnv('PATH', '');
vi.stubEnv('Path', 'C:\\one;C:\\two');
whichMock.mockResolvedValue(['C:\\py\\python.exe']);
validPythonSpawn();

await findPythonExecutable(undefined, undefined, undefined, 'win32');

expect(process.env.PATH).toBe('C:\\one;C:\\two');
});

it('throws CommandNotFoundError when every candidate is a Windows Store alias', async () => {
whichMock.mockResolvedValue([
'C:\\Users\\test\\AppData\\Local\\Microsoft\\WindowsApps\\python.exe'
]);
validPythonSpawn();

// Each candidate ends in CommandNotFoundError, so discovery reports the
// aggregate not-found error rather than any resolved shim path
await expect(findPythonExecutable(undefined, undefined, undefined, 'win32'))
.rejects.toThrow(/Python not found/);
});

it('treats a python that fails to spawn --version as invalid (validator error arm)', async () => {
whichMock.mockResolvedValue(['C:\\broken\\python.exe']);
spawnMock.mockImplementation(() => createSpawn({ exitCode: 1, error: new Error('EACCES') }));

await expect(findPythonExecutable(undefined, undefined, undefined, 'win32'))
.rejects.toThrow();
expect(spawnMock).toHaveBeenCalledWith(
'C:\\broken\\python.exe',
['-c', expect.stringContaining('sys.exit')],
expect.anything()
);
});

it('emits the verbose discovery diagnostics when DEBUG_PYTHON_DISCOVERY=true', async () => {
vi.stubEnv('DEBUG_PYTHON_DISCOVERY', 'true');
vi.stubEnv('PATH', 'C:\\a;;C:\\"quoted" ');
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
whichMock.mockResolvedValue(['C:\\py\\python.exe']);
validPythonSpawn();

await findPythonExecutable(undefined, undefined, undefined, 'win32');

expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Looking for command: py'));
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('PATH issues found'),
expect.arrayContaining([expect.stringContaining('empty entries')])
);
expect(logSpy).toHaveBeenCalledWith('[PYTHON_DISCOVERY_DEBUG]', expect.any(String));
});

it('runs the direct-spawn failure probe when verbose discovery is on and which fails', async () => {
vi.stubEnv('DEBUG_PYTHON_DISCOVERY', 'true');
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(console, 'log').mockImplementation(() => {});
whichMock.mockRejectedValue(new Error('which exploded'));
spawnMock.mockImplementation(() => createSpawn({ exitCode: 0, stdout: 'Python 3.12.0\n' }));

await expect(findPythonExecutable(undefined, undefined, undefined, 'win32'))
.rejects.toThrow();

expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('which failed for'),
expect.objectContaining({ message: 'which exploded' })
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('Direct spawn result: SUCCESS')
);
});

it('falls through to auto-detection when the env-var interpreter is not found', async () => {
vi.stubEnv('PYTHON_PATH', 'ghost-python');
// The auto-detect winner still gets a debugpy probe
spawnMock.mockImplementation(() => createSpawn({ exitCode: 0, stdout: '1.8.0\n' }));
const finder: CommandFinder = {
find: async (cmd: string) => {
if (cmd === 'ghost-python') {
throw new CommandNotFoundError(cmd);
}
return '/usr/bin/python3';
}
};

await expect(findPythonExecutable(undefined, undefined, finder, 'linux'))
.resolves.toBe('/usr/bin/python3');
});
});
172 changes: 172 additions & 0 deletions packages/adapter-rust/tests/rust-debug-adapter.toolchain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,4 +538,176 @@ describe('RustDebugAdapter toolchain logic', () => {
expect(capabilities.supportsSetExpression).toBe(false);
});
});

describe('lifecycle (coverage sprint)', () => {
function healthyToolchain(): void {
vi.mocked(resolveCodeLLDBExecutable).mockResolvedValue('/mock/vendor/codelldb');
vi.mocked(checkRustInstallation).mockResolvedValue(true);
vi.mocked(checkCargoInstallation).mockResolvedValue(true);
vi.mocked(getRustHostTriple).mockResolvedValue('x86_64-unknown-linux-gnu');
}

it('initialize reaches READY and emits initialized on a healthy toolchain', async () => {
healthyToolchain();
const initialized = vi.fn();
adapter.on('initialized', initialized);

await adapter.initialize();

expect(adapter.getState()).toBe(AdapterState.READY);
expect(initialized).toHaveBeenCalled();
});

it('initialize logs validation warnings but still succeeds', async () => {
healthyToolchain();
vi.mocked(checkRustInstallation).mockResolvedValue(false); // warning, not error

await adapter.initialize();

expect(adapter.getState()).toBe(AdapterState.READY);
expect(dependencies.logger?.warn).toHaveBeenCalledWith(
expect.stringContaining('Rust toolchain not found')
);
});

it('initialize throws ENVIRONMENT_INVALID and lands in ERROR when CodeLLDB is missing', async () => {
healthyToolchain();
vi.mocked(resolveCodeLLDBExecutable).mockResolvedValue(null);

await expect(adapter.initialize()).rejects.toThrow(/CodeLLDB executable not found/);
expect(adapter.getState()).toBe(AdapterState.ERROR);
});

it('dispose resets the adapter and emits disposed', async () => {
healthyToolchain();
await adapter.initialize();
const disposed = vi.fn();
adapter.on('disposed', disposed);

await adapter.dispose();

expect(adapter.getState()).toBe(AdapterState.UNINITIALIZED);
expect(adapter.getCurrentThreadId()).toBeNull();
expect(disposed).toHaveBeenCalled();
});

it('collects RUST_NOT_FOUND and CARGO_NOT_FOUND warnings together', async () => {
vi.mocked(resolveCodeLLDBExecutable).mockResolvedValue('/mock/vendor/codelldb');
vi.mocked(checkRustInstallation).mockResolvedValue(false);
vi.mocked(checkCargoInstallation).mockResolvedValue(false);
vi.mocked(getRustHostTriple).mockResolvedValue(null);

const result = await adapter.validateEnvironment();

expect(result.valid).toBe(true);
expect(result.warnings.map((w) => w.code)).toEqual(
expect.arrayContaining(['RUST_NOT_FOUND', 'CARGO_NOT_FOUND'])
);
});

it('wraps unexpected validation failures in VALIDATION_ERROR', async () => {
vi.mocked(resolveCodeLLDBExecutable).mockRejectedValue(new Error('resolver exploded'));

const result = await adapter.validateEnvironment();

expect(result.valid).toBe(false);
expect(result.errors[0]).toMatchObject({
code: 'VALIDATION_ERROR',
message: 'resolver exploded',
recoverable: false
});
});

it('records a discovered dlltool path on win32', async () => {
const win = new RustDebugAdapter(createDependencies(), 'win32');
vi.mocked(resolveCodeLLDBExecutable).mockResolvedValue('/mock/vendor/codelldb');
vi.mocked(checkRustInstallation).mockResolvedValue(true);
vi.mocked(checkCargoInstallation).mockResolvedValue(true);
vi.mocked(getRustHostTriple).mockResolvedValue('x86_64-pc-windows-gnu');
vi.mocked(findDlltoolExecutable).mockResolvedValue('C:/mingw64/bin/dlltool.exe');

await win.validateEnvironment();

expect((win as unknown as { dlltoolPath?: string }).dlltoolPath).toBe('C:/mingw64/bin/dlltool.exe');
});
});

describe('executable resolution edges (coverage sprint)', () => {
it('throws EXECUTABLE_NOT_FOUND when neither cargo nor rustc is available', async () => {
vi.mocked(checkCargoInstallation).mockResolvedValue(false);
vi.mocked(checkRustInstallation).mockResolvedValue(false);

await expect(adapter.resolveExecutablePath()).rejects.toThrow(/Neither cargo nor rustc found/);
});

it('falls back to the prebuilt placeholder under MCP_CONTAINER=true', async () => {
vi.stubEnv('MCP_CONTAINER', 'true');
try {
vi.mocked(checkCargoInstallation).mockResolvedValue(false);
vi.mocked(checkRustInstallation).mockResolvedValue(false);

await expect(adapter.resolveExecutablePath()).resolves.toBe('rust-prebuilt-binary');
expect(dependencies.logger?.warn).toHaveBeenCalledWith(
expect.stringContaining('MCP_CONTAINER')
);
} finally {
vi.stubEnv('MCP_CONTAINER', undefined as unknown as string);
}
});

it('returns darwin-specific search paths for a darwin-constructed adapter', () => {
const mac = new RustDebugAdapter(createDependencies(), 'darwin');
const paths = mac.getExecutableSearchPaths();
expect(paths).toContain('/opt/homebrew/bin');
expect(paths).toContain('/usr/local/bin');
});

it.each(['0', 'false', 'no'])('resolveAutoSuggestGnu treats %j as disabled', (value) => {
vi.stubEnv('RUST_AUTO_SUGGEST_GNU', value);
try {
expect((adapter as unknown as { resolveAutoSuggestGnu(): boolean }).resolveAutoSuggestGnu()).toBe(false);
} finally {
vi.stubEnv('RUST_AUTO_SUGGEST_GNU', undefined as unknown as string);
}
});

it('getDefaultExecutableName is cargo', () => {
expect(adapter.getDefaultExecutableName()).toBe('cargo');
});
});

describe('transformLaunchConfig cargo targets (coverage sprint)', () => {
it('resolves cargo.example into target/<profile>/<example>', async () => {
const linux = new RustDebugAdapter(createDependencies(), 'linux');
const transformed = await linux.transformLaunchConfig({
cargo: { example: 'demo' },
cwd: '/proj'
} as never);

expect(String(transformed.program).replace(/\\/g, '/')).toBe('/proj/target/debug/demo');
});

it('resolves cargo.test into the release dir when release is set', async () => {
const linux = new RustDebugAdapter(createDependencies(), 'linux');
const transformed = await linux.transformLaunchConfig({
cargo: { test: 'integration', release: true },
cwd: '/proj'
} as never);

expect(String(transformed.program).replace(/\\/g, '/')).toBe('/proj/target/release/integration');
});

it('falls back to the default cargo binary when no target is named', async () => {
vi.mocked(getDefaultBinary).mockResolvedValue('main-bin');
const linux = new RustDebugAdapter(createDependencies(), 'linux');

const transformed = await linux.transformLaunchConfig({
cargo: {},
cwd: '/proj'
} as never);

expect(getDefaultBinary).toHaveBeenCalledWith('/proj');
expect(String(transformed.program).replace(/\\/g, '/')).toBe('/proj/target/debug/main-bin');
});
});
});
Loading
Loading