Fold FailureDetailUtil into DetailedExitCode

This reduces the number of notable programmatic entities associated
with FailureDetail messages by one.

Removing the "Util" class helps prevent it from being a dumping ground of
FailureDetail factory methods for categories that ought to be coupled
with their subsystems' code instead.

RELNOTES: None.
PiperOrigin-RevId: 303775924
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 47d2898..0db2add 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -208,7 +208,6 @@
             "util/Clock.java",
             "util/ExitCode.java",
             "util/DetailedExitCode.java",
-            "util/FailureDetailUtil.java",
             "util/FileType.java",
             "util/FileTypeSet.java",
             "util/GccParamFileEscaper.java",
@@ -334,20 +333,9 @@
     srcs = ["util/DetailedExitCode.java"],
     deps = [
         ":exitcode-external",
-        ":failure_detail_util",
         "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
         "//third_party:jsr305",
-    ],
-)
-
-java_library(
-    name = "failure_detail_util",
-    srcs = ["util/FailureDetailUtil.java"],
-    deps = [
-        ":exitcode-external",
-        "//src/main/protobuf:failure_details_java_proto",
-        "//third_party:guava",
         "//third_party/protobuf:protobuf_java",
     ],
 )
@@ -1138,7 +1126,6 @@
     deps = [
         ":bug-report",
         ":exitcode-external",
-        ":failure_detail_util",
         ":os_util",
         ":runtime",
         ":string_util",
diff --git a/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java b/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java
index 0dd282a..e8e6488 100644
--- a/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java
+++ b/src/main/java/com/google/devtools/build/lib/server/GrpcServerImpl.java
@@ -36,9 +36,9 @@
 import com.google.devtools.build.lib.server.CommandProtos.RunResponse;
 import com.google.devtools.build.lib.server.CommandProtos.ServerInfo;
 import com.google.devtools.build.lib.server.CommandProtos.StartupOption;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.Interrupted;
 import com.google.devtools.build.lib.util.ExitCode;
-import com.google.devtools.build.lib.util.FailureDetailUtil;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.util.io.OutErr;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -615,7 +615,11 @@
     } catch (InterruptedException e) {
       result =
           BlazeCommandResult.failureDetail(
-              FailureDetailUtil.interrupted(Interrupted.Code.INTERRUPTED_UNSPECIFIED));
+              FailureDetail.newBuilder()
+                  .setMessage("interrupted")
+                  .setInterrupted(
+                      Interrupted.newBuilder().setCode(Interrupted.Code.INTERRUPTED_UNSPECIFIED))
+                  .build());
       commandId = ""; // The default value, the client will ignore it
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/util/DetailedExitCode.java b/src/main/java/com/google/devtools/build/lib/util/DetailedExitCode.java
index de37710..75d8643 100644
--- a/src/main/java/com/google/devtools/build/lib/util/DetailedExitCode.java
+++ b/src/main/java/com/google/devtools/build/lib/util/DetailedExitCode.java
@@ -14,9 +14,15 @@
 
 package com.google.devtools.build.lib.util;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.devtools.build.lib.server.FailureDetails;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.protobuf.Descriptors.EnumValueDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.MessageOrBuilder;
+import java.util.Map;
 import javax.annotation.Nullable;
 
 /** An {@link ExitCode} and an optional {@link FailureDetail}. */
@@ -33,6 +39,15 @@
     return exitCode;
   }
 
+  /** Returns the registered {@link ExitCode} associated with a {@link FailureDetail} message. */
+  private static ExitCode getExitCode(FailureDetail failureDetail) {
+    // TODO(mschaller): Consider specializing for unregistered exit codes here, if absolutely
+    //  necessary.
+    int numericExitCode = getNumericExitCode(failureDetail);
+    return checkNotNull(
+        ExitCode.forCode(numericExitCode), "No ExitCode for numericExitCode %s", numericExitCode);
+  }
+
   @Nullable
   public FailureDetail getFailureDetail() {
     return failureDetail;
@@ -81,7 +96,7 @@
    * FailureDetail}'s metadata.
    */
   public static DetailedExitCode of(FailureDetail failureDetail) {
-    return new DetailedExitCode(FailureDetailUtil.getExitCode(failureDetail), failureDetail);
+    return new DetailedExitCode(getExitCode(failureDetail), failureDetail);
   }
 
   @Override
