Skip to content

Commit 08a78a4

Browse files
authored
feat: Delete python virtual environment directory when removing deepnote environment (#150)
* feat: Delete python virtual environment directory when removing deepnote environment Signed-off-by: Tomas Kislan <tomas@kislan.sk> * Enhance error logging for virtual environment deletion in DeepnoteEnvironmentManager. Added detailed message output when deletion fails, improving debugging capabilities. Updated unit test comment for clarity. Signed-off-by: Tomas Kislan <tomas@kislan.sk> --------- Signed-off-by: Tomas Kislan <tomas@kislan.sk>
1 parent 9ee12ab commit 08a78a4

File tree

2 files changed

+72
-3
lines changed

2 files changed

+72
-3
lines changed

src/kernels/deepnote/environments/deepnoteEnvironmentManager.node.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { logger } from '../../../platform/logging';
1010
import { IDeepnoteEnvironmentManager, IDeepnoteServerStarter } from '../types';
1111
import { CreateDeepnoteEnvironmentOptions, DeepnoteEnvironment } from './deepnoteEnvironment';
1212
import { DeepnoteEnvironmentStorage } from './deepnoteEnvironmentStorage.node';
13+
import { IFileSystem } from '../../../platform/common/platform/types';
1314

1415
/**
1516
* Manager for Deepnote kernel environments.
@@ -30,7 +31,8 @@ export class DeepnoteEnvironmentManager implements IExtensionSyncActivationServi
3031
@inject(IExtensionContext) private readonly context: IExtensionContext,
3132
@inject(DeepnoteEnvironmentStorage) private readonly storage: DeepnoteEnvironmentStorage,
3233
@inject(IDeepnoteServerStarter) private readonly serverStarter: IDeepnoteServerStarter,
33-
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel
34+
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel,
35+
@inject(IFileSystem) private readonly fileSystem: IFileSystem
3436
) {}
3537

3638
/**
@@ -199,6 +201,21 @@ export class DeepnoteEnvironmentManager implements IExtensionSyncActivationServi
199201

200202
Cancellation.throwIfCanceled(token);
201203

204+
// Delete the virtual environment directory from disk
205+
try {
206+
await this.fileSystem.delete(config.venvPath);
207+
logger.info(`Deleted virtual environment directory: ${config.venvPath.fsPath}`);
208+
} catch (error) {
209+
// Log but don't fail - the directory might not exist or might already be deleted
210+
logger.warn(`Failed to delete virtual environment directory: ${config.venvPath.fsPath}`, error);
211+
const msg = error instanceof Error ? error.message : String(error);
212+
this.outputChannel.appendLine(
213+
l10n.t('Failed to delete Deepnote virtual environment directory for "{0}": {1}', config.name, msg)
214+
);
215+
}
216+
217+
Cancellation.throwIfCanceled(token);
218+
202219
this.environments.delete(id);
203220
await this.persistEnvironments();
204221
this._onDidChangeEnvironments.fire();

src/kernels/deepnote/environments/deepnoteEnvironmentManager.unit.test.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import { assert, use } from 'chai';
22
import chaiAsPromised from 'chai-as-promised';
33
import { anything, instance, mock, when, verify } from 'ts-mockito';
44
import { Uri } from 'vscode';
5+
import * as fs from 'fs';
6+
import * as os from 'os';
57
import { DeepnoteEnvironmentManager } from './deepnoteEnvironmentManager.node';
68
import { DeepnoteEnvironmentStorage } from './deepnoteEnvironmentStorage.node';
79
import { IExtensionContext, IOutputChannel } from '../../../platform/common/types';
810
import { IDeepnoteServerStarter } from '../types';
911
import { PythonEnvironment } from '../../../platform/pythonEnvironments/info';
12+
import { IFileSystem } from '../../../platform/common/platform/types';
1013

1114
use(chaiAsPromised);
1215

@@ -16,6 +19,8 @@ suite('DeepnoteEnvironmentManager', () => {
1619
let mockStorage: DeepnoteEnvironmentStorage;
1720
let mockServerStarter: IDeepnoteServerStarter;
1821
let mockOutputChannel: IOutputChannel;
22+
let mockFileSystem: IFileSystem;
23+
let testGlobalStoragePath: string;
1924

2025
const testInterpreter: PythonEnvironment = {
2126
id: 'test-python-id',
@@ -28,18 +33,39 @@ suite('DeepnoteEnvironmentManager', () => {
2833
mockStorage = mock<DeepnoteEnvironmentStorage>();
2934
mockServerStarter = mock<IDeepnoteServerStarter>();
3035
mockOutputChannel = mock<IOutputChannel>();
36+
mockFileSystem = mock<IFileSystem>();
3137

32-
when(mockContext.globalStorageUri).thenReturn(Uri.file('/global/storage'));
38+
// Create a temporary directory for test storage
39+
testGlobalStoragePath = fs.mkdtempSync(`${os.tmpdir()}/deepnote-test-`);
40+
41+
when(mockContext.globalStorageUri).thenReturn(Uri.file(testGlobalStoragePath));
3342
when(mockStorage.loadEnvironments()).thenResolve([]);
3443

44+
// Configure mockFileSystem to actually delete directories for testing
45+
when(mockFileSystem.delete(anything())).thenCall((uri: Uri) => {
46+
const dirPath = uri.fsPath;
47+
if (fs.existsSync(dirPath)) {
48+
fs.rmSync(dirPath, { recursive: true, force: true });
49+
}
50+
return Promise.resolve();
51+
});
52+
3553
manager = new DeepnoteEnvironmentManager(
3654
instance(mockContext),
3755
instance(mockStorage),
3856
instance(mockServerStarter),
39-
instance(mockOutputChannel)
57+
instance(mockOutputChannel),
58+
instance(mockFileSystem)
4059
);
4160
});
4261

62+
teardown(() => {
63+
// Clean up the temporary directory after each test
64+
if (testGlobalStoragePath && fs.existsSync(testGlobalStoragePath)) {
65+
fs.rmSync(testGlobalStoragePath, { recursive: true, force: true });
66+
}
67+
});
68+
4369
suite('activate', () => {
4470
test('should load environments on activation', async () => {
4571
const existingConfigs = [
@@ -235,6 +261,32 @@ suite('DeepnoteEnvironmentManager', () => {
235261
test('should throw error for non-existent environment', async () => {
236262
await assert.isRejected(manager.deleteEnvironment('non-existent'), 'Environment not found: non-existent');
237263
});
264+
265+
test('should delete virtual environment directory from disk', async () => {
266+
when(mockStorage.saveEnvironments(anything())).thenResolve();
267+
268+
const config = await manager.createEnvironment({
269+
name: 'Test',
270+
pythonInterpreter: testInterpreter
271+
});
272+
273+
// Create the virtual environment directory to simulate it existing
274+
const venvDirPath = config.venvPath.fsPath;
275+
fs.mkdirSync(venvDirPath, { recursive: true });
276+
277+
// Create a dummy file inside to make it a "real" directory
278+
fs.writeFileSync(`${venvDirPath}/test.txt`, 'test content');
279+
280+
// Verify directory and file exist before deletion
281+
assert.isTrue(fs.existsSync(venvDirPath), 'Directory should exist before deletion');
282+
assert.isTrue(fs.existsSync(`${venvDirPath}/test.txt`), 'File should exist before deletion');
283+
284+
// Delete the environment
285+
await manager.deleteEnvironment(config.id);
286+
287+
// Verify directory no longer exists
288+
assert.isFalse(fs.existsSync(venvDirPath), 'Directory should not exist after deletion');
289+
});
238290
});
239291

240292
suite('updateLastUsed', () => {

0 commit comments

Comments
 (0)