blob: fb4e779beabd6a0441f2d3457b75a869a719ab4c [file] [log] [blame]
// 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 org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.query2.common.AbstractBlazeQueryEnvironment;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
import com.google.devtools.build.lib.query2.engine.QueryException;
import com.google.devtools.build.lib.query2.engine.QueryUtil;
import com.google.devtools.build.lib.query2.engine.QueryUtil.AggregateAllOutputFormatterCallback;
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.OutputFormatter;
import com.google.devtools.build.lib.query2.query.output.OutputFormatters;
import com.google.devtools.build.lib.query2.query.output.QueryOptions;
import com.google.devtools.build.lib.query2.query.output.QueryOptions.OrderOutput;
import com.google.devtools.build.lib.query2.query.output.QueryOutputUtils;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.GotOptionsEvent;
import com.google.devtools.build.lib.runtime.KeepGoingOption;
import com.google.devtools.build.lib.runtime.commands.QueryCommand;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.common.options.Options;
import com.google.devtools.common.options.OptionsParser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
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.junit.runners.JUnit4;
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(JUnit4.class)
public class QueryIntegrationTest extends BuildIntegrationTestCase {
private QueryOptions queryOptions;
private boolean keepGoing;
@Before
public final void setQueryOptions() throws Exception {
queryOptions = Options.getDefaults(QueryOptions.class);
keepGoing = Options.getDefaults(KeepGoingOption.class).keepGoing;
queryOptions.universeScope = ImmutableList.of("//...:*");
}
// 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);
QueryResult result = getProtoQueryResult("deps(//foo:a)");
assertSameElementsDifferentOrder(getTargetNames(result), expected);
queryOptions.orderOutput = OrderOutput.FULL;
result = getProtoQueryResult("deps(//foo:a)");
assertThat(getTargetNames(result)).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");
queryOptions.outputFormat = minRank ? "minrank" : "maxrank";
keepGoing = true;
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);
queryOptions.orderOutput = OrderOutput.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, Collections.reverseOrder());
write("foo/BUILD", "sh_library(name = 'a', deps = [" + depString + "])", targets);
List<String> result = getStringQueryResult("deps(//foo:a)");
assertThat(result).containsExactlyElementsIn(expected).inOrder();
queryOptions.orderOutput = OrderOutput.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.
QueryResult result = getProtoQueryResult("tests(//donut:cop)");
assertThat(result.getTargetCount()).isEqualTo(1);
assertThat(result.getTarget(0).getRule().getName()).isEqualTo("//donut:shop");
}
@Test
public void testStrictTests() throws Exception {
queryOptions.strictTestSuite = true;
write("donut/BUILD",
"sh_binary(name = 'thief', srcs = ['thief.sh'])",
"test_suite(name = 'cop', tests = [':thief'])");
QueryException e =
assertThrows(QueryException.class, () -> getProtoQueryResult("tests(//donut:cop)"));
assertThat(e)
.hasMessageThat()
.contains(
"The label '//donut:thief' in the test_suite "
+ "'//donut:cop' does not refer to a test");
}
private byte[] getQueryResult(String queryString) throws Exception {
// TODO(hanwen): this should probably use BlazeRuntimeWrapper so
// we don't have to duplicate option/module handling.
OptionsParser optionsParser = runtimeWrapper.createOptionsParser();
Command command = QueryCommand.class.getAnnotation(Command.class);
CommandEnvironment env =
getBlazeWorkspace().initCommand(command, optionsParser, new ArrayList<>());
for (BlazeModule module : getRuntime().getBlazeModules()) {
module.beforeCommand(env);
}
env.getEventBus()
.post(
new GotOptionsEvent(
getRuntime().getStartupOptionsProvider(),
optionsParser,
InvocationPolicy.getDefaultInstance()));
for (BlazeModule module : getRuntime().getBlazeModules()) {
env.getSkyframeExecutor().injectExtraPrecomputedValues(module.getPrecomputedValues());
}
// In this test we are allowed to ommit 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.
}
env.setupPackageCache(optionsParser);
OutputFormatter formatter =
OutputFormatters.getFormatter(
OutputFormatters.getDefaultFormatters(), queryOptions.outputFormat);
// TODO(ulfjack): This should run the code in QueryCommand instead.
Set<Setting> settings = queryOptions.toSettings();
AbstractBlazeQueryEnvironment<Target> queryEnv =
QueryCommand.newQueryEnvironment(
env,
keepGoing,
!QueryOutputUtils.shouldStreamResults(queryOptions, formatter),
/*universeScope=*/ ImmutableList.of(),
/*loadingPhaseThreads=*/ 1,
settings,
/*useGraphlessQuery=*/ false);
AggregateAllOutputFormatterCallback<Target, ?> callback =
QueryUtil.newOrderedAggregateAllOutputFormatterCallback(queryEnv);
QueryEvalResult result = queryEnv.evaluateQuery(queryString, callback);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Reporter reporter = new Reporter(new EventBus(), events.collector());
QueryOutputUtils.output(
queryOptions,
result,
callback.getResult(),
formatter,
outputStream,
queryOptions.aspectDeps.createResolver(env.getPackageManager(), reporter),
reporter);
return outputStream.toByteArray();
}
private Document getXmlQueryResult(String queryString) throws Exception {
queryOptions.outputFormat = "xml";
byte[] queryResult = getQueryResult(queryString);
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 {
return Arrays.asList(
new String(getQueryResult(queryString), Charset.defaultCharset()).split("\n"));
}
private QueryResult getProtoQueryResult(String queryString) throws Exception {
queryOptions.outputFormat = "proto";
return QueryResult.parseFrom(getQueryResult(queryString));
}
Element getResultNode(Document xml, String ruleName) throws Exception {
return (Element) Iterables.getOnlyElement(xpathSelect(xml,
String.format("/query/*[@name='%s']", ruleName)));
}
}