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
24 changes: 20 additions & 4 deletions src/client/testing/utils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
import { TestItem, env } from 'vscode';
import { traceLog } from '../logging';
import { parseVsId } from './testController/common/projectUtils';

export async function writeTestIdToClipboard(testItem: TestItem): Promise<void> {
if (testItem && typeof testItem.id === 'string') {
if (testItem.id.includes('\\') && testItem.id.indexOf('::') === -1) {
// Strip the project scope prefix (if any) so only the test id is copied.
const testId = stripProjectId(testItem.id);
if (testId.includes('\\') && testId.indexOf('::') === -1) {
// Convert the id to a module.class.method format as this is a unittest
const moduleClassMethod = idToModuleClassMethod(testItem.id);
const moduleClassMethod = idToModuleClassMethod(testId);
if (moduleClassMethod) {
await env.clipboard.writeText(moduleClassMethod);
traceLog('Testing: Copied test id to clipboard, id: ' + moduleClassMethod);
return;
}
}
// Otherwise use the id as is for pytest
await clipboardWriteText(testItem.id);
traceLog('Testing: Copied test id to clipboard, id: ' + testItem.id);
await clipboardWriteText(testId);
traceLog('Testing: Copied test id to clipboard, id: ' + testId);
}
}

// Project ids are always URIs (see getProjectId), so only strip a prefix that looks
// like one. Otherwise a pytest parameter containing the separator text, such as
// "test_foo.py::test_value[value@@vsc@@suffix]", would be truncated.
const PROJECT_ID_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//;

function stripProjectId(vsId: string): string {
const [projectId, testId] = parseVsId(vsId);
if (projectId === undefined || !PROJECT_ID_PATTERN.test(projectId)) {
return vsId;
}
return testId;
}

export function idToModuleClassMethod(id: string): string | undefined {
// Split by backslash
const parts = id.split('\\');
Expand Down
134 changes: 88 additions & 46 deletions src/test/testing/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { TestItem, env } from 'vscode';
import * as utils from '../../client/testing/utils';
import sinon from 'sinon';
import { PROJECT_ID_SEPARATOR } from '../../client/testing/testController/common/projectUtils';

use(chaiAsPromised.default);

function test_idToModuleClassMethod() {
try {
expect(utils.idToModuleClassMethod('foo')).to.equal('foo');
expect(utils.idToModuleClassMethod('a/b/c.pyMyClass')).to.equal('c.MyClass');
expect(utils.idToModuleClassMethod('a/b/c.pyMyClassmy_method')).to.equal('c.MyClass.my_method');
expect(utils.idToModuleClassMethod('\\MyClass')).to.be.undefined;
console.log('test_idToModuleClassMethod passed');
} catch (e) {
console.error('test_idToModuleClassMethod failed:', e);
}
}

async function test_writeTestIdToClipboard() {
let clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves();
const { writeTestIdToClipboard } = utils;
try {
// unittest id
const testItem = { id: 'a/b/c.pyMyClass\\my_method' };
await writeTestIdToClipboard(testItem as any);
sinon.assert.calledOnceWithExactly(clipboardStub, 'c.MyClass.my_method');
clipboardStub.resetHistory();

// pytest id
const testItem2 = { id: 'tests/test_foo.py::TestClass::test_method' };
await writeTestIdToClipboard(testItem2 as any);
sinon.assert.calledOnceWithExactly(clipboardStub, 'tests/test_foo.py::TestClass::test_method');
clipboardStub.resetHistory();

// undefined
await writeTestIdToClipboard(undefined as any);
sinon.assert.notCalled(clipboardStub);

console.log('test_writeTestIdToClipboard passed');
} catch (e) {
console.error('test_writeTestIdToClipboard failed:', e);
} finally {
sinon.restore();
}
}

// Run tests
(async () => {
test_idToModuleClassMethod();
await test_writeTestIdToClipboard();
})();
suite('Testing - utils', () => {
suite('idToModuleClassMethod', () => {
test('single part is returned as is', () => {
expect(utils.idToModuleClassMethod('foo')).to.equal('foo');
});

test('file and class are converted to module.class', () => {
expect(utils.idToModuleClassMethod('a/b/c.py\\MyClass')).to.equal('c.MyClass');
});

test('file, class and method are converted to module.class.method', () => {
expect(utils.idToModuleClassMethod('a/b/c.py\\MyClass\\my_method')).to.equal('c.MyClass.my_method');
});

test('missing file name results in undefined', () => {
expect(utils.idToModuleClassMethod('\\MyClass')).to.be.undefined;
});
});

suite('writeTestIdToClipboard', () => {
async function copiedText(id: string): Promise<string> {
await utils.writeTestIdToClipboard(({ id } as unknown) as TestItem);
return env.clipboard.readText();
}

setup(async () => {
await env.clipboard.writeText('');
});

test('legacy pytest id is copied as is', async () => {
expect(await copiedText('tests/test_foo.py::TestClass::test_method')).to.equal(
'tests/test_foo.py::TestClass::test_method',
);
});

test('project scoped pytest id drops the project prefix', async () => {
const id = `file:///path/to/workspace${PROJECT_ID_SEPARATOR}/path/to/workspace/tests/unit/test_foo.py::test_bar`;

expect(await copiedText(id)).to.equal('/path/to/workspace/tests/unit/test_foo.py::test_bar');
});

test('project scoped parameterized pytest id keeps the parameters', async () => {
const id = `file:///path/to/workspace${PROJECT_ID_SEPARATOR}tests/unit/test_foo.py::test_pipe_single[False]`;

expect(await copiedText(id)).to.equal('tests/unit/test_foo.py::test_pipe_single[False]');
});

test('legacy parameterized pytest id containing the separator text is copied as is', async () => {
const id = `tests/test_foo.py::test_value[value${PROJECT_ID_SEPARATOR}suffix]`;

expect(await copiedText(id)).to.equal(id);
});

test('project scoped parameterized pytest id containing the separator text keeps the parameters', async () => {
const id = `file:///path/to/workspace${PROJECT_ID_SEPARATOR}tests/test_foo.py::test_value[value${PROJECT_ID_SEPARATOR}suffix]`;

expect(await copiedText(id)).to.equal(`tests/test_foo.py::test_value[value${PROJECT_ID_SEPARATOR}suffix]`);
});

test('project scoped windows pytest id drops the project prefix', async () => {
const id = `file:///c%3A/workspace${PROJECT_ID_SEPARATOR}c:\\workspace\\tests\\test_foo.py::test_bar`;

expect(await copiedText(id)).to.equal('c:\\workspace\\tests\\test_foo.py::test_bar');
});

test('legacy unittest id is converted to module.class.method', async () => {
expect(await copiedText('a/b/c.py\\MyClass\\my_method')).to.equal('c.MyClass.my_method');
});

test('project scoped unittest id is converted to module.class.method', async () => {
const id = `file:///path/to/workspace${PROJECT_ID_SEPARATOR}a/b/c.py\\MyClass\\my_method`;

expect(await copiedText(id)).to.equal('c.MyClass.my_method');
});

test('nothing is copied when there is no test item', async () => {
await utils.writeTestIdToClipboard((undefined as unknown) as TestItem);

expect(await env.clipboard.readText()).to.equal('');
});
});
});
Loading