blob: dc532268b0aa33080df917696b56b5a102ab40ad [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.starlark;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
import com.google.devtools.build.lib.packages.AspectClass;
import com.google.devtools.build.lib.packages.StarlarkAspectClass;
import com.google.devtools.build.lib.skyframe.AspectKeyCreator.AspectKey;
import java.util.Map;
import javax.annotation.Nullable;
import net.starlark.java.eval.Sequence;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for Starlark aspects {@code propagation_predicate} function. */
@RunWith(JUnit4.class)
public final class StarlarkAspectsPropagationPredicateTest extends AnalysisTestCase {
private void createTestDefs(String propagationPredicate) throws Exception {
scratch.file(
"test/BUILD",
"""
load('//test:defs.bzl', 'rule_1')
rule_1(name = 'tool')
""");
scratch.file(
"test/defs.bzl",
String.format(
"""
AspectInfo = provider()
RuleInfo = provider()
%s
def _aspect_impl(target, ctx):
res = ['cmdline_aspect on ' + str(target.label)]
for dep in ctx.rule.attr.deps:
if AspectInfo in dep:
res.extend(dep[AspectInfo].res)
return [AspectInfo(res = res)]
cmdline_aspect = aspect(
implementation = _aspect_impl,
propagation_predicate = _propagation_predicate,
attrs = {
'allowed_pkg': attr.string(default = '*'),
'ignored_tag': attr.string(default = ''),
'allowed_rule_file': attr.string(default = '*'),
'allowed_rule_name': attr.string(default = '*'),
'_tool': attr.label(default = Label('//test:tool'))},
attr_aspects = ['deps'],
)
aspect_with_required_provider = aspect(
implementation = _aspect_impl,
required_providers = [RuleInfo],
propagation_predicate = _propagation_predicate,
attr_aspects = ['deps'],
)
def _rule_impl(ctx):
pass
rule_1 = rule(
implementation = _rule_impl,
attrs = {'deps': attr.label_list()},
)
rule_2 = rule(
implementation = _rule_impl,
attrs = {'deps': attr.label_list()},
)
def _out_rule_impl(ctx):
if ctx.outputs.out:
ctx.actions.write(ctx.outputs.out, 'hi')
return []
out_rule = rule(
implementation = _out_rule_impl,
attrs = {'deps': attr.label_list(), 'out': attr.output()},
)
def _rule_with_provider_impl(ctx):
return [RuleInfo()]
rule_with_provider = rule(
implementation = _rule_with_provider_impl,
attrs = {'deps': attr.label_list()},
provides = [RuleInfo],
)
aspect_on_rule = aspect(
implementation = _aspect_impl,
propagation_predicate = _propagation_predicate,
attr_aspects = ['deps'],
)
rule_with_aspect_on_deps = rule(
implementation = _rule_impl,
attrs = {'deps': attr.label_list(aspects = [aspect_on_rule])},
)
""",
propagationPredicate));
}
private void createTestPackages() throws Exception {
scratch.file(
"pkg1/BUILD",
"""
load('//test:defs.bzl', 'rule_1', 'rule_2', 'out_rule', 'rule_with_aspect_on_deps')
rule_1(name = 't1', deps = [':t2', '//pkg2:t2'], tags = ['no-aspect'])
rule_2(name = 't2', tags = ['another-tag'])
out_rule(name = 'target_with_output', out = 'my_out.txt', deps = [':t2'])
rule_with_aspect_on_deps(name = 'target_with_aspect_on_deps', deps = [':t2', '//pkg2:t2'])
alias(name = 'alias_1', actual = ':alias_2')
alias(name = 'alias_2', actual = ':actual')
rule_1(name = 'actual')
""");
scratch.file(
"pkg2/BUILD",
"""
load('//test:defs.bzl', 'rule_1', 'rule_2', 'rule_with_provider')
rule_2(name = 't1', deps = [':t2', '//pkg1:t2'])
rule_1(name = 't2', tags = ['no-aspect', 'another-tag'])
rule_with_provider(name = 'target_with_provider', deps = [':t2'])
""");
}
@Test
public void propagationPredicateOnTargetPackage_aspectPropagatedToSatisfyingTargets()
throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
if ctx.attr.allowed_pkg != '*' and ctx.rule.label.package != ctx.attr.allowed_pkg:
return False
return True
""");
createTestPackages();
var analysisResult =
update(
ImmutableList.of("//test:defs.bzl%cmdline_aspect"),
ImmutableMap.of("allowed_pkg", "pkg1"),
"//pkg1:t1",
"//pkg2:t1");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%cmdline_aspect");
// Only the keys to the targets that satisfy the aspect's propagation predicate are present.
assertThat(aspectKeys)
.containsExactly("cmdline_aspect on //pkg1:t1", "cmdline_aspect on //pkg1:t2");
var aspectResult = getAspectResult(analysisResult.getAspectsMap(), "cmdline_aspect");
assertThat(aspectResult)
.containsExactly("cmdline_aspect on @@//pkg1:t1", "cmdline_aspect on @@//pkg1:t2");
}
@Test
public void propagationPredicateOnTargetTags_aspectPropagatedToSatisfyingTargets()
throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
if ctx.attr.ignored_tag != '' and ctx.attr.ignored_tag in ctx.rule.attr.tags.value:
return False
return True
""");
createTestPackages();
var analysisResult =
update(
ImmutableList.of("//test:defs.bzl%cmdline_aspect"),
ImmutableMap.of("ignored_tag", "no-aspect"),
"//pkg1:t1",
"//pkg2:t1");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%cmdline_aspect");
// Only the keys to the targets that satisfy the aspect's propagation predicate are present.
assertThat(aspectKeys)
.containsExactly("cmdline_aspect on //pkg2:t1", "cmdline_aspect on //pkg1:t2");
var aspectResult = getAspectResult(analysisResult.getAspectsMap(), "cmdline_aspect");
assertThat(aspectResult)
.containsExactly("cmdline_aspect on @@//pkg2:t1", "cmdline_aspect on @@//pkg1:t2");
}
@Test
public void propagationPredicateOnRuleKind_aspectPropagatedToSatisfyingTargets()
throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
qualified_kind = ctx.rule.qualified_kind
allowed_rule_file = ctx.attr.allowed_rule_file
allowed_rule_name = ctx.attr.allowed_rule_name
if allowed_rule_file != '*' and qualified_kind.file_label != Label(allowed_rule_file):
return False
if allowed_rule_name != '*' and qualified_kind.rule_name != allowed_rule_name:
return False
return True
""");
createTestPackages();
var analysisResult =
update(
ImmutableList.of("//test:defs.bzl%cmdline_aspect"),
ImmutableMap.of(
"allowed_rule_file", "@@//test:defs.bzl", "allowed_rule_name", "rule_1"),
"//pkg1:t1",
"//pkg2:t1");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%cmdline_aspect");
// Only the keys to the targets that satisfy the aspect's propagation predicate are present.
assertThat(aspectKeys)
.containsExactly("cmdline_aspect on //pkg1:t1", "cmdline_aspect on //pkg2:t2");
var aspectResult = getAspectResult(analysisResult.getAspectsMap(), "cmdline_aspect");
assertThat(aspectResult)
.containsExactly("cmdline_aspect on @@//pkg1:t1", "cmdline_aspect on @@//pkg2:t2");
}
@Test
public void accessPrivateAspectAttribute_fails() throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
tool = ctx.attr._tool
return True
""");
createTestPackages();
reporter.removeHandler(failFastHandler);
assertThrows(
ViewCreationFailedException.class,
() -> update(ImmutableList.of("//test:defs.bzl%cmdline_aspect"), "//pkg1:t1"));
assertContainsEvent("'_tool' is not a public parameter of the aspect.");
}
@Test
public void aspectOnOutputFile_propagationPredicateNotUsed() throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
return False
""");
createTestPackages();
var unused = update(ImmutableList.of("//test:defs.bzl%cmdline_aspect"), "//pkg1:my_out.txt");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%cmdline_aspect");
// The propagation predicate is not used for output files, so the aspect key is created even if
// the propagation predicate is not satisfied.
assertThat(aspectKeys).containsExactly("cmdline_aspect on //pkg1:my_out.txt");
}
@Test
public void aspectPropagatedFromRule_propagationPredicateIsUsed() throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
if ctx.rule.label == Label('//pkg1:t2'):
return False
return True
""");
createTestPackages();
var unused = update("//pkg1:target_with_aspect_on_deps");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_on_rule");
// The propagation predicate is used for the aspect_on_rule, so the aspect key is not created
// for //pkg1:t2.
assertThat(aspectKeys).containsExactly("aspect_on_rule on //pkg2:t2");
}
@Test
public void aspectOnAlias_propagationPredicateRunOnActualTarget() throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
if ctx.rule.qualified_kind.rule_name == 'rule_1':
return True
return False
""");
createTestPackages();
var unused = update(ImmutableList.of("//test:defs.bzl%cmdline_aspect"), "//pkg1:alias_1");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%cmdline_aspect");
// The propagation predicate is evaluated on the actual target of the alias which in this case
// satisfies the predicate.
assertThat(aspectKeys)
.containsExactly(
"cmdline_aspect on //pkg1:alias_1",
"cmdline_aspect on //pkg1:alias_2",
"cmdline_aspect on //pkg1:actual");
}
@Test
public void requiredProviderSatisfied_propagationPredicateNotSatisfied_aspectNotPropagated()
throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
return False
""");
createTestPackages();
var unused =
update(
ImmutableList.of("//test:defs.bzl%aspect_with_required_provider"),
"//pkg2:target_with_provider");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_with_required_provider");
// The propagation predicate is not satisfied, so the aspect is not propagated even though its
// required provider is satisfied.
assertThat(aspectKeys).isEmpty();
}
@Test
public void requiredProviderSatisfied_propagationPredicateSatisfied_aspectPropagated()
throws Exception {
createTestDefs(
"""
def _propagation_predicate(ctx):
return True
""");
createTestPackages();
var unused =
update(
ImmutableList.of("//test:defs.bzl%aspect_with_required_provider"),
"//pkg2:target_with_provider");
var aspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_with_required_provider");
// The propagation predicate and the required provider are satisfied, so the aspect is
// propagated to the target.
assertThat(aspectKeys)
.containsExactly("aspect_with_required_provider on //pkg2:target_with_provider");
}
@Test
public void aspectOnAspect_eachPropagationPredicateEvaluatedSeparately() throws Exception {
scratch.file("test/BUILD");
scratch.file(
"test/defs.bzl",
"""
CInfo = provider()
BInfo = provider()
AInfo = provider()
def _aspect_a_propagation_predicate(ctx):
return True
def _aspect_b_propagation_predicate(ctx):
if ctx.rule.label.package == 'pkg1':
return True
return False
def _aspect_c_propagation_predicate(ctx):
return True
def _aspect_a_impl(target, ctx):
res = 'aspect_a on %s' % target.label
if BInfo in target:
res += ' with BInfo'
if CInfo in target:
res += ' with CInfo'
res_list = [res]
for dep in ctx.rule.attr.deps:
if AInfo in dep:
res_list.extend(dep[AInfo].res)
return [AInfo(res = res_list)]
def _aspect_b_impl(target, ctx):
return [BInfo()]
def _aspect_c_impl(target, ctx):
return [CInfo()]
aspect_a = aspect(
implementation = _aspect_a_impl,
propagation_predicate = _aspect_a_propagation_predicate,
attr_aspects = ['deps'],
required_aspect_providers = [[BInfo], [CInfo]],
)
aspect_b = aspect(
implementation = _aspect_b_impl,
propagation_predicate = _aspect_b_propagation_predicate,
attr_aspects = ['deps'],
provides = [BInfo],
)
aspect_c = aspect(
implementation = _aspect_c_impl,
propagation_predicate = _aspect_c_propagation_predicate,
attr_aspects = ['deps'],
provides = [CInfo],
)
def _rule_impl(ctx):
pass
my_rule = rule(
implementation = _rule_impl,
attrs = {'deps': attr.label_list()},
)
""");
scratch.file(
"pkg1/BUILD",
"""
load('//test:defs.bzl', 'my_rule')
my_rule(name = 't1', deps = [':t2', '//pkg2:t2'])
my_rule(name = 't2')
""");
scratch.file(
"pkg2/BUILD",
"""
load('//test:defs.bzl', 'my_rule')
my_rule(name = 't1', deps = [':t2'])
my_rule(name = 't2')
""");
var analysisResult =
update(
ImmutableList.of(
"//test:defs.bzl%aspect_c", "//test:defs.bzl%aspect_b", "//test:defs.bzl%aspect_a"),
"//pkg1:t1",
"//pkg2:t1");
// The propagation predicate of each aspect is evaluated separately, then the aspect-on-aspect
// relation is created between the filtered aspects.
var aAspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_a");
assertThat(aAspectKeys)
.containsExactly(
"aspect_a on //pkg1:t1 with base aspects: aspect_b,aspect_c",
"aspect_a on //pkg1:t2 with base aspects: aspect_b,aspect_c",
"aspect_a on //pkg2:t1 with base aspects: aspect_c",
"aspect_a on //pkg2:t2 with base aspects: aspect_c");
var bAspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_b");
assertThat(bAspectKeys).containsExactly("aspect_b on //pkg1:t1", "aspect_b on //pkg1:t2");
var cAspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_c");
assertThat(cAspectKeys)
.containsExactly(
"aspect_c on //pkg1:t1",
"aspect_c on //pkg1:t2",
"aspect_c on //pkg2:t1",
"aspect_c on //pkg2:t2");
var aspectAonPkg1T1 =
getAspectResult(analysisResult.getAspectsMap(), "aspect_a", "//pkg1:t1", "AInfo");
assertThat(aspectAonPkg1T1)
.containsExactly(
"aspect_a on @@//pkg1:t1 with BInfo with CInfo",
"aspect_a on @@//pkg1:t2 with BInfo with CInfo",
"aspect_a on @@//pkg2:t2 with CInfo");
var aspectAonPkg2T1 =
getAspectResult(analysisResult.getAspectsMap(), "aspect_a", "//pkg2:t1", "AInfo");
assertThat(aspectAonPkg2T1)
.containsExactly(
"aspect_a on @@//pkg2:t1 with CInfo", "aspect_a on @@//pkg2:t2 with CInfo");
}
@Test
public void requiredAspects_propagationPredicateOfRequiredAspectIsUsed() throws Exception {
scratch.file("test/BUILD");
scratch.file(
"test/defs.bzl",
"""
AInfo = provider()
BInfo = provider()
def _aspect_a_propagation_predicate(ctx):
return True
def _aspect_b_propagation_predicate(ctx):
if ctx.rule.label.package == 'pkg1':
return True
return False
def _aspect_a_impl(target, ctx):
res = 'aspect_a on %s' % target.label
if BInfo in target:
res += ' with BInfo'
else:
res += ' without BInfo'
res_list = [res]
for dep in ctx.rule.attr.deps:
if AInfo in dep:
res_list.extend(dep[AInfo].res)
return [AInfo(res = res_list)]
def _aspect_b_impl(target, ctx):
return [BInfo()]
aspect_b = aspect(
implementation = _aspect_b_impl,
propagation_predicate = _aspect_b_propagation_predicate,
)
aspect_a = aspect(
implementation = _aspect_a_impl,
propagation_predicate = _aspect_a_propagation_predicate,
attr_aspects = ['deps'],
requires = [aspect_b],
)
def _rule_impl(ctx):
pass
my_rule = rule(
implementation = _rule_impl,
attrs = {'deps': attr.label_list()},
)
""");
scratch.file(
"pkg1/BUILD",
"""
load('//test:defs.bzl', 'my_rule')
my_rule(name = 't1', deps = [':t2', '//pkg2:t2'])
my_rule(name = 't2')
""");
scratch.file(
"pkg2/BUILD",
"""
load('//test:defs.bzl', 'my_rule')
my_rule(name = 't1', deps = [':t2'])
my_rule(name = 't2')
""");
var analysisResult =
update(ImmutableList.of("//test:defs.bzl%aspect_a"), "//pkg1:t1", "//pkg2:t1");
// The propagation predicate of both aspects is used.
var aAspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_a");
assertThat(aAspectKeys)
.containsExactly(
"aspect_a on //pkg1:t1 with base aspects: aspect_b",
"aspect_a on //pkg1:t2 with base aspects: aspect_b",
"aspect_a on //pkg2:t1",
"aspect_a on //pkg2:t2");
var bAspectKeys = getFormattedAspectKeys("//test:defs.bzl%aspect_b");
assertThat(bAspectKeys).containsExactly("aspect_b on //pkg1:t1", "aspect_b on //pkg1:t2");
var aspectAonPkg1T1 =
getAspectResult(analysisResult.getAspectsMap(), "aspect_a", "//pkg1:t1", "AInfo");
assertThat(aspectAonPkg1T1)
.containsExactly(
"aspect_a on @@//pkg1:t1 with BInfo",
"aspect_a on @@//pkg1:t2 with BInfo",
"aspect_a on @@//pkg2:t2 without BInfo");
var aspectAonPkg2T1 =
getAspectResult(analysisResult.getAspectsMap(), "aspect_a", "//pkg2:t1", "AInfo");
assertThat(aspectAonPkg2T1)
.containsExactly(
"aspect_a on @@//pkg2:t1 without BInfo", "aspect_a on @@//pkg2:t2 without BInfo");
}
private String formatAspectKey(AspectKey aspectKey) {
if (aspectKey.getBaseKeys().isEmpty()) {
return Splitter.on("%").splitToList(aspectKey.getAspectClass().toString()).get(1)
+ " on "
+ aspectKey.getLabel();
}
String baseAspects =
String.join(
",",
aspectKey.getBaseKeys().stream()
.map(k -> Splitter.on("%").splitToList(k.getAspectClass().toString()).get(1))
.collect(toImmutableList()));
return Splitter.on("%").splitToList(aspectKey.getAspectClass().toString()).get(1)
+ " on "
+ aspectKey.getLabel()
+ " with base aspects: "
+ baseAspects;
}
private ImmutableList<String> getFormattedAspectKeys(String aspectLabel) {
return skyframeExecutor.getEvaluator().getDoneValues().entrySet().stream()
.filter(
entry ->
entry.getKey() instanceof AspectKey
&& ((AspectKey) entry.getKey()).getAspectClass().toString().equals(aspectLabel))
.map(e -> formatAspectKey((AspectKey) e.getKey()))
.collect(toImmutableList());
}
private Sequence<?> getAspectResult(
Map<AspectKey, ConfiguredAspect> aspectsMap, String aspectName) throws Exception {
return getAspectResult(aspectsMap, aspectName, null, "AspectInfo");
}
@Nullable
private Sequence<?> getAspectResult(
Map<AspectKey, ConfiguredAspect> aspectsMap,
String aspectName,
String targetLabel,
String providerName)
throws Exception {
for (Map.Entry<AspectKey, ConfiguredAspect> entry : aspectsMap.entrySet()) {
AspectClass aspectClass = entry.getKey().getAspectClass();
if (aspectClass instanceof StarlarkAspectClass starlarkAspectClass) {
String aspectExportedName = starlarkAspectClass.getExportedName();
if (aspectExportedName.equals(aspectName)
&& (targetLabel == null || entry.getKey().getLabel().toString().equals(targetLabel))) {
return getStarlarkProvider(entry.getValue(), "//test:defs.bzl", providerName)
.getValue("res", Sequence.class);
}
}
}
return null;
}
}