blob: 00881b8b4b00b9577ed4bc8baacbc12240de81ff [file] [log] [blame]
// Copyright 2025 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.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.testutil.TestConstants;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class MaterializerRulesTest extends AnalysisTestCase {
@Before
public void enableDormantDeps() throws Exception {
useConfiguration(
"--experimental_dormant_deps", "--incompatible_package_group_has_public_syntax");
}
@Before
public void writeMaterializerRulesAllowlist() throws Exception {
scratch.overwriteFile(
TestConstants.TOOLS_REPOSITORY_SCRATCH
+ "tools/allowlists/materializer_rule_allowlist/BUILD",
"""
package_group(
name = 'materializer_rule_allowlist',
packages = ["public"],
)
""");
}
/** Tests materializing dormant deps through materializer rules. */
@Test
public void basicMaterializerRule_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":component_selector",
":zzz",
],
)
component_selector(
name = "component_selector",
all_components = [
":a_yes",
":b_yes",
":a_no",
":b_no",
],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "a_no")
component(name = "b_no")
component(name = "zzz")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild))
.containsExactly("aaa.txt", "a_yes.txt", "b_yes.txt", "zzz.txt");
}
/** Tests that multiple materializer rules in an attribute works. */
@Test
public void multipleMaterializerRules_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":component_selector",
":component_selector_2",
":zzz",
],
)
component_selector(
name = "component_selector",
all_components = [
":a_yes",
":b_yes",
":a_no",
":b_no",
],
)
component_selector(
name = "component_selector_2",
all_components = [
":c_yes",
":d_yes",
":c_no",
":d_no",
],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "a_no")
component(name = "b_no")
component(name = "c_yes")
component(name = "d_yes")
component(name = "c_no")
component(name = "d_no")
component(name = "zzz")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild))
.containsExactly("aaa.txt", "a_yes.txt", "b_yes.txt", "c_yes.txt", "d_yes.txt", "zzz.txt");
}
@Test
public void multipleMaterializersReturnSameTarget_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":component_selector",
":component_selector_2",
":zzz",
],
)
component_selector(
name = "component_selector",
all_components = [
":a_yes",
":b_yes", # <- overlap
":c_yes", # <- overlap
":a_no",
":b_no",
],
)
component_selector(
name = "component_selector_2",
all_components = [
":b_yes", # <- overlap
":c_yes", # <- overlap
":d_yes",
":c_no",
":d_no",
],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "a_no")
component(name = "b_no")
component(name = "c_yes")
component(name = "d_yes")
component(name = "c_no")
component(name = "d_no")
component(name = "zzz")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild))
.containsExactly("aaa.txt", "a_yes.txt", "b_yes.txt", "c_yes.txt", "d_yes.txt", "zzz.txt");
}
@Test
public void materializerForDependencyResolutionRule_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Module Implementation ######################################
ModuleImplementationInfo = provider(fields = ["output"])
def _module_implementation_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ModuleImplementationInfo(output = f)
module_implementation = rule(
implementation = _module_implementation_impl,
provides = [ModuleImplementationInfo],
)
# Module ########################################
# A provider containing a number of modules
ModulesInfo = provider(fields = ["interface", "modules"])
# A provider describing a single module
SingleModuleInfo = provider(fields = ["interface", "implementation"])
# A module collects every other module in its transitive closure.
def _module_impl(ctx):
me = SingleModuleInfo(
interface = ctx.attr.interface,
implementation = ctx.attr.implementation,
)
modules = depset(direct = [me], transitive = [dep[ModulesInfo].modules for dep in ctx.attr.deps])
return ModulesInfo(interface = ctx.attr.interface, modules = modules)
module = rule(
implementation = _module_impl,
dependency_resolution_rule = True, # Accessible from materializers
attrs = {
"interface": attr.string(),
"implementation": attr.dormant_label(),
"deps": attr.label_list(),
},
)
# Materializer rule #############################
def _module_materializer_impl(ctx):
selected = []
for m in ctx.attr.all_modules:
mi = m[ModulesInfo]
if mi.interface == "yes":
selected.extend([m.implementation for m in mi.modules.to_list()])
return MaterializedDepsInfo(deps = selected)
module_materializer = materializer_rule(
implementation = _module_materializer_impl,
attrs = {
"all_modules": attr.label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [m[ModuleImplementationInfo].output for m in ctx.attr.modules]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"modules": attr.label_list(providers = [ModuleImplementationInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "module_implementation", "module", "module_materializer", "binary")
binary(
name = "bin",
modules = [
":module_materializer",
],
)
module_materializer(
name = "module_materializer",
all_modules = [
":foo_a",
":foo_b",
":bar_a",
":bar_b",
],
)
module(name = "foo_a", interface = "yes", implementation = ":foo_a_impl", deps = [":baz_a"])
module(name = "foo_b", interface = "yes", implementation = ":foo_b_impl", deps = [":baz_b"])
module(name = "bar_a", interface = "no", implementation = ":bar_a_impl")
module(name = "bar_b", interface = "no", implementation = ":bar_b_impl")
module(name = "baz_a", implementation = ":baz_a_impl")
module(name = "baz_b", implementation = ":baz_b_impl")
module_implementation(name = "foo_a_impl")
module_implementation(name = "foo_b_impl")
module_implementation(name = "bar_a_impl")
module_implementation(name = "bar_b_impl")
module_implementation(name = "baz_a_impl")
module_implementation(name = "baz_b_impl")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild))
.containsExactly("foo_a_impl.txt", "foo_b_impl.txt", "baz_a_impl.txt", "baz_b_impl.txt");
}
@Test
public void materializerWithRealDeps_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.label_list(), # Only allows For Dependency Resolution rules
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [ctx.attr.dep[ComponentInfo].output]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo], mandatory = True),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [":component_selector"],
)
component_selector(
name = "component_selector",
all_components = [":a_yes", ":b_no", ":c_no", ":d_no"],
)
component(name = "a_yes")
component(name = "b_no")
component(name = "c_no")
component(name = "d_no")
""");
this.reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:bin"));
assertContainsEvent(
"in all_components attribute of component_selector rule //:component_selector: materializer"
+ " rules can depend on only dependency resolution rules via non-dormant attributes;"
+ " all_components is a non-dormant attribute, and //:a_yes is not a dependency"
+ " resolution rule");
assertContainsEvent(
"in all_components attribute of component_selector rule //:component_selector: materializer"
+ " rules can depend on only dependency resolution rules via non-dormant attributes;"
+ " all_components is a non-dormant attribute, and //:b_no is not a dependency"
+ " resolution rule");
assertContainsEvent(
"in all_components attribute of component_selector rule //:component_selector: materializer"
+ " rules can depend on only dependency resolution rules via non-dormant attributes;"
+ " all_components is a non-dormant attribute, and //:c_no is not a dependency"
+ " resolution rule");
assertContainsEvent(
"in all_components attribute of component_selector rule //:component_selector: materializer"
+ " rules can depend on only dependency resolution rules via non-dormant attributes;"
+ " all_components is a non-dormant attribute, and //:d_no is not a dependency"
+ " resolution rule");
}
@Test
public void dormantDepsNotAnalyzed() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider()
def _component_impl(ctx):
return ComponentInfo()
component = rule(
implementation = _component_impl,
)
# Fail Component ######################################
def _fail_component_impl(ctx):
fail("component " + ctx.label.name + " should not be analyzed")
fail_component = rule(
implementation = _fail_component_impl,
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if not "fail" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
return DefaultInfo()
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "fail_component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":component_selector",
],
)
component_selector(
name = "component_selector",
all_components = [
":a_fail",
":b",
":c_fail",
],
)
fail_component(name = "a_fail")
component(name = "b")
fail_component(name = "c_fail")
""");
// No assertion needed, the test passes if update() does not throw an exception.
update("//:bin");
}
@Test
public void aspectsThroughMaterializerRules_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider()
def _component_impl(ctx):
return ComponentInfo()
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components_dormant:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components_dormant": attr.dormant_label_list(),
},
)
# Aspect #########################################
AspectInfo = provider(fields = ["info_artifact"])
def _mt_aspect_impl(target, ctx):
if ctx.rule.kind != "component":
return []
artifact = ctx.actions.declare_file(target.label.name + ".info")
ctx.actions.write(artifact, str(target.label))
return AspectInfo(info_artifact = artifact)
mt_aspect = aspect(
implementation = _mt_aspect_impl,
attrs = {
# Context creation code is shared between aspects and rules, so having this aspect
# dependency in the test ensure that materializer dependency validation is only performed
# for rules and not for aspects.
"_tool": attr.label(
default = Label("//:aspect_tool"),
cfg = "exec",
),
}
)
# Binary #########################################
def _binary_impl(ctx):
for dep in ctx.attr.deps:
print(dep[AspectInfo])
return DefaultInfo(files = depset(direct = [dep[AspectInfo].info_artifact for dep in ctx.attr.deps]))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo], aspects = [mt_aspect]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin_dormant",
deps = [
":aaa",
":component_selector_dormant",
":zzz",
],
)
component_selector(
name = "component_selector_dormant",
all_components_dormant = [
":a_yes",
":b_yes",
":a_no",
":b_no",
],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "a_no")
component(name = "b_no")
component(name = "zzz")
genrule(
name = "aspect_tool",
outs = ["tool"],
executable = True,
cmd = "echo 'touch $$1' > $@",
)
""");
update("//:bin_dormant");
ConfiguredTarget targetDormant = getConfiguredTarget("//:bin_dormant");
NestedSet<Artifact> filesToBuildDormant =
targetDormant.getProvider(FileProvider.class).getFilesToBuild();
// The .info files come from the aspect, and only the files from the selected dormant deps
// should be returned.
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuildDormant))
.containsExactly("aaa.info", "a_yes.info", "b_yes.info", "zzz.info");
}
@Test
public void materializerToMaterializer_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
#################################################
# Component
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
#################################################
# Component selector
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
#################################################
# Binary
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [":component_selector"],
)
component_selector(
name = "component_selector",
all_components = [":component_selector2_yes", ":a_yes"],
)
component_selector(
name = "component_selector2_yes",
all_components = [":component_selector3_yes", ":b_yes", ":component_selector4_yes", ":x_no"],
)
component_selector(
name = "component_selector3_yes",
all_components = [":c_yes", ":d_yes", "e_yes", ":f_yes", ":y_no"],
)
component_selector(
name = "component_selector4_yes",
all_components = [":g_yes", ":h_yes", ":z_no"],
)
component(name = "a_yes")
component(name = "b_yes")
component(name = "c_yes")
component(name = "d_yes")
component(name = "e_yes")
component(name = "f_yes")
component(name = "g_yes")
component(name = "h_yes")
component(name = "x_no")
component(name = "y_no")
component(name = "z_no")
""");
this.reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:bin"));
assertContainsEvent(
"Error while evaluating materializer target in attribute 'deps' of target '//:bin':"
+ " Materializer target //:component_selector depends on another materializer target"
+ " //:component_selector2_yes, which is not supported.");
}
/**
* Tests that an alias can point to a materializer rule (i.e. a materializer rule can go through
* an alias). This is particularly important for materializer rules that materialize more than one
* label, because the "actual" attribute of alias() is a single label attribute, so putting a
* one-to-many materializer there would normally be disallowed.
*/
@Test
public void aliasToMaterializerRule_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider()
def _component_impl(ctx):
return ComponentInfo()
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components_dormant:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components_dormant": attr.dormant_label_list(),
},
)
# Aspect #########################################
AspectInfo = provider(fields = ["info_artifact"])
def _mt_aspect_impl(target, ctx):
if ctx.rule.kind != "component":
return []
print("aspect visiting target: " + str(target.label))
artifact = ctx.actions.declare_file(target.label.name + ".info")
ctx.actions.write(artifact, str(target.label))
return AspectInfo(info_artifact = artifact)
mt_aspect = aspect(
implementation = _mt_aspect_impl,
)
# Binary #########################################
def _binary_impl(ctx):
return DefaultInfo(files = depset(direct = [dep[AspectInfo].info_artifact for dep in ctx.attr.deps]))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo], aspects = [mt_aspect]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
# Dormant through single alias ####################################
binary(
name = "bin",
deps = [
":aaa",
":component_selector_alias",
":zzz",
],
)
alias(
name = "component_selector_alias",
actual = ":component_selector",
)
# Dormant through alias chain ####################################
binary(
name = "bin_alias_chain",
deps = [
":aaa",
":component_selector_alias_alias",
":zzz",
],
)
alias(
name = "component_selector_alias_alias",
actual = ":component_selector_alias",
)
# Materializer rules #############################
component_selector(
name = "component_selector",
all_components_dormant = [
":a_yes",
":b_yes",
":a_no",
":b_no",
],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "a_no")
component(name = "b_no")
component(name = "zzz")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild))
.containsExactly("aaa.info", "a_yes.info", "b_yes.info", "zzz.info");
eventCollector.clear();
cleanSkyframe();
update("//:bin_alias_chain");
ConfiguredTarget targetAliasChain = getConfiguredTarget("//:bin_alias_chain");
NestedSet<Artifact> filesToBuildAliasChain =
targetAliasChain.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuildAliasChain))
.containsExactly("aaa.info", "a_yes.info", "b_yes.info", "zzz.info");
// Especially when going through an alias chain, an aspect should still visit the nodes once.
assertContainsEventWithFrequency("aspect visiting target: @@//:aaa", 1);
assertContainsEventWithFrequency("aspect visiting target: @@//:zzz", 1);
assertContainsEventWithFrequency("aspect visiting target: @@//:b_yes", 1);
assertContainsEventWithFrequency("aspect visiting target: @@//:a_yes", 1);
assertDoesNotContainEvent("aspect visiting target: @@//:a_no");
assertDoesNotContainEvent("aspect visiting target: @@//:b_no");
}
/** Tests that a materializer can point to an alias and the final target is materialized. */
@Test
public void materializerToAlias_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components_dormant:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components_dormant": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":component_selector",
":zzz",
],
)
component_selector(
name = "component_selector",
all_components_dormant = [
":a_yes_alias",
":b_yes_alias",
":a_no",
":b_no",
":b_no_alias",
],
)
alias(name = "a_yes_alias", actual = "a")
alias(name = "b_yes_alias", actual = "b")
alias(name = "b_no_alias", actual = "b_no")
component(name = "aaa")
component(name = "a")
component(name = "b")
component(name = "a_no")
component(name = "b_no")
component(name = "zzz")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild))
.containsExactly("aaa.txt", "a.txt", "b.txt", "zzz.txt");
}
/**
* Tests alias -> materializer -> alias -> materializer -> alias throws an error that a
* materializer depends on a materializer.
*/
@Test
public void aliasToMaterializerToAliasToMaterializerToAlias_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
return MaterializedDepsInfo(deps = [ctx.attr.component])
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"component": attr.dormant_label(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":alias_to_materializer_to_alias_to_materializer_to_alias",
":zzz",
],
)
alias(
name = "alias_to_materializer_to_alias_to_materializer_to_alias",
actual = ":materializer_to_alias_to_materializer_to_alias",
)
component_selector(
name = "materializer_to_alias_to_materializer_to_alias",
component = ":alias_to_materializer_to_alias",
)
alias(
name = "alias_to_materializer_to_alias",
actual = ":materializer_to_alias",
)
component_selector(
name = "materializer_to_alias",
component = ":a_alias",
)
alias(
name = "a_alias",
actual = ":a",
)
component(name = "aaa")
component(name = "a")
component(name = "zzz")
""");
this.reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:bin"));
assertContainsEvent(
"Error while evaluating materializer target in attribute 'deps' of target '//:bin':"
+ " Materializer target //:materializer_to_alias_to_materializer_to_alias depends on"
+ " another materializer target //:materializer_to_alias, which is not supported");
}
private void writeMaterializerSplitTransitionBzlFile() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component selector setting ###########################################
ComponentSelectorProvider = provider(fields = ["selector"])
def _component_selector_setting_impl(ctx):
return ComponentSelectorProvider(selector = ctx.build_setting_value)
component_selector_setting = rule(
implementation = _component_selector_setting_impl,
build_setting = config.string()
)
# Component transition
def _component_selector_setting_transition_impl(settings, attr):
return [
{"//:component_selector_setting" : "foo"},
{"//:component_selector_setting" : "bar"},
]
component_transition = transition(
implementation = _component_selector_setting_transition_impl,
inputs = [],
outputs = ["//:component_selector_setting"],
)
# Component ############################################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector ###################################################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary ###############################################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo], cfg = component_transition),
},
)
""");
}
/** Tests a materializer rule under a split transition. */
@Test
public void materializerRulesUnderSplitTransition_works() throws Exception {
writeMaterializerSplitTransitionBzlFile();
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary", "component_selector_setting")
component_selector_setting(
name = "component_selector_setting",
build_setting_default = "foo",
)
binary(
name = "bin",
# has a split transition!
deps = [":component_selector"],
)
component_selector(
name = "component_selector",
all_components = [
":yes_a",
":yes_b",
":no_c",
":no_d",
],
)
component(name = "yes_a")
component(name = "yes_b")
component(name = "no_c")
component(name = "no_d")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
List<String> artifactNames = ActionsTestUtil.baseArtifactNames(filesToBuild);
assertThat(artifactNames).containsAtLeast("yes_a.txt", "yes_b.txt");
assertThat(artifactNames).containsNoneOf("no_c.txt", "no_d.txt");
}
/**
* Tests a materializer rule under a split transition with a select() input to the materializer.
*/
@Test
public void materializerRulesUnderSplitTransitionAndSelect_works() throws Exception {
writeMaterializerSplitTransitionBzlFile();
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary", "component_selector_setting")
component_selector_setting(
name = "component_selector_setting",
build_setting_default = "foo",
)
config_setting(
name = "config_setting_foo",
flag_values = {"//:component_selector_setting": "foo"},
)
config_setting(
name = "config_setting_bar",
flag_values = {"//:component_selector_setting": "bar"},
)
binary(
name = "bin",
# has a split transition!
deps = [":component_selector"],
)
component_selector(
name = "component_selector",
all_components = select({
":config_setting_foo": [
":yes_a",
":yes_b",
":no_f",
":no_g",
],
":config_setting_bar": [
":yes_c",
":yes_d",
":yes_e",
":no_f",
":no_g",
],
}),
)
component(name = "yes_a")
component(name = "yes_b")
component(name = "yes_c")
component(name = "yes_d")
component(name = "yes_e")
component(name = "no_f")
component(name = "no_g")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
List<String> artifactNames = ActionsTestUtil.baseArtifactNames(filesToBuild);
assertThat(artifactNames)
.containsAtLeast("yes_a.txt", "yes_b.txt", "yes_c.txt", "yes_d.txt", "yes_e.txt");
assertThat(artifactNames).containsNoneOf("no_f.txt", "no_g.txt");
}
@Test
public void materializerRulesPropagatesValidationActions_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
ctx.actions.run_shell(
inputs = [f],
outputs = [validation_output],
command = "touch $1",
arguments = [validation_output.path],
)
return [
ComponentInfo(output = f),
OutputGroupInfo(_validation = depset([validation_output])),
]
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":component_selector",
":zzz",
],
)
component_selector(
name = "component_selector",
all_components = [
":a_yes",
":b_yes",
":a_no",
":b_no",
],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "a_no")
component(name = "b_no")
component(name = "zzz")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
OutputGroupInfo outputGroupInfo = target.get(OutputGroupInfo.STARLARK_CONSTRUCTOR);
NestedSet<Artifact> validationOutputs =
outputGroupInfo.getOutputGroup(OutputGroupInfo.VALIDATION);
List<String> artifactNames = ActionsTestUtil.baseArtifactNames(validationOutputs);
assertThat(artifactNames)
.containsExactly(
"aaa.validation", "a_yes.validation", "b_yes.validation", "zzz.validation");
}
/**
* Tests that a materializer rule getting read by a materializer in a materializer attribute
* resolves.
*/
@Test
public void materializerTargetInMaterializerAttribute_works() throws Exception {
scratch.file(
"defs.bzl",
"""
ComponentInfo = provider(fields = ["components"])
def _component_impl(ctx):
current = struct(label=ctx.label, impl = ctx.attr.impl)
transitive = [d[ComponentInfo].components for d in ctx.attr.deps]
return [
ComponentInfo(components = depset(direct = [current], transitive = transitive)),
]
component = rule(
implementation = _component_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
"impl": attr.dormant_label(),
},
provides = [ComponentInfo],
dependency_resolution_rule = True,
)
######################################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
######################################
def _binary_impl(ctx):
return [DefaultInfo(files=depset(ctx.files._impls))]
def _materializer(ctx):
for c in ctx.attr.components:
print(c)
return []
binary = rule(
implementation = _binary_impl,
attrs = {
"components": attr.label_list(for_dependency_resolution = True),
"_impls": attr.label_list(materializer = _materializer),
}
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component_selector", "component", "binary")
binary(
name = "materializer_attr_bin",
components = [":a_yes", ":b_no", ":component_selector"],
)
component_selector(
name = "component_selector",
all_components = [
":c_yes",
":d_no",
],
)
component(name="a_yes")
component(name="b_no")
component(name="c_yes")
component(name="d_no")
""");
update("//:materializer_attr_bin");
// no ending ">" because after the label are the providers which are not important here.
assertContainsEvent("<target //:a_yes");
assertContainsEvent("<target //:b_no");
assertContainsEvent("<target //:c_yes");
// This was not selected by the materializer target, so it should not show up to the
// materializer attribute.
assertDoesNotContainEvent("<target //:d_no");
}
@Test
public void singletonListOfMaterializedDepsInfo_works() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return [MaterializedDepsInfo(deps = selected)]
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":component_selector",
":zzz",
],
)
component_selector(
name = "component_selector",
all_components = [":a_yes", ":b_yes", ":c_no", ":d_no"],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "c_no")
component(name = "d_no")
component(name = "zzz")
""");
update("//:bin");
ConfiguredTarget target = getConfiguredTarget("//:bin");
NestedSet<Artifact> filesToBuild = target.getProvider(FileProvider.class).getFilesToBuild();
assertThat(ActionsTestUtil.baseArtifactNames(filesToBuild))
.containsExactly("aaa.txt", "a_yes.txt", "b_yes.txt", "zzz.txt");
}
@Test
public void materializerRuleDocsAttr_works() throws Exception {
scratch.file(
"defs.bzl",
"""
materializer_rule_with_doc = materializer_rule(
implementation = lambda ctx: MaterializedDepsInfo(deps = []),
doc = "This is a doc string",
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "materializer_rule_with_doc")
materializer_rule_with_doc(
name = "materializer_rule_with_doc",
)
""");
update("//:materializer_rule_with_doc");
ConfiguredTargetAndData target = getConfiguredTargetAndData("//:materializer_rule_with_doc");
assertThat(target.getRuleClassObject().getStarlarkDocumentation())
.isEqualTo("This is a doc string");
}
@Test
public void materializerRuleInWithDefaultApplicableLicenses_works() throws Exception {
scratch.file(
"fake_licenses/BUILD",
"""
filegroup(
name = "license",
srcs = ["LICENSE"],
)
""");
scratch.file(
"defs.bzl",
"""
mr = materializer_rule(
implementation = lambda ctx: MaterializedDepsInfo(deps = []),
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "mr")
package(
default_applicable_licenses = ["//fake_licenses:license"],
)
mr(
name = "materializer_rule",
)
""");
update("//:materializer_rule");
}
@Test
public void materializerAllowList_nonAllowedThrowsError() throws Exception {
scratch.overwriteFile(
TestConstants.TOOLS_REPOSITORY_SCRATCH
+ "tools/allowlists/materializer_rule_allowlist/BUILD",
"""
package_group(
name = 'materializer_rule_allowlist',
packages = [],
)
""");
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [dep[ComponentInfo].output for dep in ctx.attr.deps]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [
":aaa",
":component_selector",
":zzz",
],
)
component_selector(
name = "component_selector",
all_components = [":a_yes", ":b_no", ":c_no", ":d_no"],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_no")
component(name = "c_no")
component(name = "d_no")
component(name = "zzz")
""");
this.reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:bin"));
assertContainsEvent(
"in component_selector rule //:component_selector: Non-allowlisted use of materializer"
+ " rule");
}
/**
* Tests that an error is thrown when a materializer rule returns something other than a
* MaterializedDepsInfo provider or singleton list thereof.
*/
@Test
public void materializerReturnsNonMaterializedDepsInfo_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def make_materializer_rule(impl):
return materializer_rule(
implementation = impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
materializer_wrong_object_DefaultInfo = make_materializer_rule(lambda ctx: DefaultInfo())
materializer_wrong_object_int = make_materializer_rule(lambda ctx: 1)
materializer_wrong_object_in_list = make_materializer_rule(lambda ctx: [DefaultInfo()])
materializer_wrong_object_in_list_int = make_materializer_rule(lambda ctx: [1])
materializer_wrong_list_size = make_materializer_rule(lambda ctx: [DefaultInfo(), ComponentInfo()])
materializer_wrong_list_size_DefaultInfo_int = make_materializer_rule(lambda ctx: [DefaultInfo(), 1])
# Binary #########################################
def _binary_impl(ctx):
return DefaultInfo()
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl",
"component",
"binary",
"materializer_wrong_object_DefaultInfo",
"materializer_wrong_object_int",
"materializer_wrong_object_in_list",
"materializer_wrong_object_in_list_int",
"materializer_wrong_list_size",
"materializer_wrong_list_size_DefaultInfo_int")
binary(name = "bin_materializer_wrong_object_DefaultInfo", deps = [":materializer_wrong_object_DefaultInfo"])
materializer_wrong_object_DefaultInfo(name = "materializer_wrong_object_DefaultInfo", all_components = [":a_yes", ":b_yes"])
binary(name = "bin_materializer_wrong_object_int", deps = [":materializer_wrong_object_int"])
materializer_wrong_object_int(name = "materializer_wrong_object_int", all_components = [":a_yes", ":b_yes"])
binary(name = "bin_materializer_wrong_object_in_list", deps = [":materializer_wrong_object_in_list"])
materializer_wrong_object_in_list(name = "materializer_wrong_object_in_list", all_components = [":a_yes", ":b_yes"])
binary(name = "bin_materializer_wrong_object_in_list_int", deps = [":materializer_wrong_object_in_list_int"])
materializer_wrong_object_in_list_int(name = "materializer_wrong_object_in_list_int", all_components = [":a_yes", ":b_yes"])
binary(name = "bin_materializer_wrong_list_size", deps = [":materializer_wrong_list_size"])
materializer_wrong_list_size(name = "materializer_wrong_list_size", all_components = [":a_yes", ":b_yes"])
binary(name = "bin_materializer_wrong_list_size_DefaultInfo_int", deps = [":materializer_wrong_list_size_DefaultInfo_int"])
materializer_wrong_list_size_DefaultInfo_int(name = "materializer_wrong_list_size_DefaultInfo_int", all_components = [":a_yes", ":b_yes"])
component(name = "a_yes")
component(name = "b_yes")
""");
reporter.removeHandler(failFastHandler);
assertThrows(
ViewCreationFailedException.class,
() -> update("//:bin_materializer_wrong_object_DefaultInfo"));
assertContainsEvent(
"Materializer rules must return exactly one MaterializedDepsInfo provider, but got"
+ " [DefaultInfo]");
eventCollector.clear();
assertThrows(
ViewCreationFailedException.class, () -> update("//:bin_materializer_wrong_object_int"));
assertContainsEvent("Rule should return a struct or a list, but got int");
eventCollector.clear();
assertThrows(
ViewCreationFailedException.class,
() -> update("//:bin_materializer_wrong_object_in_list"));
assertContainsEvent(
"Materializer rules must return exactly one MaterializedDepsInfo provider, but got"
+ " [DefaultInfo]");
eventCollector.clear();
assertThrows(
ViewCreationFailedException.class,
() -> update("//:bin_materializer_wrong_object_in_list_int"));
assertContainsEvent(
"at index 0 of result of rule implementation function, got element of type int, want Info");
eventCollector.clear();
assertThrows(
ViewCreationFailedException.class, () -> update("//:bin_materializer_wrong_list_size"));
assertContainsEvent(
"Materializer rules must return exactly one MaterializedDepsInfo provider, but got"
+ " [DefaultInfo, ComponentInfo]");
eventCollector.clear();
assertThrows(
ViewCreationFailedException.class,
() -> update("//:bin_materializer_wrong_list_size_DefaultInfo_int"));
assertContainsEvent(
"at index 1 of result of rule implementation function, got element of type int, want Info");
eventCollector.clear();
}
/** Tests that an error is thrown when a materializer goes into a single-label-typed attribute. */
@Test
public void materializerRuleInSingleLabelAttribute_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider(fields = ["output"])
def _component_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
ctx.actions.write(f, ctx.label.name)
return ComponentInfo(output = f)
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
files = [ctx.attr.dep[ComponentInfo].output]
return DefaultInfo(files = depset(direct = files))
binary = rule(
implementation = _binary_impl,
attrs = {
"dep": attr.label(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
dep = ":component_selector",
)
component_selector(
name = "component_selector",
all_components = [":a_yes", ":b_yes", ":c_no", ":d_no"],
)
component(name = "aaa")
component(name = "a_yes")
component(name = "b_yes")
component(name = "c_no")
component(name = "d_no")
component(name = "zzz")
""");
reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:bin"));
assertContainsEvent(
"Error while evaluating materializer target in attribute 'dep' of target '//:bin': Target"
+ " //:component_selector is a materializer target but attribute 'dep' is a label, not"
+ " a label list");
}
@Test
public void usingActionsInMaterializerRule_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
def _component_selector_impl(ctx):
f = ctx.actions.declare_file(ctx.label.name + ".txt")
return MaterializedDepsInfo(deps = [])
component_selector = materializer_rule(
implementation = _component_selector_impl,
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component_selector")
component_selector(
name = "component_selector",
)
""");
reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:component_selector"));
assertContainsEvent("ctx.actions is not available in materializer rules");
}
@Test
public void wrongObjectInMaterializedDepsInfo_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
def _component_impl(ctx):
return []
component = rule(
implementation = _component_impl,
)
def _component_selector_impl(ctx):
deps = ctx.attr.components + [1]
return MaterializedDepsInfo(deps = deps)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"components": attr.dormant_label_list(),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component_selector", "component")
component_selector(
name = "component_selector",
components = [":a", ":b"],
)
component(name = "a")
component(name = "b")
""");
reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:component_selector"));
assertContainsEvent(
"MaterializedDepsInfo dependencies must be Target objects (retrieved from"
+ " ctx.attr) or DormantDependency objects (from attr.dormant_label() or"
+ " attr.dormant_label_list() attributes), but got int at index 2");
}
@Test
public void materializedRuleWithWrongProvider_throwsError() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider()
def _component_impl(ctx):
return DefaultInfo()
component = rule(
implementation = _component_impl,
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "yes" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
return DefaultInfo()
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
scratch.file(
"BUILD",
"""
load(":defs.bzl", "component", "component_selector", "binary")
binary(
name = "bin",
deps = [":component_selector"],
)
component_selector(
name = "component_selector",
all_components = [":a_yes", ":b_yes"],
)
component(name = "a_yes")
component(name = "b_yes")
""");
reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//:bin"));
assertContainsEvent(
"in deps attribute of binary rule //:bin: '//:a_yes' does not have mandatory providers:"
+ " 'ComponentInfo'");
assertContainsEvent(
"in deps attribute of binary rule //:bin: '//:b_yes' does not have mandatory providers:"
+ " 'ComponentInfo'");
}
private void writeVisibilityDefsBzlFile() throws Exception {
scratch.file(
"defs.bzl",
"""
# Component ######################################
ComponentInfo = provider()
def _component_impl(ctx):
return ComponentInfo()
component = rule(
implementation = _component_impl,
provides = [ComponentInfo],
)
# Component selector #############################
def _component_selector_impl(ctx):
selected = []
for c in ctx.attr.all_components:
if "foo" in str(c.label):
selected.append(c)
return MaterializedDepsInfo(deps = selected)
component_selector = materializer_rule(
implementation = _component_selector_impl,
attrs = {
"all_components": attr.dormant_label_list(),
},
)
# Binary #########################################
def _binary_impl(ctx):
return DefaultInfo()
binary = rule(
implementation = _binary_impl,
attrs = {
"deps": attr.label_list(providers = [ComponentInfo]),
},
)
""");
}
@Test
public void materializerRuleVisibilityViolation_throwsError() throws Exception {
writeVisibilityDefsBzlFile();
scratch.file("BUILD", "");
scratch.file(
"binary1/BUILD",
"""
load("//:defs.bzl", "binary")
binary(
name = "bin1",
deps = [
"//components:component_selector",
],
)
""");
scratch.file(
"binary2/BUILD",
"""
load("//:defs.bzl", "binary")
binary(
name = "bin2",
deps = [
"//components:component_selector",
],
)
""");
scratch.file(
"components/BUILD",
"""
load("//:defs.bzl", "component", "component_selector", "binary")
component_selector(
name = "component_selector",
all_components = [
":foo_a",
":foo_b",
":bar_a",
":bar_b",
],
visibility = ["//binary1:__pkg__"],
)
component(name = "foo_a", visibility = ["//:__subpackages__"])
component(name = "foo_b", visibility = ["//:__subpackages__"])
component(name = "bar_a", visibility = ["//:__subpackages__"])
component(name = "bar_b", visibility = ["//:__subpackages__"])
""");
// The materializer target is visible to bin1.
update("//binary1:bin1");
// The materializer target is not visible to bin2.
reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//binary2:bin2"));
assertContainsEvent(
"""
ERROR /workspace/binary2/BUILD:3:7: in binary rule //binary2:bin2: Visibility error:
target '//components:component_selector' is not visible from
target '//binary2:bin2'
""");
}
@Test
public void materializerRuleMaterializedTargetVisibilityViolation_throwsError() throws Exception {
scratch.file("BUILD", "");
writeVisibilityDefsBzlFile();
scratch.file(
"binary/BUILD",
"""
load("//:defs.bzl", "binary")
binary(
name = "bin",
deps = [
"//components:component_selector",
],
)
""");
scratch.file(
"components/BUILD",
"""
load("//:defs.bzl", "component", "component_selector", "binary")
component_selector(
name = "component_selector",
all_components = [
":foo_a",
":foo_b",
":bar_a",
":bar_b",
],
visibility = ["//binary:__pkg__"],
)
component(name = "foo_a", visibility = ["//visibility:private"])
component(name = "foo_b", visibility = ["//visibility:private"])
component(name = "bar_a", visibility = ["//visibility:private"])
component(name = "bar_b", visibility = ["//visibility:private"])
""");
// The materializer target is visible to bin, but the materialized targets are not.
reporter.removeHandler(failFastHandler);
assertThrows(ViewCreationFailedException.class, () -> update("//binary:bin"));
assertContainsEvent(
"""
ERROR /workspace/binary/BUILD:3:7: in binary rule //binary:bin: Visibility error:
target '//components:foo_a' is not visible from
target '//binary:bin'
""");
assertContainsEvent(
"""
ERROR /workspace/binary/BUILD:3:7: in binary rule //binary:bin: Visibility error:
target '//components:foo_b' is not visible from
target '//binary:bin'
""");
}
}