// Copyright 2014 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.skyframe; import static com.google.common.truth.Truth.assertThat; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.skyframe.SkyFunctionException.ReifiedSkyFunctionException; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for the non-trivial creation logic of {@link ErrorInfo}. */ @RunWith(JUnit4.class) public class ErrorInfoTest { /** Dummy SkyFunctionException implementation for the sake of testing. */ private static class DummySkyFunctionException extends SkyFunctionException { private final boolean isCatastrophic; public DummySkyFunctionException(Exception cause, boolean isTransient, boolean isCatastrophic) { super(cause, isTransient ? Transience.TRANSIENT : Transience.PERSISTENT); this.isCatastrophic = isCatastrophic; } @Override public boolean isCatastrophic() { return isCatastrophic; } } private void runTestFromException(boolean isDirectlyTransient, boolean isTransitivelyTransient) { Exception exception = new IOException("ehhhhh"); SkyKey causeOfException = GraphTester.toSkyKey("CAUSE, 1234"); DummySkyFunctionException dummyException = new DummySkyFunctionException(exception, isDirectlyTransient, /*isCatastrophic=*/ false); ErrorInfo errorInfo = ErrorInfo.fromException( new ReifiedSkyFunctionException(dummyException, causeOfException), isTransitivelyTransient); assertThat(errorInfo.getRootCauses()).containsExactly(causeOfException); assertThat(errorInfo.getException()).isSameAs(exception); assertThat(errorInfo.getRootCauseOfException()).isSameAs(causeOfException); assertThat(errorInfo.getCycleInfo()).isEmpty(); assertThat(errorInfo.isDirectlyTransient()).isEqualTo(isDirectlyTransient); assertThat(errorInfo.isTransitivelyTransient()).isEqualTo( isDirectlyTransient || isTransitivelyTransient); assertThat(errorInfo.isCatastrophic()).isFalse(); } @Test public void testFromException_NonTransient() { runTestFromException(/*isDirectlyTransient=*/ false, /*isTransitivelyTransient= */ false); } @Test public void testFromException_DirectlyTransient() { runTestFromException(/*isDirectlyTransient=*/ true, /*isTransitivelyTransient= */ false); } @Test public void testFromException_TransitivelyTransient() { runTestFromException(/*isDirectlyTransient=*/ false, /*isTransitivelyTransient= */ true); } @Test public void testFromException_DirectlyAndTransitivelyTransient() { runTestFromException(/*isDirectlyTransient=*/ true, /*isTransitivelyTransient= */ true); } @Test public void testFromCycle() { CycleInfo cycle = new CycleInfo( ImmutableList.of(GraphTester.toSkyKey("PATH, 1234")), ImmutableList.of(GraphTester.toSkyKey("CYCLE, 4321"))); ErrorInfo errorInfo = ErrorInfo.fromCycle(cycle); assertThat(errorInfo.getRootCauses()).isEmpty(); assertThat(errorInfo.getException()).isNull(); assertThat(errorInfo.getRootCauseOfException()).isNull(); assertThat(errorInfo.isTransitivelyTransient()).isFalse(); assertThat(errorInfo.isCatastrophic()).isFalse(); } @Test public void testFromChildErrors() { CycleInfo cycle = new CycleInfo( ImmutableList.of(GraphTester.toSkyKey("PATH, 1234")), ImmutableList.of(GraphTester.toSkyKey("CYCLE, 4321"))); ErrorInfo cycleErrorInfo = ErrorInfo.fromCycle(cycle); Exception exception1 = new IOException("ehhhhh"); SkyKey causeOfException1 = GraphTester.toSkyKey("CAUSE1, 1234"); DummySkyFunctionException dummyException1 = new DummySkyFunctionException(exception1, /*isTransient=*/ true, /*isCatastrophic=*/ false); ErrorInfo exceptionErrorInfo1 = ErrorInfo.fromException( new ReifiedSkyFunctionException(dummyException1, causeOfException1), /*isTransitivelyTransient=*/ false); // N.B this ErrorInfo will be catastrophic. Exception exception2 = new IOException("blahhhhh"); SkyKey causeOfException2 = GraphTester.toSkyKey("CAUSE2, 5678"); DummySkyFunctionException dummyException2 = new DummySkyFunctionException(exception2, /*isTransient=*/ false, /*isCatastrophic=*/ true); ErrorInfo exceptionErrorInfo2 = ErrorInfo.fromException( new ReifiedSkyFunctionException(dummyException2, causeOfException2), /*isTransitivelyTransient=*/ false); SkyKey currentKey = GraphTester.toSkyKey("CURRENT, 9876"); ErrorInfo errorInfo = ErrorInfo.fromChildErrors( currentKey, ImmutableList.of(cycleErrorInfo, exceptionErrorInfo1, exceptionErrorInfo2)); assertThat(errorInfo.getRootCauses()).containsExactly(causeOfException1, causeOfException2); // For simplicity we test the current implementation detail that we choose the first non-null // (exception, cause) pair that we encounter. This isn't necessarily a requirement of the // interface, but it makes the test convenient and is a way to document the current behavior. assertThat(errorInfo.getException()).isSameAs(exception1); assertThat(errorInfo.getRootCauseOfException()).isSameAs(causeOfException1); assertThat(errorInfo.getCycleInfo()).containsExactly( new CycleInfo( ImmutableList.of(currentKey, Iterables.getOnlyElement(cycle.getPathToCycle())), cycle.getCycle())); assertThat(errorInfo.isTransitivelyTransient()).isTrue(); assertThat(errorInfo.isCatastrophic()).isTrue(); } @Test public void testCannotCreateErrorInfoWithoutExceptionOrCycle() { try { new ErrorInfo( NestedSetBuilder.emptySet(Order.COMPILE_ORDER), /*exception=*/ null, /*rootCauseOfException=*/ null, ImmutableList.of(), false, false, false); } catch (IllegalStateException e) { // Brittle, but confirms we failed for the right reason. assertThat(e) .hasMessage("At least one of exception and cycles must be non-null/empty, respectively"); } } @Test public void testCannotCreateErrorInfoWithExceptionButNoRootCause() { try { new ErrorInfo( NestedSetBuilder.emptySet(Order.COMPILE_ORDER), new IOException("foo"), /*rootCauseOfException=*/ null, ImmutableList.of(), false, false, false); } catch (IllegalStateException e) { // Brittle, but confirms we failed for the right reason. assertThat(e) .hasMessageThat() .startsWith("exception and rootCauseOfException must both be null or non-null"); } } }