blob: 61273ad8420c11775ae65b7d09299e8faee17c0d [file] [log] [blame]
// 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 static com.google.common.truth.Truth8.assertThat;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Crash;
import com.google.devtools.build.lib.server.FailureDetails.Crash.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.protobuf.ProtocolStringList;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.util.List;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link CrashFailureDetails}. */
@RunWith(TestParameterInjector.class)
public final class CrashFailureDetailsTest {
private static final String TEST_EXCEPTION_NAME =
"com.google.devtools.build.lib.util.CrashFailureDetailsTest$TestException";
@After
public void restoreDefaultOomDetector() {
CrashFailureDetails.setOomDetector(() -> false);
}
@Test
public void nestedThrowables() {
// This test confirms that throwables' details are recorded: their messages, types, stack
// frames, and causes. The outermost throwable is recorded at index 0.
FailureDetail failureDetail =
CrashFailureDetails.forThrowable(
functionForStackFrameTests_A(functionForStackFrameTests_B()));
assertThat(failureDetail.getMessage())
.isEqualTo(
String.format(
"Crashed: (%s) myMessage_A, (%s) myMessage_B",
TEST_EXCEPTION_NAME, TEST_EXCEPTION_NAME));
assertThat(failureDetail.hasCrash()).isTrue();
Crash crash = failureDetail.getCrash();
assertThat(crash.getCode()).isEqualTo(Code.CRASH_UNKNOWN);
assertThat(crash.getOomDetectorOverride()).isFalse();
assertThat(crash.getCausesCount()).isEqualTo(2);
FailureDetails.Throwable outerCause = crash.getCauses(0);
assertThat(outerCause.getMessage()).isEqualTo("myMessage_A");
assertThat(outerCause.getThrowableClass()).isEqualTo(TEST_EXCEPTION_NAME);
assertThat(outerCause.getStackTraceCount()).isAtLeast(2);
assertThat(outerCause.getStackTrace(0))
.contains(
"com.google.devtools.build.lib.util.CrashFailureDetailsTest."
+ "functionForStackFrameTests_A");
assertThat(outerCause.getStackTrace(1))
.contains("com.google.devtools.build.lib.util.CrashFailureDetailsTest.nestedThrowables");
FailureDetails.Throwable innerCause = crash.getCauses(1);
assertThat(innerCause.getMessage()).isEqualTo("myMessage_B");
assertThat(innerCause.getThrowableClass()).isEqualTo(TEST_EXCEPTION_NAME);
assertThat(innerCause.getStackTraceCount()).isAtLeast(2);
assertThat(innerCause.getStackTrace(0))
.contains(
"com.google.devtools.build.lib.util.CrashFailureDetailsTest."
+ "functionForStackFrameTests_B");
assertThat(innerCause.getStackTrace(1))
.contains("com.google.devtools.build.lib.util.CrashFailureDetailsTest.nestedThrowables");
}
@Test
public void causeLimit() {
// This test confirms that at most 5 throwables are recorded.
TestException inner5 = new TestException("inner5");
TestException inner4 = new TestException("inner4", inner5);
TestException inner3 = new TestException("inner3", inner4);
TestException inner2 = new TestException("inner2", inner3);
TestException inner1 = new TestException("inner1", inner2);
TestException outer = new TestException("outer", inner1);
assertThat(CrashFailureDetails.forThrowable(outer).getCrash().getCausesCount()).isEqualTo(5);
}
@Test
public void testMessageLimit() {
TestException exception = new TestException("x".repeat(5000));
String crashMessage =
CrashFailureDetails.forThrowable(exception).getCrash().getCauses(0).getMessage();
assertThat(crashMessage).hasLength(2000);
assertThat(crashMessage).endsWith("[truncated]");
}
@Test
public void causeCycle() {
// This test confirms that throwables in a cause cycle are visited at most once.
TestException inner2 = new TestException("inner2");
TestException inner1 = new TestException("inner1", inner2);
TestException outer = new TestException("outer", inner1);
inner2.initCause(inner1);
List<FailureDetails.Throwable> causesList =
CrashFailureDetails.forThrowable(outer).getCrash().getCausesList();
assertThat(causesList.stream().map(FailureDetails.Throwable::getMessage))
.containsExactly("outer", "inner1", "inner2")
.inOrder();
}
@Test
public void deepStack() {
ProtocolStringList stackTraceList =
CrashFailureDetails.forThrowable(functionForDeepStackTrace(1001))
.getCrash()
.getCauses(0)
.getStackTraceList();
assertThat(stackTraceList).hasSize(1000);
// Check that the deepest 1000 frames were recorded:
for (String stackFrame : stackTraceList) {
assertThat(stackFrame).contains("CrashFailureDetailsTest.functionForDeepStackTrace");
}
}
@Test
public void detailedExitConstruction_oom() {
var detailedExitCode = CrashFailureDetails.detailedExitCodeForThrowable(new OutOfMemoryError());
assertThat(detailedExitCode.getExitCode()).isEqualTo(ExitCode.OOM_ERROR);
assertThat(detailedExitCode.getFailureDetail().getCrash().getOomDetectorOverride()).isFalse();
}
@Test
public void detailedExitConstruction_wrappedOom() {
var detailedExitCode =
CrashFailureDetails.detailedExitCodeForThrowable(
new IllegalStateException(new OutOfMemoryError()));
assertThat(detailedExitCode.getExitCode()).isEqualTo(ExitCode.OOM_ERROR);
assertThat(detailedExitCode.getFailureDetail().getCrash().getOomDetectorOverride()).isFalse();
}
@Test
public void detailedExitConstruction_otherCrash() {
var detailedExitCode =
CrashFailureDetails.detailedExitCodeForThrowable(new IllegalStateException());
assertThat(detailedExitCode.getExitCode()).isEqualTo(ExitCode.BLAZE_INTERNAL_ERROR);
assertThat(detailedExitCode.getFailureDetail().getCrash().getOomDetectorOverride()).isFalse();
}
private enum ThrowableType {
OUT_OF_MEMORY_ERROR(new OutOfMemoryError()),
ILLEGAL_STATE_EXCEPTION(new IllegalStateException());
ThrowableType(Throwable throwable) {
this.throwable = throwable;
}
@SuppressWarnings("ImmutableEnumChecker")
final Throwable throwable;
}
@Test
public void detailExitConstruction_crashWithOomDetector_returnsOomCrash(
@TestParameter ThrowableType throwableType) {
CrashFailureDetails.setOomDetector(() -> true);
var detailedExitCode =
CrashFailureDetails.detailedExitCodeForThrowable(throwableType.throwable);
assertThat(detailedExitCode.getExitCode()).isEqualTo(ExitCode.OOM_ERROR);
if (throwableType == ThrowableType.OUT_OF_MEMORY_ERROR) {
assertThat(detailedExitCode.getFailureDetail().getCrash().getOomDetectorOverride()).isFalse();
} else {
assertThat(detailedExitCode.getFailureDetail().getCrash().getOomDetectorOverride()).isTrue();
}
}
private static TestException functionForStackFrameTests_A(TestException cause) {
return new TestException("myMessage_A", cause);
}
private static TestException functionForStackFrameTests_B() {
return new TestException("myMessage_B");
}
private static TestException functionForDeepStackTrace(int framesToBuild) {
if (framesToBuild <= 1) {
return new TestException("myMessage_deep");
} else {
return functionForDeepStackTrace(framesToBuild - 1);
}
}
private static class TestException extends Exception {
private TestException(String message) {
super(message);
}
private TestException(String message, Throwable cause) {
super(message, cause);
}
}
}