diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2187.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2187.json
index 3e0c2f6a44..67320da791 100644
--- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2187.json
+++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2187.json
@@ -1,6 +1,6 @@
{
"ruleKey": "S2187",
"hasTruePositives": true,
- "falseNegatives": 13,
+ "falseNegatives": 14,
"falsePositives": 1
}
diff --git a/java-checks-test-sources/default/pom.xml b/java-checks-test-sources/default/pom.xml
index 4f5c42669c..3a13809867 100644
--- a/java-checks-test-sources/default/pom.xml
+++ b/java-checks-test-sources/default/pom.xml
@@ -998,6 +998,11 @@
jspecify
1.0.0
+
+ org.junit.platform
+ junit-platform-suite
+ provided
+
diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/NoTestInTestClassCheckCucumberTest.java b/java-checks-test-sources/default/src/test/java/checks/tests/NoTestInTestClassCheckCucumberTest.java
new file mode 100644
index 0000000000..79589d1014
--- /dev/null
+++ b/java-checks-test-sources/default/src/test/java/checks/tests/NoTestInTestClassCheckCucumberTest.java
@@ -0,0 +1,24 @@
+package checks.tests;
+
+import org.junit.platform.suite.api.IncludeEngines;
+
+@IncludeEngines("cucumber")
+class NoTestInTestClassCheckCucumberStandardTest {}
+
+@org.junit.platform.suite.api.IncludeEngines("cucumber")
+class NoTestInTestClassCheckCucumberFullyQualifiedTest {}
+
+@IncludeEngines("bellpepper")
+class NoTestInTestClassCheckBellPepperTest {} // Noncompliant
+
+@IncludeEngines({"spring", "cucumber"})
+class NoTestInTestClassCheckTwoEnginesTest{}
+
+@NoTestInTestClassIncompatibleAnnotations.IncludeEngines(42)
+class ClassWithFakeIncludeEnginesAnnotationTest {} // Noncompliant
+
+class NoTestInTestClassIncompatibleAnnotations {
+ @interface IncludeEngines {
+ int value();
+ }
+}
diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/NoTestInTestClassCheckCucumberWithoutSemanticTest.java b/java-checks-test-sources/default/src/test/java/checks/tests/NoTestInTestClassCheckCucumberWithoutSemanticTest.java
new file mode 100644
index 0000000000..e9016ff134
--- /dev/null
+++ b/java-checks-test-sources/default/src/test/java/checks/tests/NoTestInTestClassCheckCucumberWithoutSemanticTest.java
@@ -0,0 +1,18 @@
+package checks.tests;
+
+import org.junit.platform.suite.api.IncludeEngines;
+
+@IncludeEngines("cucumber")
+class NoTestInTestClassCheckCucumberStandardWSTest {}
+
+@IncludeEngines("cucumber")
+class NoTestInTestClassCheckCucumberFullyQualifiedWSTest {}
+
+@IncludeEngines("bellpepper")
+class NoTestInTestClassCheckBellPepperWSTest {} // FN in automatic analysis
+
+@IncludeEngines({"spring", "cucumber"})
+class NoTestInTestClassCheckTwoEnginesWSTest{}
+
+@NoTestInTestClassIncompatibleAnnotations.IncludeEngines(42)
+class ClassWithFakeIncludeEnginesAnnotationWSTest {} // FN in automatic analysis
diff --git a/java-checks/src/main/java/org/sonar/java/checks/tests/NoTestInTestClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/tests/NoTestInTestClassCheck.java
index b91e394256..050df4176d 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/tests/NoTestInTestClassCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/tests/NoTestInTestClassCheck.java
@@ -170,7 +170,8 @@ private void addUsedAnnotations(Symbol.TypeSymbol classSymbol) {
}
private static boolean runWithCucumberOrSuiteOrTheoriesRunner(Symbol.TypeSymbol symbol) {
- return checkRunWith(symbol, "Cucumber", "Suite", "Theories");
+ return annotatedIncludeEnginesCucumber(symbol)
+ || checkRunWith(symbol, "Cucumber", "Suite", "Theories");
}
private static boolean runWithZohhak(Symbol.TypeSymbol symbol) {
@@ -199,6 +200,28 @@ private static boolean checkRunWithType(Symbol.TypeSymbol value, String... runne
return false;
}
+ /**
+ * True if the symbol is annotated {@code @IncludeEngines("cucumber")}
+ * (with some approximation for automatic analysis).
+ */
+ private static boolean annotatedIncludeEnginesCucumber(Symbol.TypeSymbol symbol) {
+ for (var annotation: symbol.metadata().annotations()) {
+ if (annotation.symbol().type().fullyQualifiedName().endsWith("IncludeEngines")) {
+ // values are not available in automatic analysis, so assume "cucumber" is there
+ if (annotation.values().isEmpty()) {
+ return true;
+ }
+ // otherwise check the list
+ boolean containsCucumber = annotation.values().stream().anyMatch(annotationValue ->
+ annotationValue.value() instanceof Object[] vals && Arrays.asList(vals).contains("cucumber"));
+ if (containsCucumber) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
private boolean isTestFieldOrMethod(Symbol member) {
return member.metadata().annotations().stream().anyMatch(input -> {
Type type = input.symbol().type();
diff --git a/java-checks/src/test/java/org/sonar/java/checks/tests/NoTestInTestClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/tests/NoTestInTestClassCheckTest.java
index 78f236f6a8..12d1a9d42c 100644
--- a/java-checks/src/test/java/org/sonar/java/checks/tests/NoTestInTestClassCheckTest.java
+++ b/java-checks/src/test/java/org/sonar/java/checks/tests/NoTestInTestClassCheckTest.java
@@ -93,4 +93,21 @@ void testNg() {
.withCheck(new NoTestInTestClassCheck())
.verifyIssues();
}
+
+ @Test
+ void testCucumber() {
+ CheckVerifier.newVerifier()
+ .onFile(testCodeSourcesPath("checks/tests/NoTestInTestClassCheckCucumberTest.java"))
+ .withCheck(new NoTestInTestClassCheck())
+ .verifyIssues();
+ }
+
+ @Test
+ void testCucumberWithoutSemantic() {
+ CheckVerifier.newVerifier()
+ .onFile(testCodeSourcesPath("checks/tests/NoTestInTestClassCheckCucumberWithoutSemanticTest.java"))
+ .withCheck(new NoTestInTestClassCheck())
+ .withoutSemantic()
+ .verifyNoIssues();
+ }
}