Skip to content

Commit aa8334c

Browse files
committed
feat: Delete python virtual environment directory when removing deepnote environment
Signed-off-by: Tomas Kislan <tomas@kislan.sk>
1 parent dee6066 commit aa8334c

File tree

2 files changed

+69
-3
lines changed

2 files changed

+69
-3
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { generateUuid as uuid } from '../../../platform/common/uuid';
44
import { IExtensionContext, IOutputChannel } from '../../../platform/common/types';
55
import { IExtensionSyncActivationService } from '../../../platform/activation/types';
66
import { logger } from '../../../platform/logging';
7+
import { IFileSystem } from '../../../platform/common/platform/types';
78
import { DeepnoteEnvironmentStorage } from './deepnoteEnvironmentStorage.node';
89
import {
910
CreateDeepnoteEnvironmentOptions,
@@ -31,7 +32,8 @@ export class DeepnoteEnvironmentManager implements IExtensionSyncActivationServi
3132
@inject(DeepnoteEnvironmentStorage) private readonly storage: DeepnoteEnvironmentStorage,
3233
@inject(IDeepnoteToolkitInstaller) private readonly toolkitInstaller: IDeepnoteToolkitInstaller,
3334
@inject(IDeepnoteServerStarter) private readonly serverStarter: IDeepnoteServerStarter,
34-
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel
35+
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel,
36+
@inject(IFileSystem) private readonly fileSystem: IFileSystem
3537
) {}
3638

3739
/**
@@ -191,6 +193,17 @@ export class DeepnoteEnvironmentManager implements IExtensionSyncActivationServi
191193

192194
Cancellation.throwIfCanceled(token);
193195

196+
// Delete the virtual environment directory from disk
197+
try {
198+
await this.fileSystem.delete(config.venvPath);
199+
logger.info(`Deleted virtual environment directory: ${config.venvPath.fsPath}`);
200+
} catch (error) {
201+
// Log but don't fail - the directory might not exist or might already be deleted
202+
logger.warn(`Failed to delete virtual environment directory: ${config.venvPath.fsPath}`, error);
203+
}
204+
205+
Cancellation.throwIfCanceled(token);
206+
194207
this.environments.delete(id);
195208
await this.persistEnvironments();
196209
this._onDidChangeEnvironments.fire();

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

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import { assert, use } from 'chai';
22
import chaiAsPromised from 'chai-as-promised';
33
import { anything, instance, mock, when, verify, deepEqual } 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';
10+
import { IFileSystem } from '../../../platform/common/platform/types';
811
import {
912
IDeepnoteServerStarter,
1013
IDeepnoteToolkitInstaller,
@@ -24,6 +27,8 @@ suite('DeepnoteEnvironmentManager', () => {
2427
let mockToolkitInstaller: IDeepnoteToolkitInstaller;
2528
let mockServerStarter: IDeepnoteServerStarter;
2629
let mockOutputChannel: IOutputChannel;
30+
let mockFileSystem: IFileSystem;
31+
let testGlobalStoragePath: string;
2732

2833
const testInterpreter: PythonEnvironment = {
2934
id: 'test-python-id',
@@ -49,19 +54,40 @@ suite('DeepnoteEnvironmentManager', () => {
4954
mockToolkitInstaller = mock<IDeepnoteToolkitInstaller>();
5055
mockServerStarter = mock<IDeepnoteServerStarter>();
5156
mockOutputChannel = mock<IOutputChannel>();
57+
mockFileSystem = mock<IFileSystem>();
5258

53-
when(mockContext.globalStorageUri).thenReturn(Uri.file('/global/storage'));
59+
// Create a temporary directory for test storage
60+
testGlobalStoragePath = fs.mkdtempSync(`${os.tmpdir()}/deepnote-test-`);
61+
62+
when(mockContext.globalStorageUri).thenReturn(Uri.file(testGlobalStoragePath));
5463
when(mockStorage.loadEnvironments()).thenResolve([]);
5564

65+
// Configure mockFileSystem to actually delete directories for testing
66+
when(mockFileSystem.delete(anything())).thenCall((uri: Uri) => {
67+
const dirPath = uri.fsPath;
68+
if (fs.existsSync(dirPath)) {
69+
fs.rmSync(dirPath, { recursive: true, force: true });
70+
}
71+
return Promise.resolve();
72+
});
73+
5674
manager = new DeepnoteEnvironmentManager(
5775
instance(mockContext),
5876
instance(mockStorage),
5977
instance(mockToolkitInstaller),
6078
instance(mockServerStarter),
61-
instance(mockOutputChannel)
79+
instance(mockOutputChannel),
80+
instance(mockFileSystem)
6281
);
6382
});
6483

84+
teardown(() => {
85+
// Clean up the temporary directory after each test
86+
if (testGlobalStoragePath && fs.existsSync(testGlobalStoragePath)) {
87+
fs.rmSync(testGlobalStoragePath, { recursive: true, force: true });
88+
}
89+
});
90+
6591
suite('activate', () => {
6692
test('should load environments on activation', async () => {
6793
const existingConfigs = [
@@ -312,6 +338,33 @@ suite('DeepnoteEnvironmentManager', () => {
312338
test('should throw error for non-existent environment', async () => {
313339
await assert.isRejected(manager.deleteEnvironment('non-existent'), 'Environment not found: non-existent');
314340
});
341+
342+
test('should delete virtual environment directory from disk', async () => {
343+
when(mockStorage.saveEnvironments(anything())).thenResolve();
344+
345+
const config = await manager.createEnvironment({
346+
name: 'Test',
347+
pythonInterpreter: testInterpreter
348+
});
349+
350+
// Create the virtual environment directory to simulate it existing
351+
const venvDirPath = config.venvPath.fsPath;
352+
fs.mkdirSync(venvDirPath, { recursive: true });
353+
354+
// Create a dummy file inside to make it a "real" directory
355+
fs.writeFileSync(`${venvDirPath}/test.txt`, 'test content');
356+
357+
// Verify directory and file exist before deletion
358+
assert.isTrue(fs.existsSync(venvDirPath), 'Directory should exist before deletion');
359+
assert.isTrue(fs.existsSync(`${venvDirPath}/test.txt`), 'File should exist before deletion');
360+
361+
// Delete the environment
362+
await manager.deleteEnvironment(config.id);
363+
364+
// Verify directory no longer exists (with a small delay to allow async operation)
365+
await new Promise((resolve) => setTimeout(resolve, 100));
366+
assert.isFalse(fs.existsSync(venvDirPath), 'Directory should not exist after deletion');
367+
});
315368
});
316369

317370
suite('startServer', () => {

0 commit comments

Comments
 (0)