// 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);
    }
  }
}