@@ -89,4 +104,68 @@
     return String.format(
         "DetailedExitCode{exitCode=%s, failureDetail=%s}", exitCode, failureDetail);
   }
+
+  /** Returns the numeric exit code associated with a {@link FailureDetail} message. */
+  private static int getNumericExitCode(FailureDetail failureDetail) {
+    MessageOrBuilder categoryMsg = getCategorySubmessage(failureDetail);
+    EnumValueDescriptor subcategoryDescriptor =
+        getSubcategoryDescriptor(failureDetail, categoryMsg);
+    return getNumericExitCode(subcategoryDescriptor);
+  }
+
+  /**
+   * Returns the numeric exit code associated with a {@link FailureDetail} submessage's subcategory
+   * enum value.
+   */
+  private static int getNumericExitCode(EnumValueDescriptor subcategoryDescriptor) {
+    checkArgument(
+        subcategoryDescriptor.getOptions().hasExtension(FailureDetails.metadata),
+        "Enum value %s has no FailureDetails.metadata",
+        subcategoryDescriptor);
+    return subcategoryDescriptor.getOptions().getExtension(FailureDetails.metadata).getExitCode();
+  }
+
+  /**
+   * Returns the category submessage, i.e. the message in {@link FailureDetail}'s oneof. Throws if
+   * none of those fields are set.
+   */
+  private static MessageOrBuilder getCategorySubmessage(FailureDetail failureDetail) {
+    MessageOrBuilder categoryMsg = null;
+    for (Map.Entry<FieldDescriptor, Object> entry : failureDetail.getAllFields().entrySet()) {
+      FieldDescriptor fieldDescriptor = entry.getKey();
+      if (isCategoryField(fieldDescriptor)) {
+        categoryMsg = (MessageOrBuilder) entry.getValue();
+        break;
+      }
+    }
+    return checkNotNull(
+        categoryMsg, "FailureDetail missing category submessage: %s", failureDetail);
+  }
+
+  /**
+   * Returns whether the {@link FieldDescriptor} describes a field in {@link FailureDetail}'s oneof.
+   *
+   * <p>Uses the field number criteria described in failure_details.proto.
+   */
+  private static boolean isCategoryField(FieldDescriptor fieldDescriptor) {
+    int fieldNum = fieldDescriptor.getNumber();
+    return 100 < fieldNum && fieldNum <= 10_000;
+  }
+
+  /**
+   * Returns the enum value descriptor for the enum field with field number 1 in the {@link
+   * FailureDetail}'s category submessage.
+   */
+  private static EnumValueDescriptor getSubcategoryDescriptor(
+      FailureDetail failureDetail, MessageOrBuilder categoryMsg) {
+    FieldDescriptor fieldNumberOne = categoryMsg.getDescriptorForType().findFieldByNumber(1);
+    checkNotNull(
+        fieldNumberOne, "FailureDetail category submessage has no field #1: %s", failureDetail);
+    Object fieldNumberOneVal = categoryMsg.getField(fieldNumberOne);
+    checkArgument(
+        fieldNumberOneVal instanceof EnumValueDescriptor,
+        "FailureDetail category submessage has non-enum field #1: %s",
+        failureDetail);
+    return (EnumValueDescriptor) fieldNumberOneVal;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/util/FailureDetailUtil.java b/src/main/java/com/google/devtools/build/lib/util/FailureDetailUtil.java
deleted file mode 100644
index 5f37275..0000000
--- a/src/main/java/com/google/devtools/build/lib/util/FailureDetailUtil.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright 2020 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.devtools.build.lib.util;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.devtools.build.lib.server.FailureDetails;
-import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
-import com.google.devtools.build.lib.server.FailureDetails.Interrupted;
-import com.google.protobuf.Descriptors.EnumValueDescriptor;
-import com.google.protobuf.Descriptors.FieldDescriptor;
-import com.google.protobuf.MessageOrBuilder;
-import java.util.Map;
-
-/** Utilities for constructing and managing {@link FailureDetail}s. */
-public class FailureDetailUtil {
-
-  private FailureDetailUtil() {}
-
-  /**
-   * Returns a {@link FailureDetail} specifying {@code code} in its {@link Interrupted} submessage.
-   */
-  public static FailureDetail interrupted(Interrupted.Code code) {
-    return FailureDetail.newBuilder()
-        .setInterrupted(Interrupted.newBuilder().setCode(code))
-        .build();
-  }
-
-  /** Returns the registered {@link ExitCode} associated with a {@link FailureDetail} message. */
-  public static ExitCode getExitCode(FailureDetail failureDetail) {
-    // TODO(mschaller): Consider specializing for unregistered exit codes here, if absolutely
-    //  necessary.
-    int numericExitCode = FailureDetailUtil.getNumericExitCode(failureDetail);
-    return checkNotNull(
-        ExitCode.forCode(numericExitCode), "No ExitCode for numericExitCode %s", numericExitCode);
-  }
-
-  /** Returns the numeric exit code associated with a {@link FailureDetail} message. */
-  private static int getNumericExitCode(FailureDetail failureDetail) {
-    MessageOrBuilder categoryMsg = getCategorySubmessage(failureDetail);
-    EnumValueDescriptor subcategoryDescriptor =
-        getSubcategoryDescriptor(failureDetail, categoryMsg);
-    return getNumericExitCode(subcategoryDescriptor);
-  }
-
-  /**
-   * Returns the numeric exit code associated with a {@link FailureDetail} submessage's subcategory
-   * enum value.
-   */
-  private static int getNumericExitCode(EnumValueDescriptor subcategoryDescriptor) {
-    checkArgument(
-        subcategoryDescriptor.getOptions().hasExtension(FailureDetails.metadata),
-        "Enum value %s has no FailureDetails.metadata",
-        subcategoryDescriptor);
-    return subcategoryDescriptor.getOptions().getExtension(FailureDetails.metadata).getExitCode();
-  }
-
-  /**
-   * Returns the category submessage, i.e. the message in {@link FailureDetail}'s oneof. Throws if
-   * none of those fields are set.
-   */
-  private static MessageOrBuilder getCategorySubmessage(FailureDetail failureDetail) {
-    MessageOrBuilder categoryMsg = null;
-    for (Map.Entry<FieldDescriptor, Object> entry : failureDetail.getAllFields().entrySet()) {
-      FieldDescriptor fieldDescriptor = entry.getKey();
-      if (isCategoryField(fieldDescriptor)) {
-        categoryMsg = (MessageOrBuilder) entry.getValue();
-        break;
-      }
-    }
-    return checkNotNull(
-        categoryMsg, "FailureDetail missing category submessage: %s", failureDetail);
-  }
-
-  /**
-   * Returns whether the {@link FieldDescriptor} describes a field in {@link FailureDetail}'s oneof.
-   *
-   * <p>Uses the field number criteria described in failure_details.proto.
-   */
-  private static boolean isCategoryField(FieldDescriptor fieldDescriptor) {
-    int fieldNum = fieldDescriptor.getNumber();
-    return 100 < fieldNum && fieldNum <= 10_000;
-  }
-
-  /**
-   * Returns the enum value descriptor for the enum field with field number 1 in the {@link
-   * FailureDetail}'s category submessage.
-   */
-  private static EnumValueDescriptor getSubcategoryDescriptor(
-      FailureDetail failureDetail, MessageOrBuilder categoryMsg) {
-    FieldDescriptor fieldNumberOne = categoryMsg.getDescriptorForType().findFieldByNumber(1);
-    checkNotNull(
-        fieldNumberOne, "FailureDetail category submessage has no field #1: %s", failureDetail);
-    Object fieldNumberOneVal = categoryMsg.getField(fieldNumberOne);
-    checkArgument(
-        fieldNumberOneVal instanceof EnumValueDescriptor,
-        "FailureDetail category submessage has non-enum field #1: %s",
-        failureDetail);
-    return (EnumValueDescriptor) fieldNumberOneVal;
-  }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/util/BUILD b/src/test/java/com/google/devtools/build/lib/util/BUILD
index 8a3262a..dccd4d0 100644
--- a/src/test/java/com/google/devtools/build/lib/util/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/util/BUILD
@@ -38,8 +38,8 @@
     deps = [
         "//src/main/java/com/google/devtools/build/lib:build",
         "//src/main/java/com/google/devtools/build/lib:command-utils",
+        "//src/main/java/com/google/devtools/build/lib:detailed_exit_code",
         "//src/main/java/com/google/devtools/build/lib:exitcode-external",
-        "//src/main/java/com/google/devtools/build/lib:failure_detail_util",
         "//src/main/java/com/google/devtools/build/lib:filetype",
         "//src/main/java/com/google/devtools/build/lib:os_util",
         "//src/main/java/com/google/devtools/build/lib:resource-converter",
@@ -57,6 +57,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
         "//src/main/protobuf:failure_details_java_proto",
         "//src/test/java/com/google/devtools/build/lib/testutil",
+        "//src/test/java/com/google/devtools/build/lib/util/subjects",
         "//third_party:guava",
         "//third_party:guava-testlib",
         "//third_party:junit4",
diff --git a/src/test/java/com/google/devtools/build/lib/util/DetailedExitCodeTest.java b/src/test/java/com/google/devtools/build/lib/util/DetailedExitCodeTest.java
new file mode 100644
index 0000000..24411a7
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/util/DetailedExitCodeTest.java
@@ -0,0 +1,40 @@
+// Copyright 2020 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.devtools.build.lib.util;
+
+import static com.google.devtools.build.lib.util.subjects.DetailedExitCodeSubjectFactory.assertThatDetailedExitCode;
+
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Interrupted;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link DetailedExitCode}. */
+@RunWith(JUnit4.class)
+public class DetailedExitCodeTest {
+
+  @Test
+  public void testGetInterruptedExitCode() {
+    assertThatDetailedExitCode(
+            DetailedExitCode.of(
+                FailureDetail.newBuilder()
+                    .setMessage("interrupted")
+                    .setInterrupted(
+                        Interrupted.newBuilder().setCode(Interrupted.Code.INTERRUPTED_UNSPECIFIED))
+                    .build()))
+        .hasExitCode(ExitCode.INTERRUPTED);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/util/FailureDetailUtilTest.java b/src/test/java/com/google/devtools/build/lib/util/FailureDetailUtilTest.java
deleted file mode 100644
index b589ebf..0000000
--- a/src/test/java/com/google/devtools/build/lib/util/FailureDetailUtilTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2020 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.devtools.build.lib.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.devtools.build.lib.server.FailureDetails.Interrupted;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link FailureDetailUtil}. */
-@RunWith(JUnit4.class)
-public class FailureDetailUtilTest {
-
-  @Test
-  public void testGetInterruptedExitCode() {
-    assertThat(
-            FailureDetailUtil.getExitCode(
-                FailureDetailUtil.interrupted(Interrupted.Code.INTERRUPTED_UNSPECIFIED)))
-        .isEqualTo(ExitCode.INTERRUPTED);
-  }
-}