blob: 08696878ef1ef5a68bd04c98ea21846aa877cbb0 [file] [log] [blame]
// Copyright 2018 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.analysis;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.analysis.config.BuildConfigurationValue.configurationIdMessage;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.eventbus.Subscribe;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.ConfigurationId;
import com.google.devtools.build.lib.causes.AnalysisFailedCause;
import com.google.devtools.build.lib.causes.Cause;
import com.google.devtools.build.lib.causes.LoadingFailedCause;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.server.FailureDetails.Analysis;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.PackageLoading;
import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Analysis failure reporting tests. */
@RunWith(JUnit4.class)
public class AnalysisFailureReportingTest extends AnalysisTestCase {
private final AnalysisFailureEventCollector collector = new AnalysisFailureEventCollector();
// TODO(mschaller): The below is closer now because of e.g. DetailedExitCode/FailureDetail.
// original(ulfjack): Don't check for exact error message wording; instead, add machine-readable
// details to the events, and check for those. Also check if we can remove duplicate test coverage
// for these errors, i.e., consolidate the failure reporting tests in this class.
@Before
public void setup() {
// We only test failure cases in this class.
reporter.removeHandler(failFastHandler);
eventBus.register(collector);
}
@Test
public void testMissingRequiredAttribute() throws Exception {
scratch.file(
"foo/BUILD",
"genrule(name = 'foo',", // missing "out" attribute
" cmd = '')");
AnalysisResult result = update(eventBus, defaultFlags().with(Flag.KEEP_GOING), "//foo");
assertThat(result.hasError()).isTrue();
Label topLevel = Label.parseCanonicalUnchecked("//foo");
assertThat(collector.events.keySet()).containsExactly(topLevel);
Collection<Cause> topLevelCauses = collector.events.get(topLevel);
assertThat(topLevelCauses).hasSize(1);
Cause cause = Iterables.getOnlyElement(topLevelCauses);
assertThat(cause).isInstanceOf(LoadingFailedCause.class);
assertThat(cause.getLabel()).isEqualTo(topLevel);
assertThat(((LoadingFailedCause) cause).getMessage())
.isEqualTo(
"Target '//foo:foo' contains an error and its package is in error: //foo:foo: missing"
+ " value for mandatory attribute 'outs' in 'genrule' rule");
}
@Test
public void testMissingDependency() throws Exception {
scratch.file(
"foo/BUILD",
"genrule(name = 'foo',",
" tools = ['//bar'],",
" cmd = 'command',",
" outs = ['foo.txt'])");
AnalysisResult result = update(eventBus, defaultFlags().with(Flag.KEEP_GOING), "//foo");
assertThat(result.hasError()).isTrue();
Label topLevel = Label.parseCanonicalUnchecked("//foo");
Label causeLabel = Label.parseCanonicalUnchecked("//bar");
assertThat(collector.events.keySet()).containsExactly(topLevel);
assertThat(collector.events.get(topLevel))
.containsExactly(
new AnalysisFailedCause(
causeLabel,
collector.getOnlyConfigurationId(),
createPackageLoadingDetailedExitCode(
"BUILD file not found in any of the following"
+ " directories. Add a BUILD file to a directory to mark it as a"
+ " package.\n"
+ " - /workspace/bar",
Code.BUILD_FILE_MISSING)));
}
@Test
public void testExpanderFailure() throws Exception {
scratch.file(
"test/BUILD",
"genrule(",
" name = 'bad',",
" outs = ['bad.out'],",
" cmd = 'cp $< $@', # Error to use $< with no srcs",
")");
AnalysisResult result = update(eventBus, defaultFlags().with(Flag.KEEP_GOING), "//test:bad");
assertThat(result.hasError()).isTrue();
Label topLevel = Label.parseCanonicalUnchecked("//test:bad");
assertThat(collector.events.keySet()).containsExactly(topLevel);
assertThat(collector.events)
.valuesForKey(topLevel)
.containsExactly(
new AnalysisFailedCause(
topLevel,
collector.getOnlyConfigurationId(),
createAnalysisDetailedExitCode(
"in cmd attribute of genrule rule //test:bad: variable '$<' : no input file")));
}
/**
* This error gets reported twice - once when we try to analyze the //cycles1 target, and the
* other time when we analyze the //c target (which depends on //cycles1). This test checks that
* both use the same error message.
*/
@Test
public void testSymlinkCycleReportedExactlyOnce() throws Exception {
scratch.file("gp/BUILD", "sh_library(name = 'gp', deps = ['//p'])");
scratch.file("p/BUILD", "sh_library(name = 'p', deps = ['//c'])");
scratch.file("c/BUILD", "sh_library(name = 'c', deps = ['//cycles1'])");
Path cycles1BuildFilePath =
scratch.file("cycles1/BUILD", "sh_library(name = 'cycles1', srcs = glob(['*.sh']))");
cycles1BuildFilePath
.getParentDirectory()
.getRelative("cycles1.sh")
.createSymbolicLink(PathFragment.create("cycles1.sh"));
AnalysisResult result = update(eventBus, defaultFlags().with(Flag.KEEP_GOING), "//gp");
assertThat(result.hasError()).isTrue();
Label topLevel = Label.parseCanonicalUnchecked("//gp");
String message =
"Symlink issue while evaluating globs: Symlink cycle:" + " /workspace/cycles1/cycles1.sh";
Code code = Code.EVAL_GLOBS_SYMLINK_ERROR;
assertThat(collector.events.get(topLevel))
.containsExactly(
new AnalysisFailedCause(
Label.parseCanonical("//cycles1"),
collector.getOnlyConfigurationId(),
createPackageLoadingDetailedExitCode(message, code)));
}
@Test
public void testVisibilityError() throws Exception {
scratch.file("foo/BUILD", "sh_library(name = 'foo', deps = ['//bar'])");
scratch.file("bar/BUILD", "sh_library(name = 'bar', visibility = ['//visibility:private'])");
AnalysisResult result = update(eventBus, defaultFlags().with(Flag.KEEP_GOING), "//foo");
assertThat(result.hasError()).isTrue();
Label topLevel = Label.parseCanonicalUnchecked("//foo");
assertThat(collector.events.get(topLevel))
.containsExactly(
new AnalysisFailedCause(
Label.parseCanonical("//foo"),
collector.getOnlyConfigurationId(),
createAnalysisDetailedExitCode(
"in sh_library rule //foo:foo: target '//bar:bar' is not visible from"
+ " target '//foo:foo'. Check the visibility declaration of the"
+ " former target if you think the dependency is legitimate")));
}
@Test
public void testFileVisibilityError() throws Exception {
scratch.file("foo/BUILD", "sh_library(name = 'foo', srcs = ['//bar:bar.sh'])");
scratch.file("bar/BUILD", "exports_files(['bar.sh'], visibility = ['//visibility:private'])");
scratch.file("bar/bar.sh");
AnalysisResult result = update(eventBus, defaultFlags().with(Flag.KEEP_GOING), "//foo");
assertThat(result.hasError()).isTrue();
Label topLevel = Label.parseCanonicalUnchecked("//foo");
assertThat(collector.events)
.valuesForKey(topLevel)
.containsExactly(
new AnalysisFailedCause(
Label.parseCanonical("//foo"),
collector.getOnlyConfigurationId(),
DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(
"in sh_library rule //foo:foo: target '//bar:bar.sh' is not visible"
+ " from target '//foo:foo'. Check the visibility declaration of"
+ " the former target if you think the dependency is legitimate."
+ " To set the visibility of that source file target, use the"
+ " exports_files() function")
.setAnalysis(
Analysis.newBuilder()
.setCode(Analysis.Code.CONFIGURED_VALUE_CREATION_FAILED))
.build())));
}
@Test
public void testVisibilityErrorNoKeepGoing() throws Exception {
scratch.file("foo/BUILD", "sh_test(name = 'foo', srcs = ['test.sh'], deps = ['//bar'])");
scratch.file("bar/BUILD", "sh_library(name = 'bar', visibility = ['//visibility:private'])");
try {
update(eventBus, defaultFlags(), "//foo");
} catch (ViewCreationFailedException e) {
// Ignored; we check for the correct eventbus event below.
}
Label topLevel = Label.parseCanonicalUnchecked("//foo");
BuildConfigurationValue expectedConfig =
skyframeExecutor.getSkyframeBuildView().getBuildConfiguration();
String message =
"in sh_test rule //foo:foo: target '//bar:bar' is not visible from"
+ " target '//foo:foo'. Check the visibility declaration of the"
+ " former target if you think the dependency is legitimate";
assertThat(collector.events.get(topLevel))
.containsExactly(
new AnalysisFailedCause(
Label.parseCanonical("//foo"),
configurationIdMessage(expectedConfig),
createAnalysisDetailedExitCode(message)));
}
public static DetailedExitCode createPackageLoadingDetailedExitCode(String message, Code code) {
return DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(message)
.setPackageLoading(PackageLoading.newBuilder().setCode(code))
.build());
}
public static DetailedExitCode createAnalysisDetailedExitCode(String message) {
return DetailedExitCode.of(
FailureDetail.newBuilder()
.setMessage(message)
.setAnalysis(
Analysis.newBuilder().setCode(Analysis.Code.CONFIGURED_VALUE_CREATION_FAILED))
.build());
}
// TODO(ulfjack): Add more tests for
// - a target that has multiple analysis errors (in the target itself)
// - a visibility error in a dependency (not in the target itself)
// - an error in a config condition
// - a missing top-level target (does that even get this far?)
// - a top-level target with an InvalidConfigurationException
// - a top-level target with a ToolchainContextException
// - a top-level target with a visibility attribute that points to a non-package_group
// - a top-level target with a package_group that refers to a non-package_group
// - aspect errors
/** Class to collect analysis failures. */
public static class AnalysisFailureEventCollector {
private final Multimap<Label, Cause> events = HashMultimap.create();
@Subscribe
public void failureEvent(AnalysisFailureEvent event) {
ConfiguredTargetKey failedTarget = event.getFailedTarget();
events.putAll(failedTarget.getLabel(), event.getRootCauses().toList());
}
private ConfigurationId getOnlyConfigurationId() {
// Analysis errors after the target's configuration has been determined are reported using a
// possibly transitioned ID which is hard to retrieve from the graph if analysis of that
// target fails. This method simply extracts them from the event ID.
return getOnlyElement(events.entries())
.getValue()
.getIdProto()
.getConfiguredLabel()
.getConfiguration();
}
}
}