Skip to content

Commit

Permalink
Unit Test Detection: Improve language specific cases (#4387)
Browse files Browse the repository at this point in the history
  • Loading branch information
RXminuS authored May 30, 2024
1 parent 6fe120c commit 82066df
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 14 deletions.
18 changes: 18 additions & 0 deletions vscode/src/commands/utils/test-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ describe('isValidTestFile', () => {
['example_test.cpp', true],
['example_test.js', true],
['test_example.rb', true],
['test_logic.ts', false],
['test_logic.spec.ts', true],

// Should not cache false positives
['contest.ts', false],
Expand All @@ -91,6 +93,22 @@ describe('isTestFileForOriginal', () => {
['Example.groovy', 'ExampleSpec.groovy', true],
['example.rb', 'test_example.rb', true],
['/path/test/to/file.js', '/path/to/test-file.js', false],
['testClient.ts', 'testClient.test.ts', true],
['/src/main/java/com/example/MyClass.java', '/src/test/java/com/example/MyClassTest.java', true], // This follows the Maven/Gradle standard directory layout for Java.
['/src/main/kotlin/com/example/MyClass.kt', '/src/test/kotlin/com/example/MyClassTest.kt', true], // This is the preferred way to structure tests in Kotlin projects using Gradle or Maven.
['/src/main/scala/com/my/MyClass.scala', '/src/test/scala/com/my/MyClassSpec.scala', true], // This follows the sbt standard directory layout for Scala.
['/src/components/Button.js', '/src/components/__tests__/Button.test.js', true], // Jest encourages placing tests in a `__tests__` directory adjacent to the files they are testing.
['/src/server/app.ts', '/test/app.test.ts', true], // Common structure for backend projects using Mocha where test files are placed in a separate `test` directory.
['/src/server/models/User.js', '/src/server/models/User.spec.js', true], // Some projects prefer keeping tests next to the files they are testing with a `.spec.js` suffix.
['/project/module.py', '/tests/test_module.py', true], // This follows the convention of placing tests in a separate `tests` directory with a `test_` prefix.
['/project/package/module.py', '/project/package/test_module.py', true], // pytest allows tests to be placed in the same directory as the code with a `test_` prefix.
['/project/module.py', '/tests/module_tests.py', true], // Some projects using nose place tests in a separate `tests` directory with a `_tests` suffix.
['/src/project/main.go', '/src/project/main_test.go', true], // The Go testing package expects test files to be in the same directory with a `_test.go` suffix.
['/src/project/main.go', '/test/project/main_test.go', true], // Some projects prefer placing tests in a separate `test` directory.
['/test_logic.ts', 'test_logic.ts', false],
['a/b/Client.py', 'test/Client.py', true], // Test file located in a `test` directory adjacent to the file being tested.
['a/b/test_logic.ts', 'c/d/test_logic.ts', false], // Test file located in a different directory.
['a/b/Client.py', 'test/client.py', false], // Test file located in a `test` directory adjacent to the file being tested.
])('for file %j and test file %j it returns %j', (file, testFile, condition) => {
expect(isTestFileForOriginal(URI.file(file), URI.file(testFile))).toBe(condition)
})
Expand Down
58 changes: 44 additions & 14 deletions vscode/src/commands/utils/test-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,20 @@ export function extractTestType(text: string): string {
* Also returns false for any files in node_modules directory.
*/
export function isValidTestFile(uri: URI): boolean {
const fileNameWithoutExt = uriBasename(uri, uriExtname(uri))
const ext = uriExtname(uri)
const fileNameWithoutExt = uriBasename(uri, ext)

const suffixTest = /([._-](test|spec))|Test|Spec$/
// For some languages we check for a prefix
const prefixTest = /\.(?:py|rb)$/
if (prefixTest.test(ext)) {
if (fileNameWithoutExt.startsWith('test_')) {
return true
}
}

return fileNameWithoutExt.startsWith('test_') || suffixTest.test(fileNameWithoutExt)
// All other cases we check the suffix
const suffixTest = /([._-](test|spec))|Test|Spec$/
return suffixTest.test(fileNameWithoutExt)
}

/**
Expand All @@ -39,20 +48,41 @@ export function isValidTestFile(uri: URI): boolean {
* @returns True if the test file matches the file
*/
export function isTestFileForOriginal(file: URI, testFile: URI): boolean {
// We assume that a file can never be its own testFile for the original file.
// Instead make sure to test if the file IS a validTestFile.
if (file.path === testFile.path) {
return false
}

const fileDir = Utils.dirname(file)?.path
const testDir = Utils.dirname(testFile)?.path

// Assume not a test file for the current file if they are in different directories
// and the testFile's file path do not include test(s)
if (Utils.dirname(file)?.path !== Utils.dirname(testFile)?.path) {
if (!/test/i.test(Utils.dirname(testFile)?.path)) {
return false
}
// and the testFile's file path does not include a test dir
const pathRegex = /(?:^|\/)_{0,2}tests?_{0,2}(?:\/|$)/i
if (fileDir !== testDir && !pathRegex.test(testDir)) {
return false
}

// The file extension should match as it's rare to write a test in another language.
if (uriExtname(file) !== uriExtname(testFile)) {
return false
}

const regex = /[^a-zA-Z0-9]/g
const fileName = Utils.basename(file).toLowerCase().replace(regex, '')
const testFileName = Utils.basename(testFile).toLowerCase().replace(regex, '')
// Finally we check if the filename without typical test keywords matches. This is pretty naiive
// but seems to cover quite a few test cases.
const sanitizeFileName = (name: string) =>
name
.replace(/[^a-zA-Z0-9]/g, '')
.toLowerCase()
.replace(/spec|tests?/g, '')

const fileName = Utils.basename(file)
const testFileName = Utils.basename(testFile)

const strippedFile = fileName.replace('spec', '').replace('test', '')
const strippedTestFile = testFileName.replace('spec', '').replace('test', '')
if (fileName.toLowerCase() === testFileName.toLowerCase() && fileName !== testFileName) {
return false
}

return strippedFile === strippedTestFile
return sanitizeFileName(fileName) === sanitizeFileName(testFileName)
}

0 comments on commit 82066df

Please sign in to comment.