| // 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 com.google.common.truth.Truth.assertWithMessage; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static java.util.concurrent.TimeUnit.SECONDS; | |
| import static org.junit.Assert.fail; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Iterables; | |
| import com.google.common.collect.Maps; | |
| import com.google.devtools.build.lib.actions.FileValue; | |
| import com.google.devtools.build.lib.analysis.BlazeDirectories; | |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; | |
| import com.google.devtools.build.lib.events.EventKind; | |
| import com.google.devtools.build.lib.events.util.EventCollectionApparatus; | |
| import com.google.devtools.build.lib.query2.proto.proto2api.Build; | |
| import com.google.devtools.build.lib.query2.proto.proto2api.Build.QueryResult; | |
| import com.google.devtools.build.lib.query2.query.output.QueryOptions; | |
| import com.google.devtools.build.lib.runtime.BlazeCommandResult; | |
| import com.google.devtools.build.lib.runtime.BlazeModule; | |
| import com.google.devtools.build.lib.runtime.BlazeRuntime; | |
| import com.google.devtools.build.lib.runtime.CommandEnvironment; | |
| import com.google.devtools.build.lib.runtime.GotOptionsEvent; | |
| import com.google.devtools.build.lib.runtime.WorkspaceBuilder; | |
| import com.google.devtools.build.lib.runtime.commands.QueryCommand; | |
| import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; | |
| import com.google.devtools.build.lib.server.FailureDetails; | |
| import com.google.devtools.build.lib.skyframe.DefaultSyscallCache; | |
| import com.google.devtools.build.lib.skyframe.SkyFunctions; | |
| import com.google.devtools.build.lib.testutil.TestConstants; | |
| import com.google.devtools.build.lib.testutil.TestUtils; | |
| import com.google.devtools.build.lib.unix.UnixFileSystem; | |
| import com.google.devtools.build.lib.util.ExitCode; | |
| import com.google.devtools.build.lib.vfs.DigestHashFunction; | |
| import com.google.devtools.build.lib.vfs.FileStateKey; | |
| import com.google.devtools.build.lib.vfs.FileStatus; | |
| import com.google.devtools.build.lib.vfs.FileSystem; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.RootedPath; | |
| import com.google.devtools.build.lib.vfs.SyscallCache; | |
| import com.google.devtools.build.skyframe.NotifyingHelper; | |
| import com.google.devtools.build.skyframe.TrackingAwaiter; | |
| import com.google.devtools.common.options.OptionsParsingResult; | |
| import com.google.protobuf.ExtensionRegistry; | |
| import com.google.testing.junit.testparameterinjector.TestParameter; | |
| import com.google.testing.junit.testparameterinjector.TestParameterInjector; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.IOException; | |
| import java.nio.charset.Charset; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import javax.annotation.Nullable; | |
| import javax.xml.parsers.DocumentBuilderFactory; | |
| import javax.xml.xpath.XPathConstants; | |
| import javax.xml.xpath.XPathExpression; | |
| import javax.xml.xpath.XPathFactory; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.w3c.dom.Document; | |
| import org.w3c.dom.Element; | |
| import org.w3c.dom.Node; | |
| import org.w3c.dom.NodeList; | |
| /** Integration tests for 'blaze query'. */ | |
| @RunWith(TestParameterInjector.class) | |
| public class QueryIntegrationTest extends BuildIntegrationTestCase { | |
| private final CustomFileSystem fs = new CustomFileSystem(); | |
| private final SyscallCache syscallCache = DefaultSyscallCache.newBuilder().build(); | |
| private final List<String> options = new ArrayList<>(); | |
| @Override | |
| protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception { | |
| return super.getRuntimeBuilder() | |
| .addBlazeModule( | |
| new BlazeModule() { | |
| @Override | |
| public void workspaceInit( | |
| BlazeRuntime runtime, BlazeDirectories directories, WorkspaceBuilder builder) { | |
| builder.setSyscallCache(syscallCache); | |
| } | |
| }); | |
| } | |
| @Override | |
| protected EventCollectionApparatus createEvents() { | |
| ImmutableSet.Builder<EventKind> eventsSet = ImmutableSet.builder(); | |
| eventsSet.addAll(EventKind.ERRORS_AND_WARNINGS_AND_OUTPUT); | |
| eventsSet.add(EventKind.PROGRESS); | |
| return new EventCollectionApparatus(eventsSet.build()); | |
| } | |
| private static class CustomFileSystem extends UnixFileSystem { | |
| final Map<PathFragment, FileStatus> stubbedStats = new HashMap<>(); | |
| final Map<PathFragment, Runnable> watchedPaths = Maps.newConcurrentMap(); | |
| CustomFileSystem() { | |
| super(DigestHashFunction.SHA256, ""); | |
| } | |
| void stubStat(Path path, @Nullable FileStatus stubbedResult) { | |
| stubbedStats.put(path.asFragment(), stubbedResult); | |
| } | |
| @Override | |
| public FileStatus statIfFound(PathFragment path, boolean followSymlinks) throws IOException { | |
| Runnable runnable = watchedPaths.get(path); | |
| if (runnable != null) { | |
| runnable.run(); | |
| } | |
| if (stubbedStats.containsKey(path)) { | |
| return stubbedStats.get(path); | |
| } | |
| return super.statIfFound(path, followSymlinks); | |
| } | |
| } | |
| private static class QueryOutput { | |
| private final BlazeCommandResult blazeCommandResult; | |
| private final byte[] stdout; | |
| public QueryOutput(BlazeCommandResult blazeCommandResult, byte[] stdout) { | |
| this.blazeCommandResult = blazeCommandResult; | |
| this.stdout = stdout; | |
| } | |
| public BlazeCommandResult getBlazeCommandResult() { | |
| return blazeCommandResult; | |
| } | |
| public byte[] getStdout() { | |
| return stdout; | |
| } | |
| } | |
| private static class ProtoQueryOutput { | |
| private final QueryResult queryResult; | |
| private final QueryOutput queryOutput; | |
| public ProtoQueryOutput(QueryOutput queryOutput, QueryResult queryResult) { | |
| this.queryResult = queryResult; | |
| this.queryOutput = queryOutput; | |
| } | |
| public QueryResult getQueryResult() { | |
| return queryResult; | |
| } | |
| public QueryOutput getQueryOutput() { | |
| return queryOutput; | |
| } | |
| } | |
| @Override | |
| protected FileSystem createFileSystem() { | |
| return fs; | |
| } | |
| @Before | |
| public final void setQueryOptions() { | |
| runtimeWrapper.addOptionsClass(QueryOptions.class); | |
| } | |
| // Number large enough that an unordered collection with this many elements will never happen to | |
| // iterate over them in their "natural" order. | |
| private static final int NUM_DEPS = 1000; | |
| private static void assertSameElementsDifferentOrder(List<String> actual, List<String> expected) { | |
| assertThat(actual).containsExactlyElementsIn(expected); | |
| int i = 0; | |
| for (; i < expected.size(); i++) { | |
| if (!actual.get(i).equals(expected.get(i))) { | |
| break; | |
| } | |
| } | |
| assertWithMessage("Lists should not have been in same order") | |
| .that(i < expected.size()) | |
| .isTrue(); | |
| } | |
| private static List<String> getTargetNames(QueryResult result) { | |
| List<String> results = new ArrayList<>(); | |
| for (Build.Target target : result.getTargetList()) { | |
| results.add(target.getRule().getName()); | |
| } | |
| return results; | |
| } | |
| @Test | |
| public void testProtoUnorderedAndOrdered() throws Exception { | |
| List<String> expected = new ArrayList<>(NUM_DEPS + 1); | |
| String targets = ""; | |
| String depString = ""; | |
| for (int i = 0; i < NUM_DEPS; i++) { | |
| String dep = Integer.toString(i); | |
| depString += "'" + dep + "', "; | |
| expected.add("//foo:" + dep); | |
| targets += "sh_library(name = '" + dep + "')\n"; | |
| } | |
| expected.add("//foo:a"); | |
| Collections.sort(expected, Collections.reverseOrder()); | |
| write("foo/BUILD", "sh_library(name = 'a', deps = [" + depString + "])", targets); | |
| ProtoQueryOutput result = getProtoQueryResult("deps(//foo:a)"); | |
| assertSameElementsDifferentOrder(getTargetNames(result.getQueryResult()), expected); | |
| options.add("--order_output=full"); | |
| result = getProtoQueryResult("deps(//foo:a)"); | |
| assertThat(getTargetNames(result.getQueryResult())) | |
| .containsExactlyElementsIn(expected) | |
| .inOrder(); | |
| } | |
| /** | |
| * Test that {min,max}rank work as expected with ordering. Since minrank and maxrank have special | |
| * handling for cycles in the graph, we put a cycle in to exercise that code. | |
| */ | |
| private void assertRankUnorderedAndOrdered(boolean minRank) throws Exception { | |
| List<String> expected = new ArrayList<>(2 * NUM_DEPS + 1); | |
| // The build file looks like: | |
| // sh_library(name = 'a', deps = ['cycle1', '1', '2', ..., ] | |
| // sh_library(name = '1') | |
| // ... | |
| // sh_library(name = 'n') | |
| // sh_library(name = 'cycle0', deps = ['cyclen']) | |
| // sh_library(name = 'cycle1', deps = ['cycle0']) | |
| // ... | |
| // sh_library(name = 'cyclen', deps = ['cycle{n-1}']) | |
| String targets = ""; | |
| String depString = ""; | |
| for (int i = 0; i < NUM_DEPS; i++) { | |
| String dep = Integer.toString(i); | |
| depString += "'" + dep + "', "; | |
| expected.add("1 //foo:" + dep); | |
| expected.add("1 //foo:cycle" + dep); | |
| targets += "sh_library(name = '" + dep + "')\n"; | |
| targets += "sh_library(name = 'cycle" + dep + "', deps = ['cycle"; | |
| if (i > 0) { | |
| targets += i - 1; | |
| } else { | |
| targets += NUM_DEPS - 1; | |
| } | |
| targets += "'])\n"; | |
| } | |
| Collections.sort(expected); | |
| expected.add(0, "0 //foo:a"); | |
| options.add("--output=" + (minRank ? "minrank" : "maxrank")); | |
| options.add("--keep_going"); | |
| write("foo/BUILD", "sh_library(name = 'a', deps = ['cycle0', " + depString + "])", targets); | |
| List<String> result = getStringQueryResult("deps(//foo:a)"); | |
| assertWithMessage(result.toString()).that(result.get(0)).isEqualTo("0 //foo:a"); | |
| assertSameElementsDifferentOrder(result, expected); | |
| options.add("--order_output=full"); | |
| result = getStringQueryResult("deps(//foo:a)"); | |
| assertWithMessage(result.toString()).that(result.get(0)).isEqualTo("0 //foo:a"); | |
| assertThat(result).containsExactlyElementsIn(expected).inOrder(); | |
| } | |
| @Test | |
| public void testMinrankUnorderedAndOrdered() throws Exception { | |
| assertRankUnorderedAndOrdered(true); | |
| } | |
| @Test | |
| public void testMaxrankUnorderedAndOrdered() throws Exception { | |
| assertRankUnorderedAndOrdered(false); | |
| } | |
| @Test | |
| public void testLabelOrderedFullAndDeps() throws Exception { | |
| List<String> expected = new ArrayList<>(NUM_DEPS + 1); | |
| String targets = ""; | |
| String depString = ""; | |
| for (int i = 0; i < NUM_DEPS; i++) { | |
| String dep = Integer.toString(i); | |
| depString += "'" + dep + "', "; | |
| expected.add("//foo:" + dep); | |
| targets += "sh_library(name = '" + dep + "')\n"; | |
| } | |
| expected.add("//foo:a"); | |
| Collections.sort(expected); | |
| write("foo/BUILD", "sh_library(name = 'a', deps = [" + depString + "])", targets); | |
| List<String> result = getStringQueryResult("deps(//foo:a)"); | |
| assertThat(result).containsExactlyElementsIn(expected).inOrder(); | |
| options.add("--order_output=deps"); | |
| result = getStringQueryResult("deps(//foo:a)"); | |
| assertSameElementsDifferentOrder(result, expected); | |
| } | |
| @Test | |
| public void testInputFileElementContainsPackageGroups() throws Exception { | |
| write( | |
| "fruit/BUILD", | |
| "package_group(name='coconut', packages=['//fruit/walnut'])", | |
| "exports_files(['chestnut'], visibility=[':coconut'])"); | |
| Document result = getXmlQueryResult("//fruit:chestnut"); | |
| Element resultNode = getResultNode(result, "//fruit:chestnut"); | |
| assertThat( | |
| Iterables.getOnlyElement( | |
| xpathSelect(resultNode, "package-group[@name='//fruit:coconut']"))) | |
| .isNotNull(); | |
| } | |
| @Test | |
| public void testNonStrictTests() throws Exception { | |
| write( | |
| "donut/BUILD", | |
| "sh_binary(name = 'thief', srcs = ['thief.sh'])", | |
| "cc_test(name = 'shop', srcs = ['shop.cc'])", | |
| "test_suite(name = 'cop', tests = [':thief', ':shop'])"); | |
| // This should not throw an exception, and return 0 targets. | |
| ProtoQueryOutput result = getProtoQueryResult("tests(//donut:cop)"); | |
| QueryResult queryResult = result.getQueryResult(); | |
| assertThat(queryResult.getTargetCount()).isEqualTo(1); | |
| assertThat(queryResult.getTarget(0).getRule().getName()).isEqualTo("//donut:shop"); | |
| } | |
| @Test | |
| public void testStrictTests() throws Exception { | |
| options.add("--strict_test_suite=true"); | |
| write( | |
| "donut/BUILD", | |
| "sh_binary(name = 'thief', srcs = ['thief.sh'])", | |
| "test_suite(name = 'cop', tests = [':thief'])"); | |
| ProtoQueryOutput result = getProtoQueryResult("tests(//donut:cop)"); | |
| BlazeCommandResult blazeCommandResult = result.getQueryOutput().getBlazeCommandResult(); | |
| assertExitCode(result.getQueryOutput(), ExitCode.ANALYSIS_FAILURE); | |
| assertThat(blazeCommandResult.getFailureDetail().getMessage()) | |
| .contains( | |
| "The label '//donut:thief' in the test_suite " | |
| + "'//donut:cop' does not refer to a test"); | |
| } | |
| private void createBadBarBuild() throws IOException { | |
| Path barBuildFile = write("bar/BUILD", "sh_library(name = 'bar/baz')"); | |
| FileStatus inconsistentFileStatus = | |
| new FileStatus() { | |
| @Override | |
| public boolean isFile() { | |
| return false; | |
| } | |
| @Override | |
| public boolean isSpecialFile() { | |
| return false; | |
| } | |
| @Override | |
| public boolean isDirectory() { | |
| return false; | |
| } | |
| @Override | |
| public boolean isSymbolicLink() { | |
| return false; | |
| } | |
| @Override | |
| public long getSize() { | |
| return 0; | |
| } | |
| @Override | |
| public long getLastModifiedTime() { | |
| return 0; | |
| } | |
| @Override | |
| public long getLastChangeTime() { | |
| return 0; | |
| } | |
| @Override | |
| public long getNodeId() { | |
| return 0; | |
| } | |
| }; | |
| fs.stubStat(barBuildFile, inconsistentFileStatus); | |
| } | |
| // Regression test for b/14248208. | |
| private void runInconsistentFileSystem(boolean keepGoing) throws Exception { | |
| createBadBarBuild(); | |
| if (keepGoing) { | |
| options.add("--keep_going"); | |
| } | |
| QueryOutput result = getQueryResult("deps(//bar:baz)"); | |
| assertExitCode(result, ExitCode.ANALYSIS_FAILURE); | |
| events.assertContainsError("Inconsistent filesystem operations"); | |
| assertThat(events.errors()).hasSize(1); | |
| } | |
| @Test | |
| public void inconsistentFileSystemKeepGoing() throws Exception { | |
| runInconsistentFileSystem(/*keepGoing=*/ true); | |
| } | |
| @Test | |
| public void inconsistentFileSystemNoKeepGoing() throws Exception { | |
| runInconsistentFileSystem(/*keepGoing=*/ false); | |
| } | |
| @Test | |
| public void depInconsistentFileSystem(@TestParameter boolean keepGoing) throws Exception { | |
| write("foo/BUILD", "sh_library(name = 'foo', deps = ['//bar:baz'])"); | |
| createBadBarBuild(); | |
| if (keepGoing) { | |
| options.add("--keep_going"); | |
| } | |
| QueryOutput result = getQueryResult("deps(//foo:foo)"); | |
| ExitCode expectedExitcode = | |
| keepGoing ? ExitCode.PARTIAL_ANALYSIS_FAILURE : ExitCode.ANALYSIS_FAILURE; | |
| assertExitCode(result, expectedExitcode); | |
| events.assertContainsError("Inconsistent filesystem operations"); | |
| events.assertContainsError("and referenced by '//foo:foo'"); | |
| if (keepGoing) { | |
| events.assertContainsError("Evaluation of query \"deps(//foo:foo)\" failed: errors were "); | |
| } else { | |
| events.assertContainsError( | |
| "Evaluation of query \"deps(//foo:foo)\" failed: preloading transitive closure failed: "); | |
| } | |
| // TODO(janakr): We emit duplicate events: in the ErrorPrintingTargetEdgeErrorObserver and in | |
| // TransitiveTargetFunction. Should be able to remove one of them, most likely | |
| // TransitiveTargetFunction. | |
| assertThat(events.errors()).hasSize(keepGoing ? 3 : 2); | |
| } | |
| @Test | |
| public void invalidQueryFailsParsing() throws Exception { | |
| QueryOutput result = getQueryResult("deps(\"--bad_target_name_from_bad_script\")"); | |
| assertCommandLineErrorExitCode(result); | |
| assertThat(result.getStdout()).isEmpty(); | |
| events.assertContainsError("target literal must not begin with (-)"); | |
| } | |
| @Test | |
| public void siblingsFunction() throws Exception { | |
| write( | |
| "foo/BUILD", | |
| "sh_library(name='t1')", | |
| "sh_library(name='t2')", | |
| "sh_library(name='t3')", | |
| "sh_library(name='t4')", | |
| "sh_library(name='t5')"); | |
| QueryOutput result = getQueryResult("siblings(//foo:t1)"); | |
| assertSuccessfulExitCode(result); | |
| assertThat(result.getStdout()).isNotEmpty(); | |
| } | |
| @Test | |
| public void samePackageDirectRDepsFunction() throws Exception { | |
| write( | |
| "foo/BUILD", | |
| "sh_library(name='t1', srcs=['t1.sh'])", | |
| "sh_library(name='t2', srcs=['t2.sh'])", | |
| "sh_library(name='t3', srcs=['t2.sh'])"); | |
| QueryOutput result = getQueryResult("same_pkg_direct_rdeps(//foo:t1.sh)"); | |
| assertSuccessfulExitCode(result); | |
| assertQueryOutputContains(result, "//foo:t1"); | |
| assertQueryOutputDoesNotContain(result, "//foo:t2", "/foo:t3"); | |
| } | |
| @Test | |
| public void graphlessQuery() throws Exception { | |
| write("foo/BUILD", "sh_library(name='foo', srcs=['foo.sh'])"); | |
| QueryOutput result = | |
| getQueryResult("//foo", "--experimental_graphless_query", "--order_output=no"); | |
| assertSuccessfulExitCode(result); | |
| assertQueryOutputContains(result, "//foo:foo"); | |
| } | |
| @Test | |
| public void graphlessQueryRequiresUnorderedOutput() throws Exception { | |
| write("foo/BUILD", "sh_library(name='foo', srcs=['foo.sh'])"); | |
| QueryOutput result = | |
| getQueryResult("//foo", "--experimental_graphless_query", "--order_output=deps"); | |
| events.assertContainsError( | |
| "--experimental_graphless_query requires --order_output=no or --order_output=auto"); | |
| assertCommandLineErrorExitCode(result); | |
| assertThat(result.getStdout()).isEmpty(); | |
| } | |
| @Test | |
| public void graphlessQueryWithLexicographicalOutput() throws Exception { | |
| write("foo/BUILD", "sh_library(name='foo', srcs=['foo.sh'])"); | |
| QueryOutput result = | |
| getQueryResult( | |
| "//foo", | |
| "--experimental_graphless_query", | |
| "--order_output=auto", | |
| "--incompatible_lexicographical_output"); | |
| assertSuccessfulExitCode(result); | |
| assertThat(result.getStdout()).isNotEmpty(); | |
| } | |
| @Test | |
| public void graphlessQueryRequiresStreamedFormatter() throws Exception { | |
| write("foo/BUILD", "sh_library(name='foo', srcs=['foo.sh'])"); | |
| QueryOutput result = | |
| getQueryResult( | |
| "//foo", "--experimental_graphless_query", "--order_output=no", "--output=maxrank"); | |
| assertCommandLineErrorExitCode(result); | |
| assertThat(result.getStdout()).isEmpty(); | |
| events.assertContainsError( | |
| "--experimental_graphless_query requires --order_output=no or --order_output=auto and an" | |
| + " --output option that supports streaming"); | |
| } | |
| @Test | |
| public void ruleStackInBuildOutput() throws Exception { | |
| /* | |
| * See b/151165647 - This needs a non-trivial package name to avoid | |
| * including extraneous directories in the generator_location. | |
| */ | |
| write( | |
| "package/inc.bzl", | |
| "def _impl(ctx): pass", | |
| "myrule = rule(implementation = _impl)", | |
| "def f():", | |
| " g()", | |
| "def g():", | |
| " myrule(name='a')"); | |
| write("package/BUILD", "load('inc.bzl', 'f')\n" + "f()"); | |
| QueryOutput result = getQueryResult("//package:a", "--output=build"); | |
| assertSuccessfulExitCode(result); | |
| // TODO(b/151165647): fix the heuristic that incorrectly creates generator_location by// | |
| // relativizing package name "p" relative to /foo/tmp/ regardless of segment boundaries. | |
| // TODO(b/151151653): the output should contain only workspace-relative paths. | |
| String workspaceDir = getWorkspace().toString(); | |
| String expectedOut = | |
| "# " | |
| + workspaceDir | |
| + "/package/BUILD:2:2\n" | |
| + "myrule(\n" | |
| + " name = \"a\",\n" | |
| + " generator_name = \"a\",\n" | |
| + " generator_function = \"f\",\n" | |
| + " generator_location = " | |
| + "\"package/BUILD:2:2\",\n" | |
| + ")\n" | |
| + "# Rule a instantiated at (most recent call last):\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/BUILD:2:2 in <toplevel>\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/inc.bzl:4:4 in f\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/inc.bzl:6:9 in g\n" | |
| + "# Rule myrule defined at (most recent call last):\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/inc.bzl:2:14 in <toplevel>\n\n"; | |
| String out = new String(result.getStdout(), UTF_8); | |
| assertThat(out).isEqualTo(expectedOut); | |
| } | |
| /* | |
| * Test of instantiation_stack (b/36593041) through query --output=build | |
| */ | |
| @Test | |
| public void ruleStackInProtoOutput() throws Exception { | |
| write( | |
| "p/inc.bzl", | |
| "def _impl(ctx): pass", | |
| "myrule = rule(implementation = _impl)", | |
| "def f():", | |
| " g()", | |
| "def g():", | |
| " myrule(name='a')"); | |
| write("p/BUILD", "load('inc.bzl', 'f')", "f()"); | |
| ProtoQueryOutput result = | |
| getProtoQueryResult("//p:a", "--output=proto", "--proto:instantiation_stack=true"); | |
| assertSuccessfulExitCode(result.getQueryOutput()); | |
| String expectedProtoOut = | |
| " instantiation_stack: \"p/BUILD:2:2: <toplevel>\"\n" | |
| + " instantiation_stack: \"p/inc.bzl:4:4: f\"\n" | |
| + " instantiation_stack: \"p/inc.bzl:6:9: g\""; | |
| String actualProtoOut = result.getQueryResult().toString(); | |
| assertThat(actualProtoOut).contains(expectedProtoOut); | |
| } | |
| /* | |
| * Regression test for b/162110273. | |
| */ | |
| @Test | |
| public void ruleStackRegressionTest() throws Exception { | |
| /* | |
| * See b/151165647 - This needs a non-trivial package name to avoid | |
| * including extraneous directories in the generator_location. | |
| */ | |
| write( | |
| "package/inc.bzl", | |
| "def g(name):", | |
| " native.filegroup(name = name)", | |
| "", | |
| "def f(name):", | |
| " g(name)"); | |
| write("package/BUILD", "load(\"inc.bzl\", \"f\")", "f(name = \"a\")", "f(name = \"b\")"); | |
| QueryOutput result = getQueryResult("//package:all", "--output=build"); | |
| assertSuccessfulExitCode(result); | |
| String workspaceDir = getWorkspace().toString(); | |
| String expectedOut = | |
| "# " | |
| + workspaceDir | |
| + "/package/BUILD:2:2\n" | |
| + "filegroup(\n" | |
| + " name = \"a\",\n" | |
| + " generator_name = \"a\",\n" | |
| + " generator_function = \"f\",\n" | |
| + " generator_location = " | |
| + "\"package/BUILD:2:2\",\n" | |
| + ")\n" | |
| + "# Rule a instantiated at (most recent call last):\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/BUILD:2:2 in <toplevel>\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/inc.bzl:5:6 in f\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/inc.bzl:2:21 in g\n" | |
| + "\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/BUILD:3:2\n" | |
| + "filegroup(\n" | |
| + " name = \"b\",\n" | |
| + " generator_name = \"b\",\n" | |
| + " generator_function = \"f\",\n" | |
| + " generator_location = " | |
| + "\"package/BUILD:3:2\",\n" | |
| + ")\n" | |
| + "# Rule b instantiated at (most recent call last):\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/BUILD:3:2 in <toplevel>\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/inc.bzl:5:6 in f\n" | |
| + "# " | |
| + workspaceDir | |
| + "/package/inc.bzl:2:21 in g\n\n"; | |
| String out = new String(result.getStdout(), UTF_8); | |
| assertThat(out).isEqualTo(expectedOut); | |
| } | |
| @Test | |
| public void depthBoundedQuery(@TestParameter boolean orderResults) throws Exception { | |
| if (orderResults) { | |
| options.add("--order_output=auto"); | |
| } else { | |
| options.add("--order_output=no"); | |
| options.add("--universe_scope=//depth:*"); | |
| } | |
| write( | |
| "depth/BUILD", | |
| "sh_binary(name = 'one', srcs = ['one.sh'], deps = [':two'])", | |
| "sh_library(name = 'two', srcs = ['two.sh'],", | |
| " deps = [':div2', ':three', '//depth2:three'])", | |
| "sh_library(name = 'three', srcs = ['three.sh'], deps = [':four'])", | |
| "sh_library(name = 'four', srcs = ['four.sh'], deps = [':div2', ':five'])", | |
| "sh_library(name = 'five', srcs = ['five.sh'])", | |
| "sh_library(name = 'div2', srcs = ['two.sh'])"); | |
| write("depth2/BUILD", "sh_library(name = 'three', srcs = ['three.sh'])"); | |
| write("depth/one.sh", ""); | |
| write("depth/two.sh", ""); | |
| write("depth/three.sh", ""); | |
| write("depth/four.sh", ""); | |
| write("depth/five.sh", ""); | |
| write("depth2/three.sh", ""); | |
| QueryOutput oneDep = getQueryResult("deps(//depth:one, 1)"); | |
| assertQueryOutputContains(oneDep, "//depth:one.sh", "//depth:two", TestConstants.LAUNCHER_PATH); | |
| assertQueryOutputDoesNotContain(oneDep, "//depth2"); | |
| // Ensure that the whole transitive closure wasn't pulled in earlier if not pre-loading. | |
| QueryOutput threeDep = | |
| getQueryResult("deps(//depth:one, 3)", "--experimental_ui_debug_all_events"); | |
| if (orderResults) { | |
| events.assertContainsEvent(EventKind.PROGRESS, "Loading package: depth2"); | |
| } | |
| assertQueryOutputContains( | |
| threeDep, | |
| "//depth:one", | |
| "//depth:one.sh", | |
| "//depth:two", | |
| "//depth:two.sh", | |
| "//depth:div2", | |
| "//depth:three", | |
| "//depth:three.sh", | |
| "//depth:four", | |
| "//depth2:three", | |
| "//depth2:three.sh", | |
| TestConstants.LAUNCHER_PATH); | |
| QueryOutput oneDepNonExperimental = getQueryResult("deps(//depth:one, 3)"); | |
| /* | |
| * --experimental_ui_debug_all_events and expect_query_targets are not | |
| * mutually compatible at this time, so we run this again to check that the | |
| * output is exact rather than a superset. | |
| */ | |
| assertQueryOutputContains( | |
| oneDepNonExperimental, | |
| "//depth:one", | |
| "//depth:one.sh", | |
| "//depth:two", | |
| "//depth:two.sh", | |
| "//depth:div2", | |
| "//depth:three", | |
| "//depth:three.sh", | |
| "//depth:four", | |
| "//depth2:three", | |
| "//depth2:three.sh", | |
| TestConstants.LAUNCHER_PATH); | |
| QueryOutput twoDep = | |
| getQueryResult("deps(//depth:one, 2)", "--experimental_ui_debug_all_events"); | |
| events.clear(); | |
| // Restricting the query, however, should not cause reloading. | |
| events.assertDoesNotContainEvent("Loading package:"); | |
| assertQueryOutputContains( | |
| twoDep, | |
| "//depth:one", | |
| "//depth:one.sh", | |
| "//depth:two", | |
| "//depth:two.sh", | |
| "//depth:three", | |
| "//depth:div2", | |
| "//depth2:three", | |
| TestConstants.LAUNCHER_PATH); | |
| // Same as above | |
| QueryOutput twoDepNonExperimental = getQueryResult("deps(//depth:one, 2)"); | |
| assertQueryOutputContains( | |
| twoDepNonExperimental, | |
| "//depth:one", | |
| "//depth:one.sh", | |
| "//depth:two", | |
| "//depth:two.sh", | |
| "//depth:three", | |
| "//depth:div2", | |
| "//depth2:three", | |
| TestConstants.LAUNCHER_PATH); | |
| } | |
| @Test | |
| public void inconsistentSkyQueryIncremental() throws Exception { | |
| write("foo/BUILD"); | |
| PathFragment barFile = PathFragment.create("bar/BUILD"); | |
| PathFragment bar = barFile.getParentDirectory(); | |
| Path badFile = write(barFile.getPathString()); | |
| fs.stubStat(badFile, null); | |
| CountDownLatch directoryListingLatch = new CountDownLatch(1); | |
| getSkyframeExecutor() | |
| .getEvaluator() | |
| .injectGraphTransformerForTesting( | |
| NotifyingHelper.makeNotifyingTransformer( | |
| (key, type, order, context) -> { | |
| if (NotifyingHelper.EventType.IS_READY.equals(type) | |
| && FileStateKey.FILE_STATE.equals(key.functionName()) | |
| && barFile.equals(((RootedPath) key.argument()).getRootRelativePath())) { | |
| TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions( | |
| directoryListingLatch, "Directory never listed"); | |
| } else if (NotifyingHelper.EventType.SET_VALUE.equals(type) | |
| && NotifyingHelper.Order.AFTER.equals(order) | |
| && SkyFunctions.DIRECTORY_LISTING_STATE.equals(key.functionName()) | |
| && bar.equals(((RootedPath) key.argument()).getRootRelativePath())) { | |
| directoryListingLatch.countDown(); | |
| } | |
| })); | |
| QueryOutput queryResult = | |
| getQueryResult("set()", "--universe_scope=//bar/...", "-k", "--order_output=no"); | |
| assertThat( | |
| queryResult | |
| .getBlazeCommandResult() | |
| .getDetailedExitCode() | |
| .getFailureDetail() | |
| .getPackageLoading() | |
| .getCode()) | |
| .isEqualTo(FailureDetails.PackageLoading.Code.TRANSIENT_INCONSISTENT_FILESYSTEM_ERROR); | |
| assertThat(directoryListingLatch.await(0, SECONDS)).isTrue(); | |
| TrackingAwaiter.INSTANCE.assertNoErrors(); | |
| } | |
| @Test | |
| public void skyQueryStatExtensionPackage() throws Exception { | |
| write("foo/BUILD", "load('//foo/bar:bar.bzl', 'sym')"); | |
| write("foo/bar/bar.bzl", "sym = 0"); | |
| Path barBuild = write("foo/bar/BUILD"); | |
| AtomicInteger barBuildCount = new AtomicInteger(0); | |
| fs.watchedPaths.put(barBuild.asFragment(), barBuildCount::incrementAndGet); | |
| QueryOutput queryResult = | |
| getQueryResult("buildfiles(//foo:*)", "--universe_scope=//foo/...", "--order_output=no"); | |
| assertQueryOutputContains(queryResult, "//foo:BUILD", "//foo/bar:BUILD", "//foo/bar:bar.bzl"); | |
| assertThat(barBuildCount.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void skyQueryExtensionPackageBuildFileDeletedAfterStat() throws Exception { | |
| write("foo/BUILD", "load('//foo/bar:bar.bzl', 'sym')"); | |
| Path barBzl = write("foo/bar/bar.bzl", "sym = 0"); | |
| Path barBuild = write("foo/bar/BUILD"); | |
| AtomicInteger barBuildCount = new AtomicInteger(0); | |
| fs.watchedPaths.put(barBuild.asFragment(), barBuildCount::incrementAndGet); | |
| fs.watchedPaths.put( | |
| barBzl.asFragment(), | |
| () -> { | |
| syscallCache.clear(); | |
| try { | |
| barBuild.delete(); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| }); | |
| QueryOutput queryResult = | |
| getQueryResult("buildfiles(//foo:*)", "--universe_scope=//foo/...", "--order_output=no"); | |
| assertQueryOutputContains(queryResult, "//foo:BUILD", "//foo/bar:BUILD", "//foo/bar:bar.bzl"); | |
| assertThat(barBuildCount.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void skyQueryExtensionPackageBuildFileNotInUniverseHasError() throws Exception { | |
| write("foo/BUILD", "load('//foo/bar:bar.bzl', 'sym')"); | |
| Path barBzl = write("foo/bar/bar.bzl", "sym = 0"); | |
| Path barBuild = write("foo/bar/BUILD", "bad syntax won't matter"); | |
| AtomicInteger barBuildCount = new AtomicInteger(0); | |
| fs.watchedPaths.put(barBuild.asFragment(), barBuildCount::incrementAndGet); | |
| fs.watchedPaths.put( | |
| barBzl.asFragment(), | |
| () -> { | |
| syscallCache.clear(); | |
| try { | |
| barBuild.delete(); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| }); | |
| QueryOutput queryResult = | |
| getQueryResult("buildfiles(//foo:*)", "--universe_scope=//foo:*", "--order_output=no"); | |
| assertQueryOutputContains(queryResult, "//foo:BUILD", "//foo/bar:BUILD", "//foo/bar:bar.bzl"); | |
| assertThat(barBuildCount.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void nokeepGoingStopsLoadingPackages() throws Exception { | |
| Path fooBuild = write("foo/BUILD", "sh_library(name = 'foo', deps = ['//deppackage'])"); | |
| write("bar/BUILD", "sh_library(name = 'bar', deps= ['//missing'])"); | |
| fs.watchedPaths.put( | |
| fooBuild.getParentDirectory().getChild("deppackage").asFragment(), | |
| () -> fail("deppackage should not have been statted")); | |
| PathFragment depPackageBuild = PathFragment.create("deppackage/BUILD"); | |
| getSkyframeExecutor() | |
| .getEvaluator() | |
| .injectGraphTransformerForTesting( | |
| NotifyingHelper.makeNotifyingTransformer( | |
| (key, type, order, context) -> { | |
| if (order == NotifyingHelper.Order.BEFORE | |
| && FileValue.FILE.equals(key.functionName())) { | |
| if (!((RootedPath) key.argument()) | |
| .getRootRelativePath() | |
| .endsWith(depPackageBuild)) { | |
| return; | |
| } | |
| try { | |
| Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS); | |
| fail("Should have been interrupted"); | |
| } catch (InterruptedException e) { | |
| // Expected. | |
| Thread.currentThread().interrupt(); | |
| } | |
| } | |
| })); | |
| QueryOutput queryResult = getQueryResult("deps(//foo:all + //bar:all)", "--nokeep_going"); | |
| assertExitCode(queryResult, ExitCode.ANALYSIS_FAILURE); | |
| events.assertDoesNotContainEvent("deppackage"); | |
| } | |
| private void assertExitCode(QueryOutput result, ExitCode expected) { | |
| assertThat(result.getBlazeCommandResult().getExitCode()).isEqualTo(expected); | |
| } | |
| private void assertSuccessfulExitCode(QueryOutput result) { | |
| assertExitCode(result, ExitCode.SUCCESS); | |
| } | |
| private void assertCommandLineErrorExitCode(QueryOutput result) { | |
| assertExitCode(result, ExitCode.COMMAND_LINE_ERROR); | |
| } | |
| private void assertQueryOutputContains(QueryOutput result, String... expectedStrings) { | |
| String out = new String(result.getStdout(), UTF_8); | |
| for (String expectedString : expectedStrings) { | |
| assertThat(out).contains(expectedString); | |
| } | |
| } | |
| private void assertQueryOutputDoesNotContain(QueryOutput result, String... unexpected) { | |
| String out = new String(result.getStdout(), UTF_8); | |
| for (String log : unexpected) { | |
| assertThat(out).doesNotContain(log); | |
| } | |
| } | |
| private QueryOutput getQueryResult(String queryString, String... flags) throws Exception { | |
| Collections.addAll(options, flags); | |
| runtimeWrapper.resetOptions(); | |
| runtimeWrapper.addOptions(options); | |
| runtimeWrapper.addOptions(queryString); | |
| CommandEnvironment env = runtimeWrapper.newCommand(QueryCommand.class); | |
| OptionsParsingResult options = env.getOptions(); | |
| for (BlazeModule module : getRuntime().getBlazeModules()) { | |
| module.beforeCommand(env); | |
| } | |
| env.getEventBus() | |
| .post( | |
| new GotOptionsEvent( | |
| getRuntime().getStartupOptionsProvider(), | |
| options, | |
| InvocationPolicy.getDefaultInstance())); | |
| for (BlazeModule module : getRuntime().getBlazeModules()) { | |
| env.getSkyframeExecutor().injectExtraPrecomputedValues(module.getPrecomputedValues()); | |
| } | |
| // In this test we are allowed to omit the beforeCommand; so force setting of a command | |
| // id in the CommandEnvironment, as we will need it in a moment even though we deviate from | |
| // normal calling order. | |
| try { | |
| env.getCommandId(); | |
| } catch (IllegalArgumentException e) { | |
| // Ignored, as we know the test deviates from normal calling order. | |
| } | |
| ByteArrayOutputStream stdout = new ByteArrayOutputStream(); | |
| env.getReporter() | |
| .addHandler( | |
| event -> { | |
| if (event.getKind().equals(EventKind.STDOUT)) { | |
| try { | |
| stdout.write(event.getMessageBytes()); | |
| } catch (IOException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| }); | |
| BlazeCommandResult lastBlazeCommandResult = new QueryCommand().exec(env, options); | |
| return new QueryOutput(lastBlazeCommandResult, stdout.toByteArray()); | |
| } | |
| private Document getXmlQueryResult(String queryString) throws Exception { | |
| options.add("--output=xml"); | |
| byte[] queryResult = getQueryResult(queryString).getStdout(); | |
| return DocumentBuilderFactory.newInstance() | |
| .newDocumentBuilder() | |
| .parse(new ByteArrayInputStream(queryResult)); | |
| } | |
| private static List<Node> xpathSelect(Node doc, String expression) throws Exception { | |
| XPathExpression expr = XPathFactory.newInstance().newXPath().compile(expression); | |
| NodeList result = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); | |
| List<Node> list = new ArrayList<>(); | |
| for (int i = 0; i < result.getLength(); i++) { | |
| list.add(result.item(i)); | |
| } | |
| return list; | |
| } | |
| private List<String> getStringQueryResult(String queryString) throws Exception { | |
| QueryOutput result = getQueryResult(queryString); | |
| return Arrays.asList(new String(result.getStdout(), Charset.defaultCharset()).split("\n")); | |
| } | |
| private ProtoQueryOutput getProtoQueryResult(String queryString, String... flags) | |
| throws Exception { | |
| options.add("--output=proto"); | |
| Collections.addAll(options, flags); | |
| QueryOutput result = getQueryResult(queryString); | |
| byte[] stdout = result.getStdout(); | |
| QueryResult queryResult = QueryResult.parseFrom(stdout, ExtensionRegistry.getEmptyRegistry()); | |
| return new ProtoQueryOutput(result, queryResult); | |
| } | |
| Element getResultNode(Document xml, String ruleName) throws Exception { | |
| return (Element) | |
| Iterables.getOnlyElement(xpathSelect(xml, String.format("/query/*[@name='%s']", ruleName))); | |
| } | |
| } |