| // 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.buildtool; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.assertThrows; |
| |
| import com.google.common.util.concurrent.Futures; |
| import com.google.devtools.build.lib.actions.BuildFailedException; |
| import com.google.devtools.build.lib.testutil.Suite; |
| import com.google.devtools.build.lib.testutil.TestSpec; |
| import com.google.devtools.build.lib.testutil.TestUtils; |
| import com.google.devtools.build.lib.util.LoggingUtil; |
| import com.google.devtools.build.lib.vfs.Path; |
| import java.io.IOException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.logging.Logger; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Test case for crashes caused by exceptions happening during package lookups during the |
| * execution phase under certain conditions. To trigger a failure we must: |
| * |
| * <ul> |
| * <li>have a populated action cache on disk but no in memory state (ie: build after restart)</li> |
| * <li>have a build that does include scanning where the includes are not explicitly declared |
| * and reside in a directory below a package (common)</li> |
| * <li>have some sort of filesystem event that would trigger exceptions between the loading |
| * and execution phase (ie: network disconnecting, credentials expiring, etc)</li> |
| * </ul> |
| */ |
| @TestSpec(size = Suite.MEDIUM_TESTS) |
| @RunWith(JUnit4.class) |
| public class ExecutionPhaseContainingPackageLookupTest extends IoHookTestCase { |
| |
| @Test |
| public void testIOExceptionDuringExecutionPhaseContainingPackageLookup() throws Exception { |
| // Our main code that includes the include we'll be scanning for during the exec phase. |
| write( |
| "foo/BUILD", |
| "cc_library(name = 'foo', srcs = ['foo.cc'], deps = ['//bar', '//notfinished'])"); |
| write( |
| "foo/foo.cc", |
| "#include \"bar/includes/include.h\"", |
| "#include \"notfinished/includes/include.h\"", |
| "int main() { return -1; }"); |
| |
| // Dummy package containing a not immediately obvious include. |
| write("bar/BUILD", |
| "cc_library(name = 'bar', srcs = [], hdrs_check = 'loose',", |
| " visibility = ['//visibility:public'])"); |
| write("bar/includes/include.h"); |
| |
| // Package that won't have its BUILD file looked up when the second build aborts. |
| write( |
| "notfinished/BUILD", |
| "cc_library(name = 'notfinished', srcs = [], hdrs_check = 'loose',", |
| " visibility = ['//visibility:public'])"); |
| write("notfinished/includes/include.h"); |
| |
| // Build and toss the analysis cache, imperative to make sure we go to disk in |
| // the next build. |
| addOptions("--discard_analysis_cache", "--features=cc_include_scanning"); |
| buildTarget("//foo:foo"); |
| |
| final AtomicBoolean interrupted = new AtomicBoolean(true); |
| |
| // GoogleBuildIntegrationTestCase installs a remote logger that calls Runtime.halt, and |
| // SkyframeBuilder calls logToRemote when there's a BuildFileNotFoundException, and the code |
| // below triggers a BuildFileNotFoundException. Previously, the ActionExecutionFunction was |
| // rethrowing that exception as a ActionExecutionFunctionException during error bubbling, but |
| // it now ignores it, which triggers the code path in SkyframeBuilder. However, the test runner |
| // catches Runtime.halt, and throws an exception instead. We just overwrite the remote logger |
| // with one that silently ignores the log. |
| LoggingUtil.installRemoteLoggerForTesting(Futures.immediateFuture(Logger.getAnonymousLogger())); |
| |
| // Now we want any lookup for "bar/includes/BUILD" to throw an IOException. |
| setListener( |
| new FileListener() { |
| @Override |
| public void handle(PathOp op, Path path) throws IOException { |
| if (path.getPathString().contains("notfinished/includes/BUILD")) { |
| // Make sure we don't have the PackageLookup results for notfinished/includes before |
| // we throw an IOException below. This tests that we'll still throw the proper |
| // exception even without all the deps being present. |
| try { |
| Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS); |
| interrupted.set(false); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| } else if (path.getPathString().contains("bar/includes/BUILD")) { |
| throw new IOException("FAIIIIIILLLLLLLLL"); |
| } |
| } |
| }); |
| |
| BuildFailedException bfe = |
| assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo")); |
| assertThat(bfe).hasMessageThat().contains("no such package 'bar/includes'"); |
| assertThat(interrupted.get()).isTrue(); |
| } |
| |
| } |