Add undeclared test case failures to test.xml

This matches the behavior of the maven surefire plugin.

Fixes #19949

Closes #19966.

PiperOrigin-RevId: 578255510
Change-Id: I2512c387ee63adf054de8af3e19e9ddedc4e368d
diff --git a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
index 3a90867..a9ca4cf 100644
--- a/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
+++ b/src/java_tools/junitrunner/java/com/google/testing/junit/runner/model/TestSuiteModel.java
@@ -182,6 +182,23 @@
       } else {
         test.testFailure(throwable, now());
       }
+    } else {
+      // this is a test case dynamically added by the suite runner (such as mockito)
+      TestSuiteNode testSuite =
+          (TestSuiteNode)
+              rootNode.getChildren().stream()
+                  .filter(node -> node instanceof TestSuiteNode)
+                  .filter(
+                      node ->
+                          node.getDescription().getTestClass().equals(description.getTestClass()))
+                  .findAny()
+                  .orElseThrow(() -> new IllegalStateException("expected to find test suite node"));
+      TestCaseNode testCase = new TestCaseNode(description, testSuite);
+      testsMap.put(description, testCase);
+      testCaseMap.put(description, testCase);
+      testSuite.addTestCase(testCase);
+      // since this is the first time we're learning of this, the timing data will be incorrect :(
+      testCase.testFailure(throwable, now());
     }
   }
 
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4_testbridge_integration_tests.sh b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4_testbridge_integration_tests.sh
index ac006c8..2494005 100755
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4_testbridge_integration_tests.sh
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/junit4_testbridge_integration_tests.sh
@@ -108,4 +108,16 @@
   expect_log 'Failures: 2'
 }
 
+# Test that we fail on suite failures even if individual test cases pass
+function test_JunitUndeclaredTestCaseFailures() {
+  cd "${TEST_TMPDIR}" || fail "Unexpected failure"
+
+  "${TESTBED}" \
+  --jvm_flag="-D${SUITE_PARAMETER}=com.google.testing.junit.runner.testbed.Junit4UndeclaredTestCaseFailures" \
+   &> "${TEST_log}" && fail "Expected failure"
+  expect_log 'unnecessary Mockito stubbings'
+  grep -q "tests='2' failures='1'" ${XML_OUTPUT_FILE} || \
+    fail "Expected 1 failure in xml output: `cat ${XML_OUTPUT_FILE}`"
+}
+
 run_suite "junit4_testbridge_integration_test"
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/testbed/BUILD b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/testbed/BUILD
index 33de6c4..97a9e74 100644
--- a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/testbed/BUILD
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/testbed/BUILD
@@ -14,6 +14,7 @@
     deps = [
         "//third_party:guava",
         "//third_party:junit4",
+        "//third_party:mockito",
         "//third_party:truth",
     ],
 )
diff --git a/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/testbed/Junit4UndeclaredTestCaseFailures.java b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/testbed/Junit4UndeclaredTestCaseFailures.java
new file mode 100644
index 0000000..ef1d591
--- /dev/null
+++ b/src/java_tools/junitrunner/javatests/com/google/testing/junit/runner/testbed/Junit4UndeclaredTestCaseFailures.java
@@ -0,0 +1,36 @@
+// Copyright 2023 The Bazel Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.testing.junit.runner.testbed;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/** A JUnit4-style test meant to be invoked by junit4_testbridge_tests.sh. */
+@RunWith(MockitoJUnitRunner.Strict.class)
+public class Junit4UndeclaredTestCaseFailures {
+
+  @SuppressWarnings("unchecked")
+  private final List<String> mockList = mock(List.class);
+
+  @Test
+  public void passesButHasUnnecessaryStubs() {
+    when(mockList.add("")).thenReturn(true); // this won't get called
+  }
+}