Path: blob/main/extensions/copilot/src/extension/prompt/node/test/testFiles.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { suite, test } from 'vitest';7import type * as vscode from 'vscode';8import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';9import { AbstractSearchService } from '../../../../platform/search/common/searchService';10import { ITabsAndEditorsService, TabChangeEvent, TabInfo } from '../../../../platform/tabs/common/tabsAndEditorsService';11import * as glob from '../../../../util/common/glob';12import { createTextDocumentData } from '../../../../util/common/test/shims/textDocument';13import { CancellationToken } from '../../../../util/vs/base/common/cancellation';14import { Event } from '../../../../util/vs/base/common/event';15import { normalize } from '../../../../util/vs/base/common/path';16import { basename } from '../../../../util/vs/base/common/resources';17import { URI } from '../../../../util/vs/base/common/uri';18import { TestFileFinder, isTestFile, suffix2Language } from '../testFiles';1920suite.skipIf(process.platform === 'win32')('TestFileFinder', function () {2122class TestSearchService extends AbstractSearchService {23override async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {24return {};25}2627override findTextInFiles2(query: vscode.TextSearchQuery2, options?: vscode.FindTextInFilesOptions2, token?: vscode.CancellationToken): vscode.FindTextInFilesResponse {28return {} as vscode.FindTextInFilesResponse;29}3031override async findFiles(filePattern: vscode.GlobPattern, options?: vscode.FindFiles2Options | undefined, token?: vscode.CancellationToken | undefined): Promise<vscode.Uri[]> {32return [];33}34}3536class TestTabsService implements ITabsAndEditorsService {37declare _serviceBrand: undefined;38onDidChangeActiveTextEditor: vscode.Event<vscode.TextEditor | undefined> = Event.None;39onDidChangeTabs: vscode.Event<TabChangeEvent> = Event.None;40activeTextEditor: vscode.TextEditor | undefined = undefined;41visibleTextEditors: readonly vscode.TextEditor[] = [];42activeNotebookEditor: vscode.NotebookEditor | undefined = undefined;43visibleNotebookEditors: readonly vscode.NotebookEditor[] = [];44tabs: TabInfo[] = [];45}4647test('returns undefined when no test file exists', async function () {48const testFileFinder = new TestFileFinder(new TestSearchService(), new TestTabsService());49const sourceFile = URI.file('/path/to/source/file.ts');5051const result = await testFileFinder.findTestFileForSourceFile(createTextDocument(sourceFile), CancellationToken.None);5253assert.deepStrictEqual(result, undefined);54});5556test('uses tab info', async function () {5758const sourceFile = URI.file('/path/to/source/file.ts');59const testFileFinder = new TestFileFinder(new TestSearchService(), new class extends TestTabsService {60override tabs: TabInfo[] = [{61uri: URI.file('/path/to/source/file.spec.ts'),62tab: null!63}];64});6566const result = await testFileFinder.findTestFileForSourceFile(createTextDocument(sourceFile), CancellationToken.None);67assert.deepStrictEqual(result?.toString(), 'file:///path/to/source/file.spec.ts');68});6970test('returns test file URI when it exists', async function () {71const sourceFile = '/path/to/source/file.ts';72const testFile = '/path/to/source/file.test.ts';73await assertTestFileFoundAsync(sourceFile, testFile);74});7576const possibleTestNames = ['file.test.xx', 'file_test.xx', 'file.spec.xx', 'fileSpec.xx', 'test_file.xx'];77for (const testName of possibleTestNames) {78test(`for unknown languages, returns test file URI if it exists (${testName})`, async function () {79await assertTestFileFoundAsync('/path/file.xx', '/path/file.test.xx');80});81}8283test('returns a test for Go', async function () {84const sourceFile = '/path/to/source/foo.go';85const testFile = '/path/to/source/foo_test.go';86await assertTestFileFoundAsync(sourceFile, testFile);87});8889test('returns impl for Go', async function () {90const sourceFile = '/path/to/source/foo.go';91const testFile = '/path/to/source/foo_test.go';92await assertImplFileFoundAsync(testFile, sourceFile);93});9495test('returns same folder with prefix as fallback', async function () {96const sourceFile = '/path/to/source/foo.go';97const existingTestFile = '/path/to/source/foo_test.go';98await assertTestFileFoundAsync(sourceFile, existingTestFile, '/path/to/source');99});100101102103test('returns a test for Java for maven layout', async function () {104const sourceFile = '/src/main/java/p/Foo.java';105const testFile = '/src/test/java/p/FooTest.java';106await assertTestFileFoundAsync(sourceFile, testFile);107});108109test('returns a impl for Java for maven layout', async function () {110const testFile = '/src/test/java/p/FooTest.java';111const sourceFile = '/src/main/java/p/Foo.java';112await assertImplFileFoundAsync(testFile, sourceFile);113});114115test('returns a test for PHP', async function () {116const sourceFile = '/src/Foo.php';117const testFile = '/tests/FooTest.php';118await assertTestFileFoundAsync(sourceFile, testFile);119});120121test('returns a test for Dart', async function () {122const sourceFile = '/project/Foo.dart';123const testFile = '/tests/Foo_test.dart';124await assertTestFileFoundAsync(sourceFile, testFile);125});126127test('returns a test for Python', async function () {128const sourceFile = '/project/foo.py';129const testFile = '/tests/test_foo.py';130await assertTestFileFoundAsync(sourceFile, testFile);131});132133test('returns a test for C#', async function () {134const sourceFile = 'src/project/Foo.cs';135await assertTestFileFoundAsync(sourceFile, '/src/tests/project/FooTest.cs');136// assertTestFileFound(sourceFile, '/unit-tests/project/FooTest.cs');137// assertTestFileFound(sourceFile, '/unittests/project/FooTest.cs');138});139140test('returns a test for Ruby', async function () {141const sourceFile = 'app/api/foo.rb';142await assertTestFileFoundAsync(sourceFile, '/test/app/api/foo_test.rb');143});144145test('determine java test file with absolute path', async () => {146await assertTestFileFoundAsync(147'/Users/copilot/git/commons-io/src/main/java/org/apache/commons/io/EndianUtils.java',148'/Users/copilot/git/commons-io/src/test/java/org/apache/commons/io/EndianUtilsTest.java',149'file:///Users/copilot/git/commons-io'150);151});152153test('determine rb test file with absolute path', async () => {154await assertTestFileFoundAsync(155'/Users/copilot/git/github/foo/util.rb',156'/Users/copilot/git/github/test/foo/util_test.rb',157'file:///Users/copilot/git/github'158);159});160161test('determine php test file with absolute path', async () => {162await assertTestFileFoundAsync(163'/Users/copilot/git/github/foo/util.php',164'/Users/copilot/git/github/tests/utilTest.php',165'file:///Users/copilot/git/github'166);167});168169test('determine ps1 test file with absolute path', async () => {170await assertTestFileFoundAsync(171'/Users/copilot/git/github/foo/util.ps1',172'/Users/copilot/git/github/Tests/util.Tests.ps1',173'file:///Users/copilot/git/github'174);175});176177type TestSample = { filename: string; isTestFile: boolean };178const testSamples: TestSample[] = [179{ filename: 'foo.js', isTestFile: false },180{ filename: 'foo.test.js', isTestFile: true },181{ filename: 'foo.spec.js', isTestFile: true },182{ filename: 'foo.ts', isTestFile: false },183{ filename: 'foo.test.ts', isTestFile: true },184{ filename: 'foo.spec.ts', isTestFile: true },185{ filename: 'foo.py', isTestFile: false },186{ filename: 'test_foo.py', isTestFile: true },187{ filename: 'foo_test.py', isTestFile: true },188{ filename: 'foo.rb', isTestFile: false },189{ filename: 'foo_test.rb', isTestFile: true },190{ filename: 'foo.go', isTestFile: false },191{ filename: 'foo_test.go', isTestFile: true },192{ filename: 'foo.php', isTestFile: false },193{ filename: 'fooTest.php', isTestFile: true },194{ filename: 'Foo.java', isTestFile: false },195{ filename: 'FooTest.java', isTestFile: true },196{ filename: 'Foo.cs', isTestFile: false },197{ filename: 'FooTest.cs', isTestFile: true },198{ filename: 'foo.xx', isTestFile: false },199{ filename: 'foo~Test.xx', isTestFile: true },200{ filename: 'foo.spec.xx', isTestFile: true },201{ filename: 'fooTest.xx', isTestFile: true },202{ filename: 'test_foo.xx', isTestFile: true },203{ filename: 'foo.Tests.ps1', isTestFile: true },204];205// test for each sample206for (const sample of testSamples) {207test(`is ${sample.filename} a test file?`, () => {208const isTest = isTestFile(URI.file(sample.filename));209assert.strictEqual(isTest, sample.isTestFile);210});211}212213function createTextDocument(uri: URI) {214const sourceDocumentData = createTextDocumentData(uri, '', suffix2Language[basename(uri).substring(1)] ?? '');215return TextDocumentSnapshot.create(sourceDocumentData.document);216}217218async function assertTestFileFoundAsync(sourceFilePath: string, expectedTestFilePath: string, workspaceUri?: string) {219220const sourceFile = URI.file(sourceFilePath);221const expectedTestFile = URI.file(expectedTestFilePath);222223const testFileFinder = new TestFileFinder(new class extends AbstractSearchService {224override async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {225if (glob.shouldInclude(expectedTestFile, { include: options.include ? [options.include] : undefined, exclude: options.exclude ? [options.exclude] : undefined })) {226progress.report({ uri: expectedTestFile, ranges: [], preview: { matches: [], text: '' } });227}228return {};229}230override findTextInFiles2(query: vscode.TextSearchQuery2, options?: vscode.FindTextInFilesOptions2, token?: vscode.CancellationToken): vscode.FindTextInFilesResponse {231throw new Error('not implemented');232}233override async findFiles(filePattern: vscode.GlobPattern, options?: vscode.FindFiles2Options | undefined, token?: vscode.CancellationToken | undefined): Promise<vscode.Uri[]> {234if (glob.shouldInclude(expectedTestFile, { include: [filePattern], exclude: options?.exclude ? options.exclude : undefined })) {235return [expectedTestFile];236}237return [];238}239}, new TestTabsService());240241const sourceDocument = createTextDocument(sourceFile);242const result = await testFileFinder.findTestFileForSourceFile(sourceDocument, CancellationToken.None);243244assert.ok(result);245assert.strictEqual(normalize(result!.path), normalize(expectedTestFilePath.toString()));246}247248async function assertImplFileFoundAsync(testFilePath: string, expectedImplFilePath: string) {249250const testFile = URI.file(testFilePath);251const expectedImplFile = URI.file(expectedImplFilePath);252253const testFileFinder = new TestFileFinder(new class extends AbstractSearchService {254override async findTextInFiles(query: vscode.TextSearchQuery, options: vscode.FindTextInFilesOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {255if (glob.isMatch(expectedImplFile, options.include!) && (!options.exclude || !glob.isMatch(expectedImplFile, options.exclude))) {256progress.report({257uri: expectedImplFile,258ranges: [],259preview: { text: '', matches: [] }260});261}262return {};263}264override findTextInFiles2(query: vscode.TextSearchQuery2, options?: vscode.FindTextInFilesOptions2, token?: vscode.CancellationToken): vscode.FindTextInFilesResponse {265throw new Error('not implemented');266}267override async findFiles(filePattern: vscode.GlobPattern, options?: vscode.FindFiles2Options | undefined, token?: vscode.CancellationToken | undefined): Promise<vscode.Uri[]> {268if (glob.isMatch(expectedImplFile, filePattern) && (!options?.exclude || !options.exclude.some(e => glob.isMatch(expectedImplFile, e)))) {269return [expectedImplFile];270}271return [];272}273}, new TestTabsService());274275const testFileDocument = createTextDocument(testFile);276const result = await testFileFinder.findFileForTestFile(testFileDocument, CancellationToken.None);277278assert.notStrictEqual(result, undefined);279assert.strictEqual(normalize(result!.path), normalize(expectedImplFilePath.toString()));280}281});282283284