Skip to content

Commit 687920f

Browse files
committed
updates to support unittest copy
1 parent faaf5e2 commit 687920f

File tree

4 files changed

+104
-6
lines changed

4 files changed

+104
-6
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@
12401240
{
12411241
"command": "python.copyTestId",
12421242
"group": "navigation",
1243-
"when": "resourceLangId == 'python'"
1243+
"when": "controllerId == 'python-tests'"
12441244
}
12451245
],
12461246
"commandPalette": [

src/client/testing/main.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import { DelayedTrigger, IDelayedTrigger } from '../common/utils/delayTrigger';
3030
import { ExtensionContextKey } from '../common/application/contextKeys';
3131
import { checkForFailedTests, updateTestResultMap } from './testController/common/testItemUtilities';
3232
import { Testing } from '../common/utils/localize';
33-
import { traceLog, traceVerbose } from '../logging';
33+
import { traceVerbose } from '../logging';
34+
import { writeTestIdToClipboard } from './utils';
3435

3536
@injectable()
3637
export class TestingService implements ITestingService {
@@ -205,10 +206,7 @@ export class UnitTestManagementService implements IExtensionActivationService {
205206
};
206207
}),
207208
commandManager.registerCommand(constants.Commands.CopyTestId, async (testItem: TestItem) => {
208-
if (testItem && typeof testItem.id === 'string') {
209-
await env.clipboard.writeText(testItem.id);
210-
traceLog('Testing: Copied test id to clipboard, id: ' + testItem.id);
211-
}
209+
writeTestIdToClipboard(testItem);
212210
}),
213211
);
214212
}

src/client/testing/utils.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { TestItem, env } from 'vscode';
2+
import { traceLog } from '../logging';
3+
4+
export async function writeTestIdToClipboard(testItem: TestItem): Promise<void> {
5+
if (testItem && typeof testItem.id === 'string') {
6+
if (testItem.id.includes('\\') && testItem.id.indexOf('::') === -1) {
7+
// Convert the id to a module.class.method format as this is a unittest
8+
const moduleClassMethod = idToModuleClassMethod(testItem.id);
9+
if (moduleClassMethod) {
10+
await env.clipboard.writeText(moduleClassMethod);
11+
traceLog('Testing: Copied test id to clipboard, id: ' + moduleClassMethod);
12+
return;
13+
}
14+
}
15+
// Otherwise use the id as is for pytest
16+
await clipboardWriteText(testItem.id);
17+
traceLog('Testing: Copied test id to clipboard, id: ' + testItem.id);
18+
}
19+
}
20+
21+
export function idToModuleClassMethod(id: string): string | undefined {
22+
// Split by backslash
23+
const parts = id.split('\\');
24+
if (parts.length === 1) {
25+
// Only one part, likely a parent folder or file
26+
return parts[0];
27+
}
28+
if (parts.length === 2) {
29+
// Two parts: filePath and className
30+
const [filePath, className] = parts.slice(-2);
31+
const fileName = filePath.split(/[\\/]/).pop();
32+
if (!fileName) {
33+
return undefined;
34+
}
35+
const module = fileName.replace(/\.py$/, '');
36+
return `${module}.${className}`;
37+
}
38+
// Three or more parts: filePath, className, methodName
39+
const [filePath, className, methodName] = parts.slice(-3);
40+
const fileName = filePath.split(/[\\/]/).pop();
41+
if (!fileName) {
42+
return undefined;
43+
}
44+
const module = fileName.replace(/\.py$/, '');
45+
return `${module}.${className}.${methodName}`;
46+
}
47+
export function clipboardWriteText(text: string): Thenable<void> {
48+
return env.clipboard.writeText(text);
49+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { expect, use } from 'chai';
2+
import * as chaiAsPromised from 'chai-as-promised';
3+
import * as utils from '../../client/testing/utils';
4+
import sinon from 'sinon';
5+
use(chaiAsPromised.default);
6+
7+
describe('idToModuleClassMethod', () => {
8+
it('returns the only part if there is one', () => {
9+
expect(utils.idToModuleClassMethod('foo')).to.equal('foo');
10+
});
11+
it('returns module.class for two parts', () => {
12+
expect(utils.idToModuleClassMethod('a/b/c.py\\MyClass')).to.equal('c.MyClass');
13+
});
14+
it('returns module.class.method for three parts', () => {
15+
expect(utils.idToModuleClassMethod('a/b/c.py\\MyClass\\my_method')).to.equal('c.MyClass.my_method');
16+
});
17+
it('returns undefined if fileName is missing', () => {
18+
expect(utils.idToModuleClassMethod('\\MyClass')).to.be.undefined;
19+
});
20+
});
21+
22+
describe('writeTestIdToClipboard', () => {
23+
let clipboardStub: sinon.SinonStub;
24+
25+
afterEach(() => {
26+
sinon.restore();
27+
});
28+
29+
it('writes module.class.method for unittest id', async () => {
30+
clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves();
31+
const { writeTestIdToClipboard } = utils;
32+
const testItem = { id: 'a/b/c.py\\MyClass\\my_method' };
33+
await writeTestIdToClipboard(testItem as any);
34+
sinon.assert.calledOnceWithExactly(clipboardStub, 'c.MyClass.my_method');
35+
});
36+
37+
it('writes id as is for pytest id', async () => {
38+
clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves();
39+
const { writeTestIdToClipboard } = utils;
40+
const testItem = { id: 'tests/test_foo.py::TestClass::test_method' };
41+
await writeTestIdToClipboard(testItem as any);
42+
sinon.assert.calledOnceWithExactly(clipboardStub, 'tests/test_foo.py::TestClass::test_method');
43+
});
44+
45+
it('does nothing if testItem is undefined', async () => {
46+
clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves();
47+
const { writeTestIdToClipboard } = utils;
48+
await writeTestIdToClipboard(undefined as any);
49+
sinon.assert.notCalled(clipboardStub);
50+
});
51+
});

0 commit comments

Comments
 (0)