Reporting skipped test cases

`<skipped>` is a valid and common test case status supported by JUnit XML. Reporting skipped test cases as "passed" hides potential broken test cases. This PR reports them when `--test_summary=detailed`.

Before the fix:

```
INFO: Found 1 target and 1 test target...
INFO: Elapsed time: 103.095s, Critical Path: 43.23s
INFO: 476 processes: 36 internal, 440 darwin-sandbox.
INFO: Build completed successfully, 476 total actions
//foo/collect:go_default_test    PASSED in 0.6s
    PASSED  collect.TestCollect (0.0s)
    PASSED  collect.TestCollectBuildFailure (0.0s)
    PASSED  collect.TestFindComponentTestPaths (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_does_not_exist_in_directory_tree (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_does_not_exist_in_provided_directory (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_exists_in_directory_tree (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_exists_in_provided_directory (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_should_be_the_same (0.0s)
    PASSED  collect.TestFindComponentTestPaths/directory_above_service_root (0.0s)
    PASSED  collect.TestFindComponentTestPaths/in_component_tests_directory (0.0s)
    PASSED  collect.TestFindComponentTestPaths/multiple_component_tests_dirs (0.0s)
    PASSED  collect.TestFindPythonTestPath (0.0s)
    PASSED  collect.TestFindPythonTestPath/not_a_python_package (0.0s)
    PASSED  collect.TestFindPythonTestPath/python_tests_path_does_not_exist (0.0s)
    PASSED  collect.TestFindPythonTestPath/python_tests_path_exists (0.0s)
    PASSED  collect.TestRunComponentTests (0.0s)
    PASSED  collect.TestSetTest (0.0s)
    PASSED  collect.TestSetTest/component (0.0s)
    PASSED  collect.TestSetTest/fail (0.0s)
    PASSED  collect.TestSetTest/unit (0.0s)
    PASSED  collect.TestString (0.0s)
    PASSED  collect.TestString/should_be_component (0.0s)
    PASSED  collect.TestString/should_be_unit (0.0s)
Test cases: finished with 23 passing and 0 failing out of 23 test cases

Executed 1 out of 1 test: 1 test passes.
```

After the fix:

```
INFO: Found 1 target and 1 test target...
INFO: Elapsed time: 147.787s, Critical Path: 42.03s
INFO: 476 processes: 36 internal, 440 darwin-sandbox.
INFO: Build completed successfully, 476 total actions
//foo/collect:go_default_test    PASSED in 0.7s
    PASSED  collect.TestCollectBuildFailure (0.0s)
    PASSED  collect.TestFindComponentTestPaths (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_does_not_exist_in_directory_tree (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_does_not_exist_in_provided_directory (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_exists_in_directory_tree (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_exists_in_provided_directory (0.0s)
    PASSED  collect.TestFindComponentTestPaths/component_tests_dir_should_be_the_same (0.0s)
    PASSED  collect.TestFindComponentTestPaths/directory_above_service_root (0.0s)
    PASSED  collect.TestFindComponentTestPaths/in_component_tests_directory (0.0s)
    PASSED  collect.TestFindComponentTestPaths/multiple_component_tests_dirs (0.0s)
    PASSED  collect.TestFindPythonTestPath (0.0s)
    PASSED  collect.TestFindPythonTestPath/not_a_python_package (0.0s)
    PASSED  collect.TestFindPythonTestPath/python_tests_path_does_not_exist (0.0s)
    PASSED  collect.TestFindPythonTestPath/python_tests_path_exists (0.0s)
    PASSED  collect.TestRunComponentTests (0.0s)
    PASSED  collect.TestSetTest (0.0s)
    PASSED  collect.TestSetTest/component (0.0s)
    PASSED  collect.TestSetTest/fail (0.0s)
    PASSED  collect.TestSetTest/unit (0.0s)
    PASSED  collect.TestString (0.0s)
    PASSED  collect.TestString/should_be_component (0.0s)
    PASSED  collect.TestString/should_be_unit (0.0s)
    SKIPPED collect.TestCollect (0.0s)
Test cases: finished with 22 passing, 1 skipped and 0 failing out of 23 test cases

Executed 1 out of 1 test: 1 test passes.
```

