| // 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.devtools.build.lib.testutil.MoreAsserts.assertContainsEvent; |
| import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.BuildFailedException; |
| import com.google.devtools.build.lib.analysis.ViewCreationFailedException; |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; |
| import com.google.devtools.build.lib.testutil.Suite; |
| import com.google.devtools.build.lib.testutil.TestSpec; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Integration tests for the 'genquery' rule. |
| */ |
| @TestSpec(size = Suite.MEDIUM_TESTS) |
| @RunWith(JUnit4.class) |
| public class GenQueryIntegrationTest extends BuildIntegrationTestCase { |
| @Test |
| public void testDoesNotFailHorribly() throws Exception { |
| write( |
| "fruits/BUILD", |
| "sh_library(name='melon', deps=[':papaya'])", |
| "sh_library(name='papaya')", |
| "genquery(name='q',", |
| " scope=[':melon'],", |
| " expression='deps(//fruits:melon)')"); |
| assertQueryResult("//fruits:q", "//fruits:melon", "//fruits:papaya"); |
| } |
| |
| private void deterministicTestHelper(boolean graphless) throws Exception { |
| write( |
| "fruits/BUILD", |
| "sh_library(name='melon', deps=[':papaya', ':apple'])", |
| "sh_library(name='papaya', deps=[':banana'])", |
| "sh_library(name='banana', deps=[':apple'])", |
| "sh_library(name='apple', deps=[':cherry'])", |
| "sh_library(name='cherry')", |
| "genquery(name='q',", |
| " scope=[':melon'],", |
| " expression='deps(//fruits:melon)')"); |
| String firstResult = getQueryResult("//fruits:q"); |
| for (int i = 0; i < 10; i++) { |
| createFilesAndMocks(); // Do a clean. |
| if (graphless) { |
| runtimeWrapper.addOptions("--experimental_genquery_use_graphless_query"); |
| } |
| assertThat(getQueryResult("//fruits:q")).isEqualTo(firstResult); |
| } |
| } |
| |
| @Test |
| public void testDeterministic() throws Exception { |
| deterministicTestHelper(/* graphless= */ false); |
| } |
| |
| @Test |
| public void testDeterministicGraphless() throws Exception { |
| runtimeWrapper.addOptions("--experimental_genquery_use_graphless_query"); |
| deterministicTestHelper(/* graphless= */ true); |
| } |
| |
| @Test |
| public void testiDuplicateName() throws Exception { |
| write("one/BUILD", "sh_library(name='foo')"); |
| write("two/BUILD", "sh_library(name='foo')"); |
| write( |
| "query/BUILD", |
| "sh_library(name='common', deps=['//one:foo', '//two:foo'])", |
| "genquery(name='q',", |
| " scope=['//query:common'],", |
| " expression='deps(//query:common)')"); |
| runtimeWrapper.addOptions("--experimental_genquery_use_graphless_query"); |
| assertThat(getQueryResult("//query:q").split("\n")).hasLength(3); |
| } |
| |
| @Test |
| public void testFailsIfGoesOutOfScope() throws Exception { |
| write("vegetables/BUILD", |
| "sh_library(name='tomato', deps=[':cabbage'])", |
| "sh_library(name='cabbage')", |
| "genquery(name='q',", |
| " scope=[':cabbage'],", |
| " expression='deps(//vegetables:tomato)')"); |
| |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//vegetables:q")); |
| |
| assertContainsEvent(events.collector(), "is not within the scope of the query"); |
| } |
| |
| // Regression test for http://b/29964062. |
| @Test |
| public void testFailsIfGoesOutOfScopeViaSelect() throws Exception { |
| write("q/BUILD", |
| "genquery(name='q', expression='deps(//q:f)', scope=['f'])", |
| "config_setting(name='cs', values={'define':'D=1'})", |
| "filegroup(name='f', srcs=select({'cs':[], '//conditions:default':['//dne']}))"); |
| |
| addOptions("--keep_going"); |
| addOptions("--define=D=1"); |
| assertThrows(BuildFailedException.class, () -> buildTarget("//q")); |
| |
| assertContainsEvent( |
| events.collector(), |
| "errors were encountered while computing transitive closure of the scope."); |
| } |
| |
| // Regression test for http://b/34132681 |
| @Test |
| public void testFailsIfBrokenDependencyViaSelect() throws Exception { |
| write( |
| "q/BUILD", |
| "genquery(name='q', expression='deps(//q:f)', scope=['f'])", |
| "config_setting(name='cs', values={'define':'D=1'})", |
| "filegroup(name='f', srcs=select({'cs':[], '//conditions:default':['//d:d']}))"); |
| // d exists but is missing "srcs" |
| write("d/BUILD", "sh_binary(name = 'd')"); |
| |
| addOptions("--keep_going"); |
| addOptions("--define=D=1"); |
| assertThrows(BuildFailedException.class, () -> buildTarget("//q")); |
| |
| assertContainsEvent( |
| events.collector(), |
| "errors were encountered while computing transitive closure of the scope"); |
| } |
| |
| |
| @Test |
| public void testResultsAlphabetized() throws Exception { |
| write( |
| "fruits/BUILD", |
| "sh_library(name='melon', deps=[':a', ':z', ':c', ':1', '//z:a', '//a:z', '//c:c'])", |
| "sh_library(name='a')", |
| "sh_library(name='z')", |
| "sh_library(name='1')", |
| "sh_library(name='c')", |
| "genquery(name='q',", |
| " scope=[':melon'],", |
| " expression='deps(//fruits:melon)')"); |
| write("z/BUILD", "sh_library(name = 'a')"); |
| write("a/BUILD", "sh_library(name = 'z')"); |
| write("c/BUILD", "sh_library(name = 'c', deps = ['//z:a'])"); |
| assertQueryResult( |
| "//fruits:q", |
| // Results are ordered in reverse dependency order (nodes, then their dependencies), and in |
| // reverse alphabetic order for ties. |
| "//fruits:melon", |
| "//fruits:z", |
| "//fruits:c", |
| "//fruits:a", |
| "//fruits:1", |
| "//c:c", |
| "//z:a", |
| "//a:z"); |
| } |
| |
| @Test |
| public void testQueryReexecutedIfDepsChange() throws Exception { |
| write("food/BUILD", |
| "sh_library(name='fruit_salad', deps=['//fruits:tropical'])", |
| "genquery(name='q',", |
| " scope=[':fruit_salad']," + |
| " expression='deps(//food:fruit_salad)')"); |
| |
| write("fruits/BUILD", |
| "sh_library(name='tropical', deps=[':papaya'])", |
| "sh_library(name='papaya')"); |
| |
| assertQueryResult("//food:q", "//food:fruit_salad", "//fruits:tropical", "//fruits:papaya"); |
| |
| write("fruits/BUILD", |
| "sh_library(name='tropical', deps=[':papaya', ':coconut'])", |
| "sh_library(name='papaya')", |
| "sh_library(name='coconut')"); |
| |
| assertQueryResult( |
| "//food:q", |
| "//food:fruit_salad", |
| "//fruits:tropical", |
| "//fruits:papaya", |
| "//fruits:coconut"); |
| } |
| |
| @Test |
| public void testGenQueryEncountersAnotherGenQuery() throws Exception { |
| write("spices/BUILD", |
| "sh_library(name='cinnamon', deps=[':nutmeg'])", |
| "sh_library(name='nutmeg')", |
| "genquery(name='q',", |
| " scope=[':cinnamon'],", |
| " expression='deps(//spices:cinnamon)')"); |
| |
| write("fruits/BUILD", |
| "sh_library(name='pear', deps=[':plum'])", |
| "sh_library(name='plum')", |
| "genquery(name='q',", |
| " scope=[':pear', '//spices:q'],", |
| " expression='deps(//fruits:pear) + deps(//spices:q)')"); |
| |
| assertQueryResult( |
| "//fruits:q", |
| "//spices:q", |
| "//spices:cinnamon", |
| "//spices:nutmeg", |
| "//fruits:pear", |
| "//fruits:plum"); |
| } |
| |
| /** |
| * Regression test for b/14227750: genquery referring to non-existent target crashes on skyframe. |
| */ |
| @Test |
| public void testHandlesMissingTargetGracefully() throws Exception { |
| write("a/BUILD", |
| "genquery(name='query', scope=['//b:target'], expression='deps(//b:nosuchtarget)')"); |
| write("b/BUILD", |
| "sh_library(name = 'target')"); |
| assertThrows(ViewCreationFailedException.class, () -> buildTarget("//a:query")); |
| events.assertContainsError( |
| "in genquery rule //a:query: query failed: no such target '//b:nosuchtarget'"); |
| } |
| |
| @Test |
| public void testMultiplePatternsInQuery() throws Exception { |
| String buildFile = ""; |
| String genQuery = "genquery(name = 'q', scope = [':top'], " |
| + "expression = 'deps(//spices:top) ' + \n"; |
| String topTarget = "sh_library(name = 'top', deps = [\n"; |
| for (int i = 0; i < 20; i++) { |
| String targetName = (i % 2 == 0 ? "in" : "out") + i; |
| buildFile += "sh_library(name = '" + targetName + "')\n"; |
| if (i % 2 != 0) { |
| genQuery += "' - //spices:" + targetName + " ' + \n"; |
| } |
| topTarget += " ':" + targetName + "',\n"; |
| } |
| topTarget += "]\n)\n"; |
| genQuery += "'')"; |
| write("spices/BUILD", buildFile, topTarget, genQuery); |
| List<String> expected = new ArrayList<>(11); |
| for (int i = 0; i < 20; i += 2) { |
| expected.add(i / 2, "//spices:in" + i); |
| } |
| Collections.sort(expected, Collections.reverseOrder()); |
| expected.add(0, "//spices:top"); |
| assertQueryResult("//spices:q", expected.toArray(new String[0])); |
| } |
| |
| @Test |
| public void testGraphOutput_Factored() throws Exception { |
| write( |
| "fruits/BUILD", |
| "sh_library(name='melon', deps=[':papaya', ':coconut', ':mango'])", |
| "sh_library(name='papaya')", |
| "sh_library(name = 'mango')", |
| "sh_library(name = 'coconut')", |
| "genquery(name='q',", |
| " scope=[':melon'],", |
| " opts = ['--output=graph'],", |
| " expression='deps(//fruits:melon)')"); |
| assertPartialQueryResult( |
| "//fruits:q", |
| " \"//fruits:melon\"", |
| " \"//fruits:melon\" -> \"//fruits:coconut\\n//fruits:mango\\n//fruits:papaya\"", |
| " \"//fruits:coconut\\n//fruits:mango\\n//fruits:papaya\""); |
| } |
| |
| @Test |
| public void testGraphOutput_Unfactored() throws Exception { |
| write( |
| "fruits/BUILD", |
| "sh_library(name='melon', deps=[':papaya', ':coconut', ':mango'])", |
| "sh_library(name='papaya')", |
| "sh_library(name = 'mango')", |
| "sh_library(name = 'coconut')", |
| "genquery(name='q',", |
| " scope=[':melon'],", |
| " opts = ['--output=graph', '--nograph:factored'],", |
| " expression='deps(//fruits:melon)')"); |
| assertPartialQueryResult( |
| "//fruits:q", |
| " \"//fruits:melon\"", |
| " \"//fruits:melon\" -> \"//fruits:coconut\"", |
| " \"//fruits:melon\" -> \"//fruits:mango\"", |
| " \"//fruits:melon\" -> \"//fruits:papaya\"", |
| " \"//fruits:papaya\"", |
| " \"//fruits:mango\"", |
| " \"//fruits:coconut\""); |
| } |
| |
| @Test |
| public void testDoesntAllowLocationOutputWithLoadfiles() throws Exception { |
| write( |
| "foo/bzl.bzl", |
| "x = 2"); |
| write( |
| "foo/BUILD", |
| "load('//foo:bzl.bzl', 'x')", |
| "sh_library(name='foo')", |
| "genquery(", |
| " name = 'gen-loadfiles',", |
| " expression = 'loadfiles(//foo:foo)',", |
| " scope = ['//foo:foo'],", |
| ")", |
| "genquery(", |
| " name = 'gen-loadfiles-location',", |
| " expression = 'loadfiles(//foo:foo)',", |
| " opts = ['--output=location'],", |
| " scope = ['//foo:foo'],", |
| ")"); |
| assertQueryResult( |
| "//foo:gen-loadfiles", |
| "//foo:bzl.bzl"); |
| assertThrows( |
| ViewCreationFailedException.class, () -> buildTarget("//foo:gen-loadfiles-location")); |
| events.assertContainsError( |
| "in genquery rule //foo:gen-loadfiles-location: query failed: Query expressions " |
| + "involving 'buildfiles' or 'loadfiles' cannot be used with --output=location"); |
| } |
| |
| @Test |
| public void testDoesntAllowLocationOutputWithBuildfiles() throws Exception { |
| write( |
| "foo/bzl.bzl", |
| "x = 2"); |
| write( |
| "foo/BUILD", |
| "load('//foo:bzl.bzl', 'x')", |
| "sh_library(name='foo')", |
| "genquery(", |
| " name = 'gen-buildfiles',", |
| " expression = 'buildfiles(//foo:foo)',", |
| " scope = ['//foo:foo'],", |
| ")", |
| "genquery(", |
| " name = 'gen-buildfiles-location',", |
| " expression = 'buildfiles(//foo:foo)',", |
| " opts = ['--output=location'],", |
| " scope = ['//foo:foo'],", |
| ")"); |
| assertQueryResult( |
| "//foo:gen-buildfiles", |
| "//foo:bzl.bzl", |
| "//foo:BUILD"); |
| assertThrows( |
| ViewCreationFailedException.class, () -> buildTarget("//foo:gen-buildfiles-location")); |
| events.assertContainsError( |
| "in genquery rule //foo:gen-buildfiles-location: query failed: Query expressions " |
| + "involving 'buildfiles' or 'loadfiles' cannot be used with --output=location"); |
| } |
| |
| /** Regression test for b/127644784. */ |
| @Test |
| public void somepathOutputDeterministic() throws Exception { |
| /* |
| * This graph structure routinely reproduces the bug within 10 iterations: |
| * |
| * ----------top------------ |
| * | | | | |
| * mid1 mid2 mid3 mid4 |
| * | | | | |
| * --lower-- | | |
| * | | | |
| * -----bottom---------- |
| */ |
| write( |
| "query/BUILD", |
| "genquery(", |
| " name = 'query',", |
| " expression = 'somepath(//top, //bottom)',", |
| " scope = ['//top', '//bottom'],", |
| ")"); |
| write("top/BUILD", "sh_library(name = 'top', deps = ['//mid1', '//mid2', '//mid3', '//mid4'])"); |
| write("mid1/BUILD", "sh_library(name = 'mid1', deps = ['//lower'])"); |
| write("mid2/BUILD", "sh_library(name = 'mid2', deps = ['//lower'])"); |
| write("mid3/BUILD", "sh_library(name = 'mid3', deps = ['//bottom'])"); |
| write("mid4/BUILD", "sh_library(name = 'mid4', deps = ['//bottom'])"); |
| write("lower/BUILD", "sh_library(name = 'lower', deps = ['//bottom'])"); |
| write("bottom/BUILD", "sh_library(name = 'bottom')"); |
| |
| String firstResult = getQueryResult("//query"); |
| for (int i = 0; i < 10; i++) { |
| createFilesAndMocks(); // Do a clean. |
| assertThat(getQueryResult("//query")).isEqualTo(firstResult); |
| } |
| } |
| |
| private void runNodepDepsTest(String optsStringValue, boolean expectVisibilityDep) |
| throws Exception { |
| write( |
| "foo/BUILD", |
| "sh_library(name = 't1', deps = [':t2'], visibility = [':pg', '//query:__pkg__'])", |
| "sh_library(name = 't2')", |
| "package_group(name = 'pg')"); |
| write( |
| "query/BUILD", |
| "genquery(", |
| " name = 'gen',", |
| " expression = 'deps(//foo:t1)',", |
| " scope = ['//foo:t1'],", |
| " opts = " + optsStringValue, |
| ")"); |
| |
| List<String> queryResultStrings = |
| ImmutableList.copyOf(getQueryResult("//query:gen").split("\n")); |
| if (expectVisibilityDep) { |
| assertThat(queryResultStrings).contains("//foo:pg"); |
| } else { |
| assertThat(queryResultStrings).doesNotContain("//foo:pg"); |
| } |
| } |
| |
| @Test |
| public void testNodepDeps_DefaultIsFalse() throws Exception { |
| runNodepDepsTest(/*optsStringValue=*/ "[]", /*expectVisibilityDep=*/ false); |
| } |
| |
| @Test |
| public void testNodepDeps_False() throws Exception { |
| runNodepDepsTest(/*optsStringValue=*/ "['--nodep_deps=false']", /*expectVisibilityDep=*/ false); |
| } |
| |
| @Test |
| public void testNodepDeps_True() throws Exception { |
| runNodepDepsTest(/*optsStringValue=*/ "['--nodep_deps=true']", /*expectVisibilityDep=*/ true); |
| } |
| |
| private void assertQueryResult(String queryTarget, String... expected) throws Exception { |
| assertThat(getQueryResult(queryTarget).split("\n")) |
| .asList() |
| .containsExactlyElementsIn(ImmutableList.copyOf(expected)) |
| .inOrder(); |
| } |
| |
| private void assertPartialQueryResult(String queryTarget, String... expected) throws Exception { |
| assertThat(getQueryResult(queryTarget).split("\n")) |
| .asList() |
| .containsAtLeastElementsIn(ImmutableList.copyOf(expected)) |
| .inOrder(); |
| } |
| |
| private String getQueryResult(String queryTarget) throws Exception { |
| buildTarget(queryTarget); |
| Artifact output = Iterables.getOnlyElement(getArtifacts(queryTarget)); |
| return readContentAsLatin1String(output); |
| } |
| } |