Skip to content
Draft
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
50 changes: 50 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,53 @@ jobs:
- name: Run Integration Tests (non-Linux)
if: runner.os != 'Linux'
run: npm run integration-test

integration-tests-multiroot:
name: Integration Tests (Multi-Root)
runs-on: ${{ matrix.os }}
needs: [smoke-tests]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.12', '3.14']

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'

- name: Install Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Dependencies
run: npm ci

- name: Compile Extension
run: npm run compile

- name: Compile Tests
run: npm run compile-tests

- name: Configure Test Settings
run: |
mkdir -p .vscode-test/user-data/User
echo '{"python.useEnvironmentsExtension": true}' > .vscode-test/user-data/User/settings.json
shell: bash

- name: Run Integration Tests Multi-Root (Linux)
if: runner.os == 'Linux'
uses: GabrielBB/xvfb-action@v1
with:
run: npm run integration-test-multiroot

- name: Run Integration Tests Multi-Root (non-Linux)
if: runner.os != 'Linux'
run: npm run integration-test-multiroot
20 changes: 19 additions & 1 deletion .vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export default defineConfig([
},
{
label: 'integrationTests',
files: 'out/test/integration/**/*.integration.test.js',
files: 'out/test/integration/*.integration.test.js',
workspaceFolder: 'src/test/integration/test-workspace/project-a',
mocha: {
ui: 'tdd',
timeout: 60000,
Expand All @@ -64,6 +65,23 @@ export default defineConfig([
// the native Python tools (pet binary). We use inspect() for
// useEnvironmentsExtension check, so Python extension's default is ignored.
},
{
label: 'integrationTestsMultiRoot',
files: 'out/test/integration/multiroot/*.integration.test.js',
workspaceFolder: 'src/test/integration/test-workspace/integration-tests.code-workspace',
mocha: {
ui: 'tdd',
timeout: 60000,
retries: 1,
},
env: {
VSC_PYTHON_INTEGRATION_TEST: '1',
},
launchArgs: [
`--user-data-dir=${userDataDir}`,
'--disable-workspace-trust',
],
},
{
label: 'extensionTests',
files: 'out/test/**/*.test.js',
Expand Down
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@
"python-envs.pythonProjects": [],
"git.branchRandomName.enable": true,
"git.branchProtection": ["main"],
"git.branchProtectionPrompt": "alwaysCommitToNewBranch"
"git.branchProtectionPrompt": "alwaysCommitToNewBranch",
"chat.tools.terminal.autoApprove": {
"npx tsc": true,
"mkdir": true
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@
"smoke-test": "vscode-test --label smokeTests",
"e2e-test": "vscode-test --label e2eTests --install-extensions ms-python.python",
"integration-test": "vscode-test --label integrationTests --install-extensions ms-python.python",
"integration-test-multiroot": "vscode-test --label integrationTestsMultiRoot --install-extensions ms-python.python",
"vsce-package": "vsce package -o ms-python-envs-insiders.vsix"
},
"devDependencies": {
Expand Down
257 changes: 257 additions & 0 deletions src/test/integration/envCreation.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

/**
* Integration Test: Environment Creation
*
* PURPOSE:
* Verify that environment creation works correctly through the API,
* respecting configured managers and options.
*
* WHAT THIS TESTS:
* 1. createEnvironment API is available and callable
* 2. Creation respects defaultEnvManager setting
* 3. Created environments appear in discovery
* 4. Environment removal works correctly
*
* NOTE: These tests may create actual virtual environments on disk.
* Tests that create environments should clean up after themselves.
*/

import * as assert from 'assert';
import * as vscode from 'vscode';
import { PythonEnvironment, PythonEnvironmentApi } from '../../api';
import { ENVS_EXTENSION_ID } from '../constants';
import { sleep, waitForCondition } from '../testUtils';

suite('Integration: Environment Creation', function () {
this.timeout(120_000); // Environment creation can be slow

let api: PythonEnvironmentApi;

suiteSetup(async function () {
this.timeout(30_000);

const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID);
assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`);

if (!extension.isActive) {
await extension.activate();
await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate');
}

api = extension.exports as PythonEnvironmentApi;
assert.ok(api, 'API not available');
assert.ok(typeof api.createEnvironment === 'function', 'createEnvironment should be a function');
assert.ok(typeof api.removeEnvironment === 'function', 'removeEnvironment should be a function');
});

// =========================================================================
// ENVIRONMENT CREATION BEHAVIOR TESTS
// These tests verify actual user-facing creation and removal workflows.
// =========================================================================

/**
* Test: Created environment appears in discovery
*
* BEHAVIOR TESTED: User creates an environment via quickCreate,
* then the environment should be discoverable via getEnvironments.
*/
test('Created environment appears in discovery', async function () {
// --- SETUP: Ensure we have prerequisites ---
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
this.skip();
return;
}

const globalEnvs = await api.getEnvironments('global');
if (globalEnvs.length === 0) {
console.log('No global Python installations found, skipping creation test');
this.skip();
return;
}

const workspaceUri = workspaceFolders[0].uri;
let createdEnv: PythonEnvironment | undefined;

try {
// --- ACTION: User creates environment ---
createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true });

if (!createdEnv) {
console.log('Environment creation returned undefined (may require user input)');
this.skip();
return;
}

// --- VERIFY: Created environment is discoverable ---
await api.refreshEnvironments(workspaceUri);
const environments = await api.getEnvironments(workspaceUri);

const found = environments.some(
(env) =>
env.envId.id === createdEnv!.envId.id ||
env.environmentPath.fsPath === createdEnv!.environmentPath.fsPath,
);

assert.ok(found, 'Created environment should appear in discovery');
} finally {
// Cleanup: Remove the created environment
if (createdEnv) {
try {
await api.removeEnvironment(createdEnv);
} catch (e) {
console.log('Cleanup failed (may already be removed):', e);
}
}
}
});

/**
* Test: Environment removal removes from discovery
*
* BEHAVIOR TESTED: User removes an environment, then it should
* no longer appear in discovery results.
*/
test('Removed environment disappears from discovery', async function () {
// --- SETUP: Create an environment to remove ---
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
this.skip();
return;
}

const globalEnvs = await api.getEnvironments('global');
if (globalEnvs.length === 0) {
this.skip();
return;
}

const workspaceUri = workspaceFolders[0].uri;
let createdEnv: PythonEnvironment | undefined;

try {
createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true });
if (!createdEnv) {
this.skip();
return;
}

const envId = createdEnv.envId.id;

// --- ACTION: User removes environment ---
await api.removeEnvironment(createdEnv);
createdEnv = undefined;

await sleep(1000);

// --- VERIFY: Environment is no longer discoverable ---
await api.refreshEnvironments(workspaceUri);
const environments = await api.getEnvironments(workspaceUri);

const stillExists = environments.some((env) => env.envId.id === envId);

assert.ok(!stillExists, 'Removed environment should not appear in discovery');
} finally {
// Cleanup in case removal failed
if (createdEnv) {
try {
await api.removeEnvironment(createdEnv);
} catch {
// Ignore cleanup errors
}
}
}
});

/**
* Test: Creating environment for 'global' scope
*
* The 'global' scope should allow creating environments not tied to a workspace.
* Note: This may require special permissions or configurations.
*/
test('Global scope creation is handled', async function () {
// This test verifies the API handles global scope correctly
let createdEnv: PythonEnvironment | undefined;

try {
// Attempt global creation - this may prompt for user input
// so we use quickCreate and expect it might return undefined
createdEnv = await api.createEnvironment('global', { quickCreate: true });

if (createdEnv) {
// If creation succeeded, verify the environment has valid structure
assert.ok(createdEnv.envId, 'Created global env must have envId');
assert.ok(createdEnv.envId.id, 'Created global env must have envId.id');
assert.ok(createdEnv.environmentPath, 'Created global env must have environmentPath');
} else {
// quickCreate returned undefined - skip this test as feature not available
console.log('Global creation not supported with quickCreate, skipping');
this.skip();
return;
}
} finally {
// Cleanup: try to remove if created, but handle dialog errors in test mode
if (createdEnv) {
try {
await api.removeEnvironment(createdEnv);
} catch (e) {
// Ignore dialog errors in test mode - VS Code blocks dialogs
if (!String(e).includes('DialogService')) {
throw e;
}
console.log('Skipping cleanup for global environment (dialog blocked in tests)');
}
}
}
});

/**
* Test: Creation returns properly structured environment
*
* A successfully created environment should have all required fields.
*/
test('Created environment has proper structure', async function () {
const workspaceFolders = vscode.workspace.workspaceFolders;

if (!workspaceFolders || workspaceFolders.length === 0) {
this.skip();
return;
}

const globalEnvs = await api.getEnvironments('global');
if (globalEnvs.length === 0) {
this.skip();
return;
}

const workspaceUri = workspaceFolders[0].uri;
let createdEnv: PythonEnvironment | undefined;

try {
createdEnv = await api.createEnvironment(workspaceUri, { quickCreate: true });

if (!createdEnv) {
this.skip();
return;
}

// Verify structure
assert.ok(createdEnv.envId, 'Created env must have envId');
assert.ok(createdEnv.envId.id, 'envId must have id');
assert.ok(createdEnv.envId.managerId, 'envId must have managerId');
assert.ok(createdEnv.name, 'Created env must have name');
assert.ok(createdEnv.displayName, 'Created env must have displayName');
assert.ok(createdEnv.environmentPath, 'Created env must have environmentPath');
} finally {
if (createdEnv) {
try {
await api.removeEnvironment(createdEnv);
} catch {
// Ignore cleanup errors
}
}
}
});
});
Loading
Loading