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
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@
"title": "%python-envs.terminal.revertStartupScriptChanges.title%",
"category": "Python Envs",
"icon": "$(discard)"
},
{
"command": "python-envs.reportIssue",
"title": "%python-envs.reportIssue.title%",
"category": "Python Environments"
}
],
"menus": {
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"python-envs.terminal.autoActivationType.shellStartup": "Activation by modifying the terminal shell startup script. To use this feature we will need to modify your shell startup scripts.",
"python-envs.terminal.autoActivationType.off": "No automatic activation of environments.",
"python-envs.terminal.revertStartupScriptChanges.title": "Revert Shell Startup Script Changes",
"python-envs.reportIssue.title": "Report Issue",
"python-envs.setEnvManager.title": "Set Environment Manager",
"python-envs.setPkgManager.title": "Set Package Manager",
"python-envs.addPythonProject.title": "Add Python Project",
Expand Down
94 changes: 93 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { commands, ExtensionContext, LogOutputChannel, Terminal, Uri, window } from 'vscode';
import { commands, extensions, ExtensionContext, LogOutputChannel, Terminal, Uri, window, workspace } from 'vscode';
import { PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api';
import { ensureCorrectVersion } from './common/extVersion';
import { registerLogger, traceError, traceInfo } from './common/logging';
Expand Down Expand Up @@ -69,6 +69,85 @@ import { registerCondaFeatures } from './managers/conda/main';
import { registerPoetryFeatures } from './managers/poetry/main';
import { registerPyenvFeatures } from './managers/pyenv/main';

/**
* Collects relevant Python environment information for issue reporting
*/
async function collectEnvironmentInfo(
context: ExtensionContext,
envManagers: EnvironmentManagers,
projectManager: PythonProjectManager
): Promise<string> {
const info: string[] = [];

try {
// Extension version
const extensionVersion = context.extension?.packageJSON?.version || 'unknown';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be useful to get the version of the python extension too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Python extension version to the report issue output in commit fd9b8c9. The function now collects both the Python Environments extension version and the Python extension version to help with debugging compatibility issues.

info.push(`Extension Version: ${extensionVersion}`);

// Python extension version
const pythonExtension = extensions.getExtension('ms-python.python');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karthiknadig ideas on other info we need that will be useful? Still need test everything it is getting here but wanted to get your thoughts on if there is any info I can add

const pythonVersion = pythonExtension?.packageJSON?.version || 'not installed';
info.push(`Python Extension Version: ${pythonVersion}`);

// Environment managers
const managers = envManagers.managers;
info.push(`\nRegistered Environment Managers (${managers.length}):`);
managers.forEach(manager => {
info.push(` - ${manager.id} (${manager.displayName})`);
});

// Available environments
const allEnvironments: PythonEnvironment[] = [];
for (const manager of managers) {
try {
const envs = await manager.getEnvironments('all');
allEnvironments.push(...envs);
} catch (err) {
info.push(` Error getting environments from ${manager.id}: ${err}`);
}
}

info.push(`\nTotal Available Environments: ${allEnvironments.length}`);
if (allEnvironments.length > 0) {
info.push('Environment Details:');
allEnvironments.slice(0, 10).forEach((env, index) => {
info.push(` ${index + 1}. ${env.displayName} (${env.version}) - ${env.displayPath}`);
});
if (allEnvironments.length > 10) {
info.push(` ... and ${allEnvironments.length - 10} more environments`);
}
}

// Python projects
const projects = projectManager.getProjects();
info.push(`\nPython Projects (${projects.length}):`);
for (let index = 0; index < projects.length; index++) {
const project = projects[index];
info.push(` ${index + 1}. ${project.uri.fsPath}`);
try {
const env = await envManagers.getEnvironment(project.uri);
if (env) {
info.push(` Environment: ${env.displayName}`);
}
} catch (err) {
info.push(` Error getting environment: ${err}`);
}
}

// Current settings (non-sensitive)
const config = workspace.getConfiguration('python-envs');
info.push('\nExtension Settings:');
info.push(` Default Environment Manager: ${config.get('defaultEnvManager')}`);
info.push(` Default Package Manager: ${config.get('defaultPackageManager')}`);
info.push(` Terminal Auto Activation: ${config.get('terminal.autoActivationType')}`);

} catch (err) {
info.push(`\nError collecting environment information: ${err}`);
}

return info.join('\n');
}

export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
const start = new StopWatch();

Expand Down Expand Up @@ -278,6 +357,19 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
}
},
),
commands.registerCommand('python-envs.reportIssue', async () => {
try {
const issueData = await collectEnvironmentInfo(context, envManagers, projectManager);

await commands.executeCommand('workbench.action.openIssueReporter', {
extensionId: 'ms-python.vscode-python-envs',
issueTitle: '[Python Environments] ',
issueBody: `<!-- Please describe the issue you're experiencing -->\n\n<!-- The following information was automatically generated -->\n\n<details>\n<summary>Environment Information</summary>\n\n\`\`\`\n${issueData}\n\`\`\`\n\n</details>`
});
} catch (error) {
window.showErrorMessage(`Failed to open issue reporter: ${error}`);
}
}),
terminalActivation.onDidChangeTerminalActivationState(async (e) => {
await setActivateMenuButtonContext(e.terminal, e.environment, e.activated);
}),
Expand Down
112 changes: 112 additions & 0 deletions src/test/features/reportIssue.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as assert from 'assert';
import * as typeMoq from 'typemoq';
import * as vscode from 'vscode';
import { PythonEnvironment, PythonEnvironmentId } from '../../api';
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
import { PythonProject } from '../../api';

