blob: 9559a10a5dc88f3540732b383f269dcc08983156 [file] [log] [blame]
// Copyright 2023 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.query2.testutil;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
import com.google.devtools.build.lib.query2.engine.QueryException;
import com.google.devtools.build.lib.query2.testutil.AbstractQueryTest.QueryHelper.ResultAndTargets;
import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.util.ExitCode;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for query evaluation when keep_going is enabled. It covers the QueryEvalTest and adds
* additional tests that are keep_going-specific.
*/
public abstract class AbstractQueryKeepGoingTest extends QueryTest {
@Before
public final void setKeepGoing() throws Exception {
helper.setKeepGoing(true);
}
// Like eval(), but asserts that evaluation completes normally, with an error.
// Events should be checked with assertContainsEvent().
protected ResultAndTargets<Target> evalFail(String query) throws Exception {
ResultAndTargets<Target> result = helper.evaluateQuery(query);
assertWithMessage("evaluateQuery succeeded: " + query)
.that(result.getQueryEvalResult().getSuccess())
.isFalse();
return result;
}
// Like eval(), but makes no assertions about whether evaluation completes with an error.
// Because the query helper reuses its AbstractBlazeQueryEnvironment, BlazeQueryEnvironment-based
// implementations will perform graph evaluations using the same memoizing evaluator, which reuses
// the same EmittedEventState, causing later evaluations that emit the same errors to not count as
// failures. SkyQueryEnvironment-based implementations do not do this, and may report that later
// evaluations have failed.
// In either case, events should be checked with assertContainsEvent().
// TODO(bazel-team): it is probably unintentional that BlazeQueryEnvironment-based evaluations'
// error state is sensitive to prior evaluations. Tests that use this method should be fixed when
// there's a chance to fix the state that's retained across queries because of the query helper
// and BlazeQueryEnvironment.
protected Set<Target> evalMaybe(String query) throws Exception {
return helper.evaluateQuery(query).getResultSet();
}
@Override
protected EvalThrowsResult evalThrows(String query, boolean unconditionallyThrows)
throws Exception {
// This method can be called in both keep_going and nokeep_going modes: expect either an
// exception or an error message.
try {
ResultAndTargets<Target> result = evalFail(query);
assertThat(helper.isKeepGoing()).isTrue();
String msg =
helper
.getFirstEvent()
.replaceAll("^Skipping '[^']+': ", "")
.replaceAll("Evaluation of query \"[^\"]+\" failed: ", "");
return new EvalThrowsResult(
msg, result.getQueryEvalResult().getDetailedExitCode().getFailureDetail());
} catch (QueryException e) {
// TODO(ulfjack): Even in keep_going mode, the query engine sometimes throws a QueryException.
// Remove the guard and fix the problems.
if (!unconditionallyThrows) {
assertThat(helper.isKeepGoing()).isFalse();
}
String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
return new EvalThrowsResult(msg, e.getFailureDetail());
}
}
// Regression test for bug #2482284:
// "blaze query mypackage:* does not report targets that cross package boundaries"
@Test
public void testErrorWhenResultContainsLabelsCrossingSubpackage() throws Exception {
writeFile(
"pear/BUILD",
"sh_library(name='plum/peach', srcs=['peach.sh'])",
"sh_library(name='apple', srcs=['apple.sh'])");
writeFile("pear/plum/BUILD");
assertPackageLoadingCode(evalFail("//pear:apple"), Code.LABEL_CROSSES_PACKAGE_BOUNDARY);
assertContainsEvent("is invalid because 'pear/plum' is a subpackage");
}
@Test
public void testErrorWhenWildcardResultContainsLabelsCrossingSubpackage() throws Exception {
writeFile(
"pear/BUILD",
"sh_library(name='plum/peach', srcs=['peach.sh'])",
"sh_library(name='apple', srcs=['apple.sh'])");
writeFile("pear/plum/BUILD");
assertPackageLoadingCode(evalFail("//pear:all"), Code.LABEL_CROSSES_PACKAGE_BOUNDARY);
assertContainsEvent("is invalid because 'pear/plum' is a subpackage");
}
@Override
protected void writeBuildFiles3() throws Exception {
writeFile(
"a/BUILD",
"genrule(name='a', srcs=['//b', '//c'], outs=['out'], cmd=':')",
"exports_files(['a2'])");
writeFile("b/BUILD", "genrule(name='b', srcs=['//d'], outs=['out'], cmd=':')");
writeFile("c/BUILD", "genrule(name='c', srcs=['//d'], outs=['out'], cmd=':')");
writeFile("d/BUILD", "exports_files(['d'])");
}
protected void assertNoFailFast(
String errorMsg, boolean checkFailureDetail, String keepGoingErrorMsg) throws Exception {
writeFile(
"missingdep/BUILD",
"cc_library(name = 'missingdep',",
" deps = [ '//i/do/not/exist'])");
helper.setKeepGoing(false);
EvalThrowsResult throwsResult1 = evalThrows("deps(//missingdep)", false);
assertThat(throwsResult1.getMessage()).contains(errorMsg);
if (checkFailureDetail) {
assertPackageLoadingCode(throwsResult1.getFailureDetail(), Code.BUILD_FILE_MISSING);
}
// (1) --keep_going.
helper.clearEvents();
helper.setKeepGoing(true);
// partial results
ResultAndTargets<Target> failResult =
evalFail("deps(//missingdep)" + TestConstants.CC_DEPENDENCY_CORRECTION);
assertThat(failResult.getResultSet()).isEqualTo(eval("//missingdep"));
assertContainsEvent("Evaluation of query \"deps(//missingdep)\" failed: " + keepGoingErrorMsg);
if (checkFailureDetail) {
assertPackageLoadingCode(failResult, Code.BUILD_FILE_MISSING);
}
// (2) --nokeep_going.
helper.setKeepGoing(false);
EvalThrowsResult throwsResult2 = evalThrows("deps(//missingdep)", false);
assertThat(throwsResult2.getMessage()).contains(errorMsg); // no results
if (checkFailureDetail) {
assertPackageLoadingCode(throwsResult2.getFailureDetail(), Code.BUILD_FILE_MISSING);
}
}
// Regression test for bug #1234015, "blaze query --keep_going doesn't
// always work". Previously, any failure in a labels() expression would
// cause results to be suppressed. Now, partial results are printed.
@Test
public void testNoFailFastOnLabelsExpression() throws Exception {
writeFile(
"bad/BUILD", "genrule(name='bad', srcs=['x', '//missing', 'y'], outs=['out'], cmd=':')");
Set<Target> result = evalFail("labels(srcs, //bad)").getResultSet();
assertContainsEvent("no such package 'missing': " + "BUILD file not found");
assertContainsEvent("--keep_going specified, ignoring errors. Results may be inaccurate");
assertThat(result).isEqualTo(eval("//bad:x + //bad:y")); // partial results
}
// Ensure that --keep_going distinguishes malformed target literals from
// good ones that happen to refer to bad BUILD files.
@Test
public void testBadBuildFileKeepGoing() throws Exception {
writeFile("bad/BUILD", "blah blah blah");
ResultAndTargets<Target> result = evalFail("bad:*");
assertPackageLoadingCode(result, Code.SYNTAX_ERROR);
assertContainsEvent("syntax error at 'blah'");
assertContainsEvent("--keep_going specified, ignoring errors. Results may be inaccurate");
assertThat(result.getResultSet()).isEqualTo(evalMaybe("//bad:BUILD")); // partial results
}
@Test
public void testStrictTestSuiteWithFileAndKeepGoing() throws Exception {
helper.setQuerySettings(Setting.TESTS_EXPRESSION_STRICT);
writeFile("x/BUILD", "test_suite(name='a', tests=['a.txt'])");
assertThat(evalFail("tests(//x:a)").getResultSet()).isEmpty();
assertContainsEvent(
"The label '//x:a.txt' in the test_suite '//x:a' does not refer to a test "
+ "or test_suite rule!");
}
@Test
public void testQueryAllForBrokenPackage() throws Exception {
writeFile(
"x/BUILD", //
"filegroup(name = 'a')",
"x = 1 // 0",
"filegroup(name = 'c')" // not executed
);
assertThat(evalFail("//x:all").getResultSet()).hasSize(1);
assertContainsEvent("division by zero");
assertContainsEvent("Results may be inaccurate");
}
@Test
public void testQueryDotDotDotForBrokenPackage() throws Exception {
writeFile(
"x/BUILD", //
"filegroup(name = 'a')",
"x = 1 // 0",
"filegroup(name = 'c')" // not executed
);
assertThat(evalFail("//x/...").getResultSet()).hasSize(1);
assertContainsEvent("division by zero");
assertContainsEvent("Results may be inaccurate");
}
@Test
public void testNonExistentDotDotDot() throws Exception {
assertThat(evalFail("//does_not_exist/...").getResultSet()).isEmpty();
assertContainsEvent("no targets found beneath 'does_not_exist'");
assertContainsEvent("Results may be inaccurate");
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile_TBD()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile("//foo/...", 1);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile_TIP()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile("//foo/foo:all", 0);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile_ST()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile("//foo/foo:banana", 0);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile_IPAT()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile("foo/foo/banana", 0);
}
private void runTestErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile(
String queryExpression, int numExpectedTargets) throws Exception {
// Starlark imports must refer to files in packages. When the file being imported exists, but
// it has no containing package, an error should be reported for queries that involve the
// package containing that import.
// This ensures that any error message must come from query evaluation, not universe evaluation
// (in the case of SkyQueryEnvironment).
helper.setBlockUniverseEvaluationErrors(true);
// The package "//foo" can be loaded and has no errors.
writeFile("foo/BUILD", "sh_library(name='apple', srcs=['apple.sh'])");
// The package "//foo/foo" has a load statement that fails. Its ":banana" target does not depend
// on the load, but because the package failed to load, it does not exist.
writeFile(
"foo/foo/BUILD",
"load('//bar:lib.bzl', 'myfunc')",
"sh_library(name='banana', srcs=['banana.sh'])");
// This Starlark file is fine, but it has no containing package, so it can't be loaded.
writeFile("bar/lib.bzl", "custom_rule(name = 'myfunc')");
assertThat(evalFail(queryExpression).getResultSet()).hasSize(numExpectedTargets);
String expectedError =
"error loading package 'foo/foo': Every .bzl file must have a corresponding package";
assertContainsEvent(expectedError);
}
@Test
public void testPluralErrorsReportedWhenStarlarkLoadRefersToMissingPkgExistingFile()
throws Exception {
// This test does not yet pass for some SkyQueryEnvironment-specific QueryExpression
// implementations.
// Like runTestErrorReportedWhenStarlarkLoadRefersToMissingPkgExistingFile, but with multiple
// packages in error, testing that each packages' error is reported.
// The package "//foo" can be loaded and has no errors.
writeFile("foo/BUILD", "sh_library(name='apple', srcs=['apple.sh'])");
// The packages "//foo/foo" and "//foo/foo2" each have a load statement that fails. The
// ":banana" targets do not depend on the load, but because the packages failed to load, they do
// not exist.
writeFile(
"foo/foo/BUILD",
"load('//bar:lib.bzl', 'myfunc')",
"sh_library(name='banana', srcs=['banana.sh'])");
writeFile(
"foo/foo2/BUILD",
"load('//bar:lib.bzl', 'myfunc')",
"sh_library(name='banana', srcs=['banana.sh'])");
// This Starlark file is fine, but it has no containing package, so it can't be loaded.
writeFile("bar/lib.bzl", "custom_rule(name = 'myfunc')");
assertThat(evalFail("//foo/foo:*").getResultSet()).isEmpty();
String expectedError =
"error loading package 'foo/foo': Every .bzl file must have a corresponding package, "
+ "but '//bar:lib.bzl' does not have one";
assertContainsEvent(expectedError);
helper.clearEvents();
assertThat(evalFail("//foo/foo2:*").getResultSet()).isEmpty();
String expectedError2 =
"error loading package 'foo/foo2': Every .bzl file must have a corresponding package, "
+ "but '//bar:lib.bzl' does not have one";
assertContainsEvent(expectedError2);
helper.clearEvents();
assertThat(evalFail("//foo/foo:* + //foo/foo2:*").getResultSet()).isEmpty();
assertContainsEvent(expectedError);
assertContainsEvent(expectedError2);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile_TBD()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile("//foo/...", 1);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile_TIP()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile("//foo/foo:all", 0);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile_ST()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile("//foo/foo:banana", 0);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile_IPAT()
throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile("foo/foo/banana", 0);
}
private void runTestErrorReportedWhenStarlarkLoadRefersToExistingPkgMissingFile(
String queryExpression, int numExpectedTargets) throws Exception {
// Starlark imports must refer to files that exist, otherwise they will fail and an error should
// be reported. How shocking!
// This ensures that any error message must come from query evaluation, not universe evaluation
// (in the case of SkyQueryEnvironment).
helper.setBlockUniverseEvaluationErrors(true);
// The package "//foo" can be loaded and has no errors.
writeFile("foo/BUILD", "sh_library(name='apple', srcs=['apple.sh'])");
// The package "//foo/foo" has a load statement that fails. Its ":banana" target does not depend
// on the load, but because the package failed to load, it does not exist.
writeFile(
"foo/foo/BUILD",
"load('//bar:lib.bzl', 'myfunc')",
"sh_library(name='banana', srcs=['banana.sh'])");
// The load statement in "//foo/foo" refers to an existing package, but the Starlark file is
// missing.
writeFile("bar/BUILD");
assertThat(evalFail(queryExpression).getResultSet()).hasSize(numExpectedTargets);
String expectedError =
"error loading package 'foo/foo': cannot load '//bar:lib.bzl': no such file";
assertContainsEvent(expectedError);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle_TBD() throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle("//foo/...", 1);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle_TIP() throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle("//foo/foo:all", 0);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle_ST() throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle("//foo/foo:banana", 0);
}
@Test
public void testErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle_IPAT() throws Exception {
runTestErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle("foo/foo/banana", 0);
}
private void runTestErrorReportedWhenStarlarkLoadRefersToFileInSymlinkCycle(
String queryExpression, int numExpectedTargets) throws Exception {
// Starlark imports must refer to files that don't point into a symlink cycle, otherwise they
// will fail and an error should be reported. Quite astonishing!
// This ensures that any error message must come from query evaluation, not universe evaluation
// (in the case of SkyQueryEnvironment).
helper.setBlockUniverseEvaluationErrors(true);
// The package "//foo" can be loaded and has no errors.
writeFile("foo/BUILD", "sh_library(name='apple', srcs=['apple.sh'])");
// The package "//foo/foo" has a load statement that fails. Its ":banana" target does not depend
// on the load, but because the package failed to load, it does not exist.
writeFile(
"foo/foo/BUILD",
"load('//bar:lib.bzl', 'myfunc')",
"sh_library(name='banana', srcs=['banana.sh'])");
// The load statement in "//foo/foo" refers to an existing package, but the Starlark file the
// load statement refers to points into a symlink cycle.
writeFile("bar/BUILD");
ensureSymbolicLink("bar/lib.bzl", "bar/recursion");
ensureSymbolicLink("bar/recursion", "bar/recursion");
assertThat(evalFail(queryExpression).getResultSet()).hasSize(numExpectedTargets);
String expectedError =
"error loading package 'foo/foo': Encountered error while reading extension file"
+ " 'bar/lib.bzl': Symlink cycle";
assertContainsEvent(expectedError);
}
@Test
public void testNoErrorReportedWhenUniverseIncludesBrokenPkgButQueryDoesNot() throws Exception {
// The SkyQueryEnvironment implementation can emit errors from two sources: graph evaluation
// to prepare the query's universe scope, and query evaluation (which includes things like
// reading packages out of the graph). Whether the SkyQueryEnvironment emits errors during graph
// evaluation of the universe is controlled by the blockUniverseEvaluationErrors parameter (on
// QueryEnvironmentFactory#create and so on).
//
// The BlazeQueryEnvironment implementation never emits errors during universe evaluation,
// because it doesn't *do* universe evaluation. Its graph evaluation is limited to evaluating
// the target patterns that appear in the query expression.
//
// This test asserts that, when told to block errors that only occur during universe evaluation,
// neither QueryEnvironment implementation reports them.
helper.setBlockUniverseEvaluationErrors(true);
// The package "//foo" is healthy.
writeFile("foo/BUILD", "sh_library(name='apple', srcs=['apple.sh'])");
// The package "//baz" is not healthy: it contains a load statement referring to an unpackaged
// Starlark file.
writeFile(
"baz/BUILD",
"load('//bar:lib.bzl', 'myfunc')",
"sh_library(name='banana', srcs=['banana.sh'])");
writeFile("bar/lib.bzl", "custom_rule(name = 'myfunc')");
// Nevertheless, a query affecting just the healthy package emits no errors.
assertThat(eval("//foo/...")).hasSize(1);
assertDoesNotContainEvent("error loading package 'baz'");
}
@Override
@Test
public void boundedRdepsWithError() throws Exception {
writeFile(
"foo/BUILD",
"sh_library(name = 'foo', deps = [':dep'])",
"sh_library(name = 'dep', deps = ['//bar:missing'])");
ResultAndTargets<Target> targetResultAndTargets = evalFail("rdeps(//foo:foo, //foo:dep, 1)");
assertThat(
targetResultAndTargets.getResultSet().stream()
.map(t -> Label.print(t.getLabel()))
.collect(toImmutableSet()))
.containsExactly("//foo:dep", "//foo:foo");
// Ideally we wouldn't print this irrelevant error (since //bar:missing is a dep of //foo:dep,
// not an rdep), or make it fail the query.
assertThat(targetResultAndTargets.getQueryEvalResult().getDetailedExitCode().getExitCode())
.isEqualTo(ExitCode.BUILD_FAILURE);
assertContainsEvent("no such package 'bar':");
}
@Test
public void testIgnoredPackagePrefixIsTBDQuery() throws Exception {
writeFile(helper.getIgnoredPackagePrefixesFile().getPathString(), "a/b");
writeFile("a/BUILD", "filegroup(name = 'a')");
writeFile("a/b/BUILD", "filegroup(name = 'a_b')");
writeFile("a/b/c/BUILD", "filegroup(name = 'a_b_c')");
// Ensure that modified files are invalidated in the skyframe. If a file has
// already been read prior to the test's writes, this forces the query to
// pick up the modified versions.
helper.maybeHandleDiffs();
ResultAndTargets<Target> resultAndTargets = helper.evaluateQuery("//a/b/...");
assertContainsEvent("Pattern '//a/b/...' was filtered out by ignored directory 'a/b'");
assertThat(resultAndTargets.getQueryEvalResult().getSuccess()).isTrue();
assertThat(targetLabels(resultAndTargets.getResultSet())).isEmpty();
}
@Test
public void bogusVisibility() throws Exception {
writeFile(
"foo/BUILD",
"package(default_visibility = ['//visibility:public'])",
"sh_library(name = 'a', visibility = ['//bar:__pkg__', '//bad:visibility'])",
"sh_library(name = 'b')",
"sh_library(name = 'c', visibility = ['//bad:visibility'])");
writeFile("bar/BUILD");
ResultAndTargets<Target> resultAndTargets =
helper.evaluateQuery("visible(//bar:BUILD, //foo:all)");
assertThat(resultAndTargets.getQueryEvalResult().getSuccess()).isFalse();
assertThat(targetLabels(resultAndTargets.getResultSet())).containsExactly("//foo:a", "//foo:b");
assertContainsEvent("Invalid visibility label '//bad:visibility': no such package 'bad'");
assertContainsEvent("--keep_going specified, ignoring errors. Results may be inaccurate");
}
}