diff --git a/golang/src/com/google/idea/blaze/golang/run/producers/GoTestContextProvider.java b/golang/src/com/google/idea/blaze/golang/run/producers/GoTestContextProvider.java index d0d0dbe0cf7..14f87ac6e34 100644 --- a/golang/src/com/google/idea/blaze/golang/run/producers/GoTestContextProvider.java +++ b/golang/src/com/google/idea/blaze/golang/run/producers/GoTestContextProvider.java @@ -19,6 +19,7 @@ import com.goide.execution.testing.GoTestRunConfigurationProducerBase; import com.goide.psi.GoFile; import com.goide.psi.GoFunctionOrMethodDeclaration; +import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.ListenableFuture; import com.google.idea.blaze.base.dependencies.TargetInfo; import com.google.idea.blaze.base.run.ExecutorType; @@ -47,11 +48,14 @@ class GoTestContextProvider implements TestContextProvider { * When a user clicks in the ">" button, we get pointed to the "Run" part of "t.Run". * We need to walk the tree up to see the function call that has the actual argument. * + * This function doesn't worry about turning this filter into a regex which can be passed to --test_filter. + * @see #regexifyTestFilter for converting this to a --test_filter value. + * * @param element: Element the user has clicked on. * @param enclosingFunction: Go function encompassing this test. * @return String representation of the proper test filter. */ - private String calculateTestFilter(PsiElement element, GoFunctionOrMethodDeclaration enclosingFunction) { + private static String calculateRawTestFilterForElement(PsiElement element, GoFunctionOrMethodDeclaration enclosingFunction) { if (element.getParent().isEquivalentTo(enclosingFunction)) { return enclosingFunction.getName(); } else { @@ -59,6 +63,28 @@ private String calculateTestFilter(PsiElement element, GoFunctionOrMethodDeclara } } + @VisibleForTesting + static String regexifyTestFilter(String testFilter) { + return "^" + escapeRegexChars(testFilter) + "$"; + } + + private static String escapeRegexChars(String name) { + StringBuilder output = new StringBuilder(); + for (char c : name.toCharArray()) { + if (isRegexCharNeedingEscaping(c)) { + output.append("\\"); + } + output.append(c); + } + return output.toString(); + } + + private static boolean isRegexCharNeedingEscaping(char c) { + // Taken from https://cs.opensource.google/go/go/+/refs/tags/go1.21.4:src/regexp/regexp.go;l=720 + return c == '\\' || c == '.' || c == '+' || c == '*' || c == '?' || c == '(' || c == ')' || c == '|' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^' || c == '$'; + } + + public static final String GO_TEST_WRAP_TESTV = "GO_TEST_WRAP_TESTV=1"; @Nullable @Override @@ -84,11 +110,11 @@ public RunConfigurationContext getTestContext(ConfigurationContext context) { .setDescription(file.getName()) .build(); } - String testFilter = calculateTestFilter(element, function); + String testFilterRegex = regexifyTestFilter(calculateRawTestFilterForElement(element, function)); return TestContext.builder(/* sourceElement= */ function, ExecutorType.DEBUG_SUPPORTED_TYPES) .addTestEnv(GO_TEST_WRAP_TESTV) .setTarget(target) - .setTestFilter("^" + testFilter + "$") + .setTestFilter(testFilterRegex) .setDescription(String.format("%s#%s", file.getName(), function.getName())) .build(); } diff --git a/golang/tests/integrationtests/com/google/idea/blaze/golang/run/producers/BlazeGoTestConfigurationProducerTest.java b/golang/tests/integrationtests/com/google/idea/blaze/golang/run/producers/BlazeGoTestConfigurationProducerTest.java index c030d78a7b4..2defb33f1b8 100644 --- a/golang/tests/integrationtests/com/google/idea/blaze/golang/run/producers/BlazeGoTestConfigurationProducerTest.java +++ b/golang/tests/integrationtests/com/google/idea/blaze/golang/run/producers/BlazeGoTestConfigurationProducerTest.java @@ -155,12 +155,14 @@ public void testNestedTests() throws Throwable { "func TestFoo(t *testing.T) {", " t.Run(\"with_nested\", func(t *testing.T) {", " t.Run(\"subtest\", func(t *testing.T) {})", + " t.Run(\"subtest (great)\", func(t *testing.T) {})", " })", "}" ); assertElementGeneratesTestFilter(getElementAtCaret(0, goFile), "^TestFoo/with_nested$"); assertElementGeneratesTestFilter(getElementAtCaret(1, goFile), "^TestFoo/with_nested/subtest$"); + assertElementGeneratesTestFilter(getElementAtCaret(2, goFile), "^TestFoo/with_nested/subtest_\\(great\\)$"); } @Test public void testCodeBetweenTests() throws Throwable { diff --git a/golang/tests/unittests/com/google/idea/blaze/golang/run/producers/GoTestContextProviderTest.java b/golang/tests/unittests/com/google/idea/blaze/golang/run/producers/GoTestContextProviderTest.java new file mode 100644 index 00000000000..959a26cb508 --- /dev/null +++ b/golang/tests/unittests/com/google/idea/blaze/golang/run/producers/GoTestContextProviderTest.java @@ -0,0 +1,17 @@ +package com.google.idea.blaze.golang.run.producers; + +import com.google.idea.blaze.base.BlazeTestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(JUnit4.class) +public class GoTestContextProviderTest extends BlazeTestCase { + @Test + public void testRegexifyTestFilter() { + String regexified = GoTestContextProvider.regexifyTestFilter("Test/with/subtest(good \\ or \\ bad)"); + assertThat(regexified).isEqualTo("^Test/with/subtest\\(good \\\\ or \\\\ bad\\)$"); + } +}