blob: 09e50ae34cef31ac9181e98e06272dcb3d826171 [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.bugreport;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.bugreport.BugReport.BlazeRuntimeInterface;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.EventKind;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Crash.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.util.CustomExitCodePublisher;
import com.google.devtools.build.lib.util.CustomFailureDetailPublisher;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.protobuf.ExtensionRegistry;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
/** Tests for {@link BugReport}. */
@RunWith(TestParameterInjector.class)
public final class BugReportTest {
private enum CrashType {
CRASH(ExitCode.BLAZE_INTERNAL_ERROR, Code.CRASH_UNKNOWN) {
@Override
Throwable createThrowable() {
return new IllegalStateException("Crashed");
}
},
OOM(ExitCode.OOM_ERROR, Code.CRASH_OOM) {
@Override
Throwable createThrowable() {
return new OutOfMemoryError("Java heap space");
}
};
private final ExitCode expectedExitCode;
private final Code expectedFailureDetailCode;
CrashType(ExitCode expectedExitCode, Code expectedFailureDetailCode) {
this.expectedExitCode = expectedExitCode;
this.expectedFailureDetailCode = expectedFailureDetailCode;
}
abstract Throwable createThrowable();
}
@Rule public final TemporaryFolder tmp = new TemporaryFolder();
private final BlazeRuntimeInterface mockRuntime = mock(BlazeRuntimeInterface.class);
@TestParameter private CrashType crashType;
private Path exitCodeFile;
private Path failureDetailFile;
@Before
public void setup() throws Exception {
when(mockRuntime.getProductName()).thenReturn("myProductName");
BugReport.setRuntime(mockRuntime);
exitCodeFile = tmp.newFolder().toPath().resolve("exit_code_to_use_on_abrupt_exit");
failureDetailFile = tmp.newFolder().toPath().resolve("failure_detail");
CustomExitCodePublisher.setAbruptExitStatusFileDir(exitCodeFile.getParent().toString());
CustomFailureDetailPublisher.setFailureDetailFilePath(failureDetailFile.toString());
}
@After
public void resetPublishers() {
CustomExitCodePublisher.resetAbruptExitStatusFile();
CustomFailureDetailPublisher.resetFailureDetailFilePath();
}
@Test
public void convenienceMethod() throws Exception {
Throwable t = crashType.createThrowable();
FailureDetail expectedFailureDetail =
createExpectedFailureDetail(t, crashType.expectedFailureDetailCode);
assertThrows(SecurityException.class, () -> BugReport.handleCrash(t));
assertThrows(t.getClass(), BugReport::maybePropagateUnprocessedThrowableIfInTest);
verify(mockRuntime)
.cleanUpForCrash(DetailedExitCode.of(crashType.expectedExitCode, expectedFailureDetail));
verifyExitCodeWritten(crashType.expectedExitCode.getNumericExitCode());
verifyFailureDetailWritten(expectedFailureDetail);
}
@Test
public void halt() throws Exception {
Throwable t = crashType.createThrowable();
FailureDetail expectedFailureDetail =
createExpectedFailureDetail(t, crashType.expectedFailureDetailCode);
assertThrows(
SecurityException.class, () -> BugReport.handleCrash(Crash.from(t), CrashContext.halt()));
assertThrows(t.getClass(), BugReport::maybePropagateUnprocessedThrowableIfInTest);
verify(mockRuntime)
.cleanUpForCrash(DetailedExitCode.of(crashType.expectedExitCode, expectedFailureDetail));
verifyExitCodeWritten(crashType.expectedExitCode.getNumericExitCode());
verifyFailureDetailWritten(expectedFailureDetail);
}
@Test
public void keepAlive() throws Exception {
Throwable t = crashType.createThrowable();
FailureDetail expectedFailureDetail =
createExpectedFailureDetail(t, crashType.expectedFailureDetailCode);
BugReport.handleCrash(Crash.from(t), CrashContext.keepAlive());
assertThrows(t.getClass(), BugReport::maybePropagateUnprocessedThrowableIfInTest);
verify(mockRuntime)
.cleanUpForCrash(DetailedExitCode.of(crashType.expectedExitCode, expectedFailureDetail));
verifyNoExitCodeWritten();
verifyFailureDetailWritten(expectedFailureDetail);
}
@Test
public void customContext_setUpFront() {
Throwable t = crashType.createThrowable();
EventHandler handler = mock(EventHandler.class);
ArgumentCaptor<Event> event = ArgumentCaptor.forClass(Event.class);
BugReport.handleCrash(
Crash.from(t),
CrashContext.keepAlive().withExtraOomInfo("Build fewer targets!").reportingTo(handler));
assertThrows(t.getClass(), BugReport::maybePropagateUnprocessedThrowableIfInTest);
verify(handler).handle(event.capture());
assertThat(event.getValue().getKind()).isEqualTo(EventKind.FATAL);
assertThat(event.getValue().getMessage()).contains(Throwables.getStackTraceAsString(t));
if (crashType == CrashType.OOM) {
assertThat(event.getValue().getMessage()).contains("Build fewer targets!");
} else {
assertThat(event.getValue().getMessage()).doesNotContain("Build fewer targets!");
}
}
@Test
public void customContext_filledInByRuntime() {
Throwable t = crashType.createThrowable();
EventHandler handler = mock(EventHandler.class);
ArgumentCaptor<Event> event = ArgumentCaptor.forClass(Event.class);
doAnswer(
inv ->
inv.getArgument(0, CrashContext.class)
.withExtraOomInfo("Build fewer targets!")
.reportingTo(handler))
.when(mockRuntime)
.fillInCrashContext(any());
BugReport.handleCrash(Crash.from(t), CrashContext.keepAlive());
assertThrows(t.getClass(), BugReport::maybePropagateUnprocessedThrowableIfInTest);
verify(handler).handle(event.capture());
assertThat(event.getValue().getKind()).isEqualTo(EventKind.FATAL);
assertThat(event.getValue().getMessage()).contains(Throwables.getStackTraceAsString(t));
if (crashType == CrashType.OOM) {
assertThat(event.getValue().getMessage()).contains("Build fewer targets!");
} else {
assertThat(event.getValue().getMessage()).doesNotContain("Build fewer targets!");
}
}
private void verifyExitCodeWritten(int exitCode) throws Exception {
assertThat(Files.readAllLines(exitCodeFile)).containsExactly(String.valueOf(exitCode));
}
private void verifyNoExitCodeWritten() {
assertThat(exitCodeFile.toFile().exists()).isFalse();
}
private void verifyFailureDetailWritten(FailureDetail expected) throws Exception {
assertThat(
FailureDetail.parseFrom(
Files.readAllBytes(failureDetailFile), ExtensionRegistry.getEmptyRegistry()))
.isEqualTo(expected);
}
private static FailureDetail createExpectedFailureDetail(
Throwable t, Code expectedFailureDetailCode) {
return FailureDetail.newBuilder()
.setMessage(String.format("Crashed: (%s) %s", t.getClass().getName(), t.getMessage()))
.setCrash(
FailureDetails.Crash.newBuilder()
.setCode(expectedFailureDetailCode)
.addCauses(
FailureDetails.Throwable.newBuilder()
.setThrowableClass(t.getClass().getName())
.setMessage(t.getMessage())
.addAllStackTrace(
Lists.transform(
Arrays.asList(t.getStackTrace()), StackTraceElement::toString))))
.build();
}
}