// We need to mock the extension's activate function to test the collectEnvironmentInfo function
// Since it's a local function, we'll test the command registration instead

suite('Report Issue Command Tests', () => {
let mockEnvManagers: typeMoq.IMock<EnvironmentManagers>;
let mockProjectManager: typeMoq.IMock<PythonProjectManager>;

setup(() => {
mockEnvManagers = typeMoq.Mock.ofType<EnvironmentManagers>();
mockProjectManager = typeMoq.Mock.ofType<PythonProjectManager>();
});

test('should handle environment collection with empty data', () => {
mockEnvManagers.setup((em) => em.managers).returns(() => []);
mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => []);

// Test that empty collections are handled gracefully
const managers = mockEnvManagers.object.managers;
const projects = mockProjectManager.object.getProjects();

assert.strictEqual(managers.length, 0);
assert.strictEqual(projects.length, 0);
});

test('should handle environment collection with mock data', async () => {
// Create mock environment
const mockEnvId: PythonEnvironmentId = {
id: 'test-env-id',
managerId: 'test-manager'
};

const mockEnv: PythonEnvironment = {
envId: mockEnvId,
name: 'Test Environment',
displayName: 'Test Environment 3.9',
displayPath: '/path/to/python',
version: '3.9.0',
environmentPath: vscode.Uri.file('/path/to/env'),
execInfo: {
run: {
executable: '/path/to/python',
args: []
}
},
sysPrefix: '/path/to/env'
};

const mockManager = {
id: 'test-manager',
displayName: 'Test Manager',
getEnvironments: async () => [mockEnv]
} as any;

// Create mock project
const mockProject: PythonProject = {
uri: vscode.Uri.file('/path/to/project'),
name: 'Test Project'
};

mockEnvManagers.setup((em) => em.managers).returns(() => [mockManager]);
mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => [mockProject]);
mockEnvManagers.setup((em) => em.getEnvironment(typeMoq.It.isAny())).returns(() => Promise.resolve(mockEnv));

// Verify mocks are set up correctly
const managers = mockEnvManagers.object.managers;
const projects = mockProjectManager.object.getProjects();

assert.strictEqual(managers.length, 1);
assert.strictEqual(projects.length, 1);
assert.strictEqual(managers[0].id, 'test-manager');
assert.strictEqual(projects[0].name, 'Test Project');
});

test('should handle errors gracefully during environment collection', async () => {
const mockManager = {
id: 'error-manager',
displayName: 'Error Manager',
getEnvironments: async () => {
throw new Error('Test error');
}
} as any;

mockEnvManagers.setup((em) => em.managers).returns(() => [mockManager]);
mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => []);

// Verify that error conditions don't break the test setup
const managers = mockEnvManagers.object.managers;
assert.strictEqual(managers.length, 1);
assert.strictEqual(managers[0].id, 'error-manager');
});

test('should register report issue command', () => {
// Basic test to ensure command registration structure would work
// The actual command registration happens during extension activation
// This tests the mock setup and basic functionality

mockEnvManagers.setup((em) => em.managers).returns(() => []);
mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => []);

// Verify basic setup works
assert.notStrictEqual(mockEnvManagers.object, undefined);
assert.notStrictEqual(mockProjectManager.object, undefined);
});
});