Skip to content

Commit 3d371b1

Browse files
Copilotanthonykim1
andcommitted
Enhance pytest discovery error messages to offer installation option
Co-authored-by: anthonykim1 <[email protected]>
1 parent ac4cc4a commit 3d371b1

File tree

6 files changed

+40
-4
lines changed

6 files changed

+40
-4
lines changed

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export namespace Commands {
6464
export const Start_Native_REPL = 'python.startNativeREPL';
6565
export const Tests_Configure = 'python.configureTests';
6666
export const Tests_CopilotSetup = 'python.copilotSetupTests';
67+
export const Tests_Install_Pytest = 'python.installPytest';
6768
export const TriggerEnvironmentSelection = 'python.triggerEnvSelection';
6869
export const ViewOutput = 'python.viewOutput';
6970
}

src/client/testing/main.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ExtensionContextKey } from '../common/application/contextKeys';
2121
import { checkForFailedTests, updateTestResultMap } from './testController/common/testItemUtilities';
2222
import { Testing } from '../common/utils/localize';
2323
import { traceVerbose } from '../logging';
24+
import { PytestInstallationHelper } from './configuration/pytestInstallationHelper';
2425

2526
@injectable()
2627
export class TestingService implements ITestingService {
@@ -156,6 +157,22 @@ export class UnitTestManagementService implements IExtensionActivationService {
156157
await configurationService.promptToEnableAndConfigureTestFramework(wkspace!);
157158
}
158159

160+
private async installPytest(workspaceUriString: string): Promise<void> {
161+
try {
162+
const workspaceUri = Uri.parse(workspaceUriString);
163+
const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
164+
const installationHelper = new PytestInstallationHelper(appShell);
165+
166+
const success = await installationHelper.promptToInstallPytest(workspaceUri);
167+
if (success) {
168+
// Refresh test data after successful installation
169+
await this.testController?.refreshTestData(workspaceUri, { forceRefresh: true });
170+
}
171+
} catch (error) {
172+
traceVerbose('Error installing pytest from discovery error:', error);
173+
}
174+
}
175+
159176
private registerCommands(): void {
160177
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
161178

@@ -195,6 +212,9 @@ export class UnitTestManagementService implements IExtensionActivationService {
195212
},
196213
};
197214
}),
215+
commandManager.registerCommand(constants.Commands.Tests_Install_Pytest, async (workspaceUriString: string) => {
216+
await this.installPytest(workspaceUriString);
217+
}),
198218
);
199219
}
200220

src/client/testing/testController/common/resultResolver.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,19 @@ export class PythonResultResolver implements ITestResultResolver {
8686
errorNode = createErrorTestItem(this.testController, options);
8787
this.testController.items.add(errorNode);
8888
}
89-
const errorNodeLabel: MarkdownString = new MarkdownString(
90-
`[Show output](command:python.viewOutput) to view error logs`,
91-
);
89+
90+
let errorNodeLabel: MarkdownString;
91+
// Check if this is a pytest installation error and provide installation link
92+
const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider);
93+
if (options.isPytestNotInstalled) {
94+
errorNodeLabel = new MarkdownString(
95+
`[Install pytest](command:python.installPytest?${encodeURIComponent(JSON.stringify([this.workspaceUri.toString()]))}) | [Show output](command:python.viewOutput) to view error logs`,
96+
);
97+
} else {
98+
errorNodeLabel = new MarkdownString(
99+
`[Show output](command:python.viewOutput) to view error logs`,
100+
);
101+
}
92102
errorNodeLabel.isTrusted = true;
93103
errorNode.error = errorNodeLabel;
94104
} else {

src/client/testing/testController/common/testItemUtilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function removeItemByIdFromChildren(
5050
});
5151
}
5252

53-
export type ErrorTestItemOptions = { id: string; label: string; error: string };
53+
export type ErrorTestItemOptions = { id: string; label: string; error: string; isPytestNotInstalled?: boolean };
5454

5555
export function createErrorTestItem(testController: TestController, options: ErrorTestItemOptions): TestItem {
5656
const testItem = testController.createTestItem(options.id, options.label);

src/client/testing/testController/common/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export function buildErrorNodeOptions(uri: Uri, message: string, testType: strin
202202
id: `DiscoveryError:${uri.fsPath}`,
203203
label: `${labelText} [${path.basename(uri.fsPath)}]`,
204204
error: errorMessage,
205+
isPytestNotInstalled: testType === 'pytest' && isPytestNotInstalledError(message),
205206
};
206207
}
207208

src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ suite('buildErrorNodeOptions - pytest not installed detection', () => {
1818
expect(result.error).to.equal(
1919
'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.',
2020
);
21+
expect(result.isPytestNotInstalled).to.be.true;
2122
});
2223

2324
test('Should detect pytest ImportError and provide specific message', () => {
@@ -29,6 +30,7 @@ suite('buildErrorNodeOptions - pytest not installed detection', () => {
2930
expect(result.error).to.equal(
3031
'pytest is not installed in the selected Python environment. Please install pytest to enable test discovery and execution.',
3132
);
33+
expect(result.isPytestNotInstalled).to.be.true;
3234
});
3335

3436
test('Should use generic error for non-pytest-related errors', () => {
@@ -38,6 +40,7 @@ suite('buildErrorNodeOptions - pytest not installed detection', () => {
3840

3941
expect(result.label).to.equal('pytest Discovery Error [workspace]');
4042
expect(result.error).to.equal('Some other error occurred');
43+
expect(result.isPytestNotInstalled).to.be.false;
4144
});
4245

4346
test('Should use generic error for unittest errors', () => {
@@ -47,5 +50,6 @@ suite('buildErrorNodeOptions - pytest not installed detection', () => {
4750

4851
expect(result.label).to.equal('Unittest Discovery Error [workspace]');
4952
expect(result.error).to.equal("ModuleNotFoundError: No module named 'pytest'");
53+
expect(result.isPytestNotInstalled).to.be.undefined;
5054
});
5155
});

0 commit comments

Comments
 (0)