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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"git.branchProtectionPrompt": "alwaysCommitToNewBranch",
"chat.tools.terminal.autoApprove": {
"npx tsc": true,
"mkdir": true
"mkdir": true,
"npx mocha": true
}
}
14 changes: 14 additions & 0 deletions src/common/utils/asyncUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { traceError } from '../logging';

export async function timeout(milliseconds: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
}

/**
* Wraps a promise so that rejection is caught and logged instead of propagated.
* Use with `Promise.all` to run tasks independently — one failure won't block the others.
*/
export async function safeRegister(name: string, task: Promise<void>): Promise<void> {
try {
await task;
} catch (error) {
traceError(`Failed to register ${name} features:`, error);
}
}
23 changes: 17 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { StopWatch } from './common/stopWatch';
import { EventNames } from './common/telemetry/constants';
import { sendManagerSelectionTelemetry, sendProjectStructureTelemetry } from './common/telemetry/helpers';
import { sendTelemetryEvent } from './common/telemetry/sender';
import { safeRegister } from './common/utils/asyncUtils';
import { createDeferred } from './common/utils/deferred';

import {
Expand Down Expand Up @@ -522,13 +523,23 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
context.subscriptions.push(nativeFinder);
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
sysPythonManager.resolve(sysMgr);
// Each manager registers independently — one failure must not block the others.
await Promise.all([
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager),
registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager),
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
shellStartupVarsMgr.initialize(),
safeRegister(
'system',
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
),
safeRegister(
'conda',
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
),
safeRegister('pyenv', registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager)),
safeRegister('pipenv', registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager)),
safeRegister(
'poetry',
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
),
safeRegister('shellStartupVars', shellStartupVarsMgr.initialize()),
]);

await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
Expand Down
69 changes: 69 additions & 0 deletions src/test/common/asyncUtils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from 'assert';
import * as sinon from 'sinon';
import * as logging from '../../common/logging';
import { safeRegister } from '../../common/utils/asyncUtils';

suite('safeRegister', () => {
let traceErrorStub: sinon.SinonStub;

setup(() => {
traceErrorStub = sinon.stub(logging, 'traceError');
});

teardown(() => {
sinon.restore();
});

test('resolves when the task succeeds', async () => {
await safeRegister('test-manager', Promise.resolve());
assert.ok(traceErrorStub.notCalled, 'traceError should not be called on success');
});

test('resolves (not rejects) when the task fails', async () => {
const failing = Promise.reject(new Error('boom'));
// safeRegister must not propagate the rejection
await safeRegister('failing-manager', failing);
// If we got here without throwing, the test passes
});

test('logs the manager name and error when the task fails', async () => {
const error = new Error('registration exploded');
await safeRegister('conda', Promise.reject(error));

assert.ok(traceErrorStub.calledOnce, 'traceError should be called once');
const [message, loggedError] = traceErrorStub.firstCall.args;
assert.ok(message.includes('conda'), 'log message should contain the manager name');
assert.strictEqual(loggedError, error, 'original error should be passed through');
});

test('independent tasks continue when one fails', async () => {
const results: string[] = [];

await Promise.all([
safeRegister('will-fail', Promise.reject(new Error('fail'))),
safeRegister(
'will-succeed-1',
Promise.resolve().then(() => {
results.push('a');
}),
),
safeRegister(
'will-succeed-2',
Promise.resolve().then(() => {
results.push('b');
}),
),
]);

assert.deepStrictEqual(results.sort(), ['a', 'b'], 'both successful tasks should complete');
assert.ok(traceErrorStub.calledOnce, 'only the failing task should log an error');
});

test('handles non-Error rejections', async () => {
await safeRegister('string-reject', Promise.reject('just a string'));

assert.ok(traceErrorStub.calledOnce);
const [, loggedError] = traceErrorStub.firstCall.args;
assert.strictEqual(loggedError, 'just a string');
});
});
Loading