Closes #23766.

PiperOrigin-RevId: 688477216
Change-Id: I4353d1dd4801a19ec56aae40d632d9f1061cfdbe
diff --git a/src/main/java/com/google/devtools/build/lib/exec/TestXmlOutputParser.java b/src/main/java/com/google/devtools/build/lib/exec/TestXmlOutputParser.java
index fea69d2..54e84ee 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/TestXmlOutputParser.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/TestXmlOutputParser.java
@@ -216,6 +216,7 @@
       throws XMLStreamException, TestXmlOutputParserException {
     int failures = 0;
     int errors = 0;
+    boolean skipped = false;
 
     while (true) {
       int event = parser.next();
@@ -243,6 +244,10 @@
               errors += 1;
               skipCompleteElement(parser);
               break;
+            case "skipped":
+              skipped = true;
+              skipCompleteElement(parser);
+              break;
             case "testdecorator":
               builder.addChild(parseTestDecorator(parser));
               break;
@@ -268,6 +273,8 @@
             builder.setStatus(TestCase.Status.ERROR);
           } else if (failures > 0) {
             builder.setStatus(TestCase.Status.FAILED);
+          } else if (skipped) {
+            builder.setStatus(TestCase.Status.SKIPPED);
           } else {
             builder.setStatus(TestCase.Status.PASSED);
           }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifier.java b/src/main/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifier.java
index 363f53b..fb0d468 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifier.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifier.java
@@ -58,6 +58,7 @@
 
     int totalTestCases;
     int totalFailedTestCases;
+    int totalSkippedTestCases;
     int totalUnknownTestCases;
   }
 
@@ -195,6 +196,7 @@
       stats.totalTestCases += summary.getTotalTestCases();
       stats.totalUnknownTestCases += summary.getUnkownTestCases();
       stats.totalFailedTestCases += summary.getFailedTestCases().size();
+      stats.totalSkippedTestCases += summary.getSkippedTestCases().size();
     }
 
     stats.failedCount = summaries.size() - stats.passCount;
@@ -265,13 +267,20 @@
     TestSummaryFormat testSummaryFormat = options.getOptions(ExecutionOptions.class).testSummary;
     if (testSummaryFormat == DETAILED || testSummaryFormat == TESTCASE) {
       int passCount =
-          stats.totalTestCases - stats.totalFailedTestCases - stats.totalUnknownTestCases;
+          stats.totalTestCases
+              - stats.totalFailedTestCases
+              - stats.totalUnknownTestCases
+              - stats.totalSkippedTestCases;
       String message =
           String.format(
-              "Test cases: finished with %s%d passing%s and %s%d failing%s out of %d test cases",
+              "Test cases: finished with %s%d passing%s, %s%d skipped%s and %s%d failing%s out of"
+                  + " %d test cases",
               passCount > 0 ? AnsiTerminalPrinter.Mode.INFO : "",
               passCount,
               AnsiTerminalPrinter.Mode.DEFAULT,
+              stats.totalSkippedTestCases > 0 ? AnsiTerminalPrinter.Mode.WARNING : "",
+              stats.totalSkippedTestCases,
+              AnsiTerminalPrinter.Mode.DEFAULT,
               stats.totalFailedTestCases > 0 ? AnsiTerminalPrinter.Mode.ERROR : "",
               stats.totalFailedTestCases,
               AnsiTerminalPrinter.Mode.DEFAULT,
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
index 12d4ee1..80a1ffa 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestSummary.java
@@ -41,7 +41,6 @@
 import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
 import com.google.devtools.build.lib.view.test.TestStatus.FailedTestCasesStatus;
 import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
-import com.google.devtools.build.lib.view.test.TestStatus.TestCase.Status;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import com.google.protobuf.util.Durations;
 import com.google.protobuf.util.Timestamps;
@@ -216,12 +215,10 @@
         // Don't count test cases that were not run.
         return 0;
       }
-      if (testCase.getStatus() != TestCase.Status.PASSED) {
-        this.summary.failedTestCases.add(testCase);
-      }
-
-      if (testCase.getStatus() == Status.PASSED) {
-        this.summary.passedTestCases.add(testCase);
+      switch (testCase.getStatus()) {
+        case PASSED -> this.summary.passedTestCases.add(testCase);
+        case SKIPPED -> this.summary.skippedTestCases.add(testCase);
+        default -> this.summary.failedTestCases.add(testCase);
       }
 
       return 1;
@@ -409,6 +406,7 @@
   private boolean wasUnreportedWrongSize;
   private List<TestCase> failedTestCases = new ArrayList<>();
   private final List<TestCase> passedTestCases = new ArrayList<>();
+  private final List<TestCase> skippedTestCases = new ArrayList<>();
   private List<Path> passedLogs = new ArrayList<>();
   private List<Path> failedLogs = new ArrayList<>();
   private List<String> warnings = new ArrayList<>();
@@ -520,6 +518,10 @@
     return failedTestCases;
   }
 
+  public List<TestCase> getSkippedTestCases() {
+    return skippedTestCases;
+  }
+
   public List<TestCase> getPassedTestCases() {
     return passedTestCases;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java b/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
index 5f3efda..eb9ecc4 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
@@ -25,7 +25,6 @@
 import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
 import com.google.devtools.build.lib.view.test.TestStatus.FailedTestCasesStatus;
 import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
-import com.google.devtools.build.lib.view.test.TestStatus.TestCase.Status;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -167,6 +166,9 @@
       for (TestCase testCase : summary.getPassedTestCases()) {
         TestSummaryPrinter.printTestCase(terminalPrinter, testCase);
       }
+      for (TestCase testCase : summary.getSkippedTestCases()) {
+        TestSummaryPrinter.printTestCase(terminalPrinter, testCase);
+      }
 
       if (summary.getStatus() == BlazeTestStatus.FAILED) {
         if (summary.getFailedTestCasesStatus() == FailedTestCasesStatus.NOT_AVAILABLE) {
@@ -227,7 +229,12 @@
       timeSummary = "";
     }
 
-    Mode mode = (testCase.getStatus() == Status.PASSED) ? Mode.INFO : Mode.ERROR;
+    Mode mode =
+        switch (testCase.getStatus()) {
+          case PASSED -> Mode.INFO;
+          case SKIPPED -> Mode.WARNING;
+          default -> Mode.ERROR;
+        };
     terminalPrinter.print(
         "    "
             + mode
diff --git a/src/main/protobuf/test_status.proto b/src/main/protobuf/test_status.proto
index e77a19f..61cf22f 100644
--- a/src/main/protobuf/test_status.proto
+++ b/src/main/protobuf/test_status.proto
@@ -58,6 +58,7 @@
     PASSED = 0;
     FAILED = 1;
     ERROR = 2;
+    SKIPPED = 3;
   }
 
   repeated TestCase child = 1;
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifierTest.java b/src/test/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifierTest.java
index b2ad9f0..fd1ffe4 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifierTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/TerminalTestResultNotifierTest.java
@@ -56,6 +56,7 @@
 
   private BlazeTestStatus targetStatus;
   private int numFailedTestCases;
+  private int numSkippedTestCases;
   private int numUnknownTestCases;
   private int numTotalTestCases;
   private TestSummaryFormat testSummaryFormat;
@@ -172,6 +173,27 @@
     String printed = getPrintedMessage();
     assertThat(printed).contains(info("10 passing"));
     assertThat(printed).contains("0 failing");
+    assertThat(printed).contains("0 skipped");
+    assertThat(printed).contains("out of 10 test cases");
+    assertThat(printed).doesNotContain(SOME_TARGETS_ARE_MISSING_TEST_CASES_DISCLAIMER);
+    assertThat(printed).doesNotContain(AnsiTerminalPrinter.Mode.ERROR.toString());
+  }
+
+  @Test
+  public void detailedOption_allPassButSomeSkipped() throws Exception {
+    testSummaryFormat = ExecutionOptions.TestSummaryFormat.DETAILED;
+    numFailedTestCases = 0;
+    numUnknownTestCases = 0;
+    numTotalTestCases = 10;
+    numSkippedTestCases = 2;
+    targetStatus = BlazeTestStatus.PASSED;
+
+    printTestCaseSummary();
+
+    String printed = getPrintedMessage();
+    assertThat(printed).contains(info("8 passing"));
+    assertThat(printed).contains("0 failing");
+    assertThat(printed).contains(warn("2 skipped"));
     assertThat(printed).contains("out of 10 test cases");
     assertThat(printed).doesNotContain(SOME_TARGETS_ARE_MISSING_TEST_CASES_DISCLAIMER);
     assertThat(printed).doesNotContain(AnsiTerminalPrinter.Mode.ERROR.toString());
@@ -313,9 +335,12 @@
     when(testSummary.getUnkownTestCases()).thenReturn(numUnknownTestCases);
     TestCase failedTestCase = TestCase.newBuilder().setStatus(Status.FAILED).build();
     List<TestCase> failedTestCases = Collections.nCopies(numFailedTestCases, failedTestCase);
+    TestCase skippedTestCase = TestCase.newBuilder().setStatus(Status.SKIPPED).build();
+    List<TestCase> skippedTestCases = Collections.nCopies(numSkippedTestCases, skippedTestCase);
 
     Label labelA = Label.parseCanonical("//foo/bar:baz");
     when(testSummary.getFailedTestCases()).thenReturn(failedTestCases);
+    when(testSummary.getSkippedTestCases()).thenReturn(skippedTestCases);
     when(testSummary.getStatus()).thenReturn(targetStatus);
     when(testSummary.getLabel()).thenReturn(labelA);
 
@@ -342,6 +367,10 @@
     return AnsiTerminalPrinter.Mode.INFO + message + AnsiTerminalPrinter.Mode.DEFAULT;
   }
 
+  private static String warn(String message) {
+    return AnsiTerminalPrinter.Mode.WARNING + message + AnsiTerminalPrinter.Mode.DEFAULT;
+  }
+
   private static String error(String message) {
     return AnsiTerminalPrinter.Mode.ERROR + message + AnsiTerminalPrinter.Mode.DEFAULT;
   }
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/TestSummaryTest.java b/src/test/java/com/google/devtools/build/lib/runtime/TestSummaryTest.java
index a82242a..253d21f 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/TestSummaryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/TestSummaryTest.java
@@ -551,6 +551,7 @@
             .addChild(newDetail("apple", TestCase.Status.FAILED, 1000L))
             .addChild(newDetail("banana", TestCase.Status.PASSED, 1000L))
             .addChild(newDetail("cherry", TestCase.Status.ERROR, 1000L))
+            .addChild(newDetail("sugarcane", Status.SKIPPED, 0))
             .build();
 
     TestSummary summary =
@@ -562,6 +563,7 @@
     verify(printer).print(find("FAILED.*apple"));
     verify(printer).print(find("PASSED.*banana"));
     verify(printer).print(find("ERROR.*cherry"));
+    verify(printer).print(find("SKIPPED.*sugarcane"));
   }
 
   @Test
@@ -571,6 +573,7 @@
             .setName("tests")
             .setRunDurationMillis(5000L)
             .addChild(newDetail("apple", TestCase.Status.PASSED, 1000L))
+            .addChild(newDetail("banana", Status.SKIPPED, 0))
             .build();
 
     TestSummary summary =
@@ -580,6 +583,7 @@
     TestSummaryPrinter.print(summary, printer, Path::getPathString, true, true);
     verify(printer).print(contains("//package:name"));
     verify(printer).print(find("PASSED.*apple"));
+    verify(printer).print(find("SKIPPED.*banana"));
   }
 
   @Test
@@ -591,12 +595,13 @@
             .addChild(newDetail("apple", TestCase.Status.FAILED, 1000L))
             .addChild(newDetail("banana", TestCase.Status.PASSED, 1000L))
             .addChild(newDetail("cherry", TestCase.Status.ERROR, 1000L))
+            .addChild(newDetail("sugarcane", Status.SKIPPED, 0))
             .build();
 
     TestSummary summary =
         getTemplateBuilder().collectTestCases(rootCase).setStatus(BlazeTestStatus.FAILED).build();
 
-    assertThat(summary.getTotalTestCases()).isEqualTo(3);
+    assertThat(summary.getTotalTestCases()).isEqualTo(4);
   }
 
   @Test