blob: 261ce77427c72587d3f7bbae1ad126ba46c7eabf [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.skyframe;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NoSuchPackagePieceException;
import com.google.devtools.build.lib.packages.PackagePiece;
import com.google.devtools.build.lib.packages.PackagePieceIdentifier;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Types;
import com.google.devtools.build.lib.skyframe.MacroInstanceFunction.NoSuchMacroInstanceException;
import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
import com.google.devtools.build.lib.vfs.ModifiedFileSet;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.skyframe.EvaluationResult;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import com.google.testing.junit.testparameterinjector.TestParameters;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests of {@link EvalMacroFunction}. */
@RunWith(TestParameterInjector.class)
public final class EvalMacroFunctionTest extends BuildViewTestCase {
private static PackagePieceIdentifier.ForBuildFile getBuildFileKey(String pkg) {
PackageIdentifier pkgId = PackageIdentifier.createInMainRepo(pkg);
return new PackagePieceIdentifier.ForBuildFile(pkgId);
}
/**
* Returns the skykey for a {@link PackagePieceValue.ForMacro}.
*
* @param pkg the package name
* @param macroInstances a list of macro instance names from the outermost to the innermost; for
* example, ["foo", "foo_bar"] means the key for the package piece generated by expanding
* macro instance "foo_bar" which is declared in macro instance "foo".
*/
private static PackagePieceIdentifier.ForMacro getMacroKey(String pkg, String... macroInstances) {
checkArgument(macroInstances.length > 0);
PackagePieceIdentifier.ForBuildFile buildFileKey = getBuildFileKey(pkg);
PackagePieceIdentifier.ForMacro macroKey = null;
for (String macroInstance : macroInstances) {
macroKey =
new PackagePieceIdentifier.ForMacro(
buildFileKey.getPackageIdentifier(),
macroKey != null ? macroKey : buildFileKey,
macroInstance);
}
return macroKey;
}
private EvaluationResult<PackagePieceValue.ForMacro> evaluate(
PackagePieceIdentifier.ForMacro skyKey) throws InterruptedException {
return SkyframeExecutorTestUtils.evaluate(
getSkyframeExecutor(), skyKey, /* keepGoing= */ false, reporter);
}
@CanIgnoreReturnValue
private PackagePiece.ForMacro getPackagePiece(String pkg, String... macroInstances)
throws InterruptedException {
PackagePieceIdentifier.ForMacro skyKey = getMacroKey(pkg, macroInstances);
EvaluationResult<PackagePieceValue.ForMacro> result = evaluate(skyKey);
if (result.hasError()) {
fail(result.getError(skyKey).getException().getMessage());
}
PackagePiece.ForMacro value = result.get(skyKey).getPackagePiece();
assertThat(value.getIdentifier()).isEqualTo(skyKey);
return value;
}
@CanIgnoreReturnValue
private PackagePiece.ForMacro getPackagePieceWithoutErrors(String pkg, String... macroInstances)
throws InterruptedException {
PackagePiece.ForMacro value = getPackagePiece(pkg, macroInstances);
assertThat(value.containsErrors()).isFalse();
return value;
}
@CanIgnoreReturnValue
private <T> T getExceptionForPackagePiece(
Class<T> exceptionClass, String pkg, String... macroInstances) throws InterruptedException {
reporter.removeHandler(failFastHandler);
PackagePieceIdentifier.ForMacro skyKey = getMacroKey(pkg, macroInstances);
EvaluationResult<PackagePieceValue.ForMacro> result = evaluate(skyKey);
assertThat(result.hasError()).isTrue();
Exception exception = result.getError(skyKey).getException();
assertThat(exception).isInstanceOf(exceptionClass);
return exceptionClass.cast(exception);
}
@Test
public void validMacro() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
PackagePiece.ForMacro forMacro = getPackagePieceWithoutErrors("pkg", "foo");
assertThat(forMacro.getTargets()).containsKey("foo");
}
@Test
@TestParameters({"{suffix: ''}", "{suffix: '_inner'}"})
public void validNestedMacro(String suffix) throws Exception {
scratch.file(
"pkg/inner_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
inner_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/outer_macro.bzl",
String.format(
"""
load(":inner_macro.bzl", "inner_macro")
def _impl(name, visibility):
inner_macro(name = name + "%s", visibility = visibility)
outer_macro = macro(implementation = _impl)
""",
suffix));
scratch.file(
"pkg/BUILD",
"""
load(":outer_macro.bzl", "outer_macro")
outer_macro(name = "foo")
""");
String innerMacroInstanceName = "foo" + suffix;
PackagePiece.ForMacro forInnerMacro =
getPackagePieceWithoutErrors("pkg", "foo", innerMacroInstanceName);
assertThat(forInnerMacro.getTargets()).containsKey(innerMacroInstanceName);
PackagePiece.ForMacro forOuterMacro = getPackagePieceWithoutErrors("pkg", "foo");
assertThat(forOuterMacro.getMacroByName(innerMacroInstanceName))
.isSameInstanceAs(forInnerMacro.getEvaluatedMacro());
}
@Test
public void innerAndSiblingMacros_notExpandedUnlessRequested() throws Exception {
scratch.file(
"pkg/inner_macro.bzl",
"""
def _impl(name, visibility):
fail("This will fail if the inner macro is expanded")
inner_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/sibling_macro.bzl",
"""
def _impl(name, visibility):
fail("This will fail if the sibling macro is expanded")
sibling_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/outer_macro.bzl",
"""
load(":inner_macro.bzl", "inner_macro")
def _impl(name, visibility):
inner_macro(name = name + "_inner", visibility = visibility)
outer_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":sibling_macro.bzl", "sibling_macro")
load(":outer_macro.bzl", "outer_macro")
sibling_macro(name = "bar")
outer_macro(name = "foo")
""");
getPackagePieceWithoutErrors("pkg", "foo");
SkyframeExecutor.FailureToRetrieveIntrospectedValueException siblingPieceException =
assertThrows(
SkyframeExecutor.FailureToRetrieveIntrospectedValueException.class,
() -> skyframeExecutor.getDoneSkyValueForIntrospection(getMacroKey("pkg", "bar")));
assertThat(siblingPieceException)
.hasMessageThat()
.contains(
"<PackagePieceIdentifier.ForMacro name=//pkg:bar"
+ " declared_in=<PackagePieceIdentifier.ForBuildFile pkg=//pkg>> not found");
SkyframeExecutor.FailureToRetrieveIntrospectedValueException innerPieceException =
assertThrows(
SkyframeExecutor.FailureToRetrieveIntrospectedValueException.class,
() ->
skyframeExecutor.getDoneSkyValueForIntrospection(
getMacroKey("pkg", "foo", "foo_inner")));
assertThat(innerPieceException)
.hasMessageThat()
.contains(
"<PackagePieceIdentifier.ForMacro name=//pkg:foo_inner"
+ " declared_in=<PackagePieceIdentifier.ForMacro name=//pkg:foo"
+ " declared_in=<PackagePieceIdentifier.ForBuildFile pkg=//pkg>>> not found");
}
// TODO(https://github.com/bazelbuild/bazel/issues/26128): also prune outer macro changes at an
// inner macro instance.
@Test
public void buildFileChange_prunedAtMacroInstance() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
PackagePiece.ForMacro packagePieceBeforeUpdate = getPackagePieceWithoutErrors("pkg", "foo");
// Edit and invalidate BUILD file. Note that for change pruning to work, the edit must preserve
// line numbers in the macro call stack.
// TODO(https://github.com/bazelbuild/bazel/issues/26128): relax this requirement.
scratch.overwriteFile(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
cc_library(name = "unrelated")
""");
getSkyframeExecutor()
.invalidateFilesUnderPathForTesting(
reporter,
ModifiedFileSet.builder().modify(PathFragment.create("pkg/BUILD")).build(),
Root.fromPath(rootDirectory));
// PackagePieceValue.ForMacro is a NotComparableSkyValue; if we get back the same instance after
// update, it means all of the PackagePieceValue's deps were either change-pruned or unchanged.
assertThat(getPackagePieceWithoutErrors("pkg", "foo"))
.isSameInstanceAs(packagePieceBeforeUpdate);
}
@Test
public void maxComputationSteps_enforced() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
# exceed max_computation_steps
for i in range(1000):
pass
native.cc_library(name = name, visibility = visibility)
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
setBuildLanguageOptions("--max_computation_steps=100"); // sufficient for BUILD but not my_macro
assertThat(getExceptionForPackagePiece(NoSuchPackagePieceException.class, "pkg", "foo"))
.hasMessageThat()
.containsMatch(
"symbolic macro evaluation took 1\\d{3} computation steps, but"
+ " --max_computation_steps=100");
}
@Test
public void noBuildFile_failsCleanly() throws Exception {
assertThat(getExceptionForPackagePiece(NoSuchPackageException.class, "no_such_pkg", "foo"))
.hasMessageThat()
.contains("no such package 'no_such_pkg': BUILD file not found");
}
@Test
public void badBuildFile_failsCleanly() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
load(":bad_load.bzl", "bad_value")
my_macro(name = "foo")
""");
assertThat(getExceptionForPackagePiece(NoSuchPackageException.class, "pkg", "foo"))
.hasMessageThat()
.contains("cannot load '//pkg:bad_load.bzl': no such file");
}
@Test
public void noMacroInstance_failsCleanly() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
assertThat(
getExceptionForPackagePiece(NoSuchMacroInstanceException.class, "pkg", "no_such_name"))
.hasMessageThat()
.contains("Macro instance 'no_such_name' not found in top-level package piece");
}
@Test
public void badMacroImplementation_producesPackagePieceWithErrors() throws Exception {
scratch.file(
"pkg/my_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
fail("fail fail fail")
my_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_macro.bzl", "my_macro")
my_macro(name = "foo")
""");
reporter.removeHandler(failFastHandler);
PackagePiece.ForMacro forMacro = getPackagePiece("pkg", "foo");
assertThat(forMacro.containsErrors()).isTrue();
assertThat(((Rule) forMacro.getTarget("foo")).containsErrors()).isTrue();
assertContainsEvent(
"""
ERROR /workspace/pkg/my_macro.bzl:3:9: Traceback (most recent call last):
\tFile "/workspace/pkg/BUILD", line 2, column 9, in <toplevel>
\t\tmy_macro(name = "foo")
\tFile "/workspace/pkg/my_macro.bzl", line 4, column 1, in my_macro
\t\tmy_macro = macro(implementation = _impl)
\tFile "/workspace/pkg/my_macro.bzl", line 3, column 9, in _impl
\t\tfail("fail fail fail")
Error in fail: fail fail fail\
""");
}
@Test
public void finalizers_seeNonFinalizerDefinedRulesOrderedByName() throws Exception {
scratch.file(
"pkg/my_finalizer.bzl",
"""
# Dummy rule used to save native.existing_rules() keys in a string list attribute.
_existing_rules_saver = rule(
implementation = lambda ctx: [],
attrs = {"existing_rules": attr.string_list()},
)
def _impl(name, visibility):
_existing_rules_saver(name = name, existing_rules = list(native.existing_rules()))
my_finalizer = macro(implementation = _impl, finalizer = True)
""");
scratch.file(
"pkg/other_macro.bzl",
"""
def _other_inner_macro_impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
other_inner_macro = macro(implementation = _other_inner_macro_impl)
def _other_macro_impl(name, visibility):
other_inner_macro(name = name + "_c_inner", visibility = visibility)
native.cc_library(name = name + "_b", visibility = visibility)
other_inner_macro(name = name + "_a_inner", visibility = visibility)
other_macro = macro(implementation = _other_macro_impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_finalizer.bzl", "my_finalizer")
load(":other_macro.bzl", "other_macro")
my_finalizer(name = "finalize")
other_macro(name = "macro_declared")
cc_library(name = "a_top_level")
cc_library(name = "z_top_level")
""");
// getPackagePieceWithoutErrors("pkg", "finalize");
PackagePiece.ForMacro finalizerPiece = getPackagePieceWithoutErrors("pkg", "finalize");
Rule existingRulesSaverRule = (Rule) finalizerPiece.getTarget("finalize");
List<String> existingRules =
Types.STRING_LIST.cast(existingRulesSaverRule.getAttr("existing_rules"));
assertThat(existingRules)
.containsExactly(
// Ordered by name.
"a_top_level",
"macro_declared_a_inner",
"macro_declared_b",
"macro_declared_c_inner",
"z_top_level")
.inOrder();
}
@Test
public void finalizers_doNotSeeFinalizerDefinedTargets() throws Exception {
scratch.file(
"pkg/my_finalizer.bzl",
"""
# Dummy rule used to save native.existing_rules() keys in a string list attribute.
_existing_rules_saver = rule(
implementation = lambda ctx: [],
attrs = {"existing_rules": attr.string_list()},
)
def _impl(name, visibility):
native.cc_library(name = name + "_dummy_rule")
_existing_rules_saver(name = name, existing_rules = list(native.existing_rules()))
my_finalizer = macro(implementation = _impl, finalizer = True)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_finalizer.bzl", "my_finalizer")
my_finalizer(name = "finalize")
my_finalizer(name = "other_finalize")
""");
PackagePiece.ForMacro finalizerPiece = getPackagePieceWithoutErrors("pkg", "finalize");
assertThat(
Types.STRING_LIST.cast(
((Rule) finalizerPiece.getTarget("finalize")).getAttr("existing_rules")))
.isEmpty();
PackagePiece.ForMacro otherFinalizerPiece =
getPackagePieceWithoutErrors("pkg", "other_finalize");
assertThat(
Types.STRING_LIST.cast(
((Rule) otherFinalizerPiece.getTarget("other_finalize")).getAttr("existing_rules")))
.isEmpty();
}
@Test
public void finalizers_notEvaluated_ifNonFinalizerPackagePieceInError() throws Exception {
scratch.file(
"pkg/my_finalizer.bzl",
"""
def _impl(name, visibility):
native.filegroup(name = name + "_saw_rules", srcs = native.existing_rules())
my_finalizer = macro(implementation = _impl, finalizer = True)
""");
scratch.file(
"pkg/fail_macro.bzl",
"""
def _impl(name, visibility):
native.cc_library(name = name, visibility = visibility)
fail("fail fail fail")
fail_macro = macro(implementation = _impl)
""");
scratch.file(
"pkg/BUILD",
"""
load(":fail_macro.bzl", "fail_macro")
load(":my_finalizer.bzl", "my_finalizer")
my_finalizer(name = "finalize")
cc_library(name = "top_level_rule")
fail_macro(name = "failing_macro")
""");
reporter.removeHandler(failFastHandler);
PackagePiece.ForMacro finalizerPiece = getPackagePiece("pkg", "finalize");
assertThat(finalizerPiece.containsErrors()).isTrue();
assertThat(finalizerPiece.getTargets()).isEmpty();
assertThat(finalizerPiece.getFailureDetail().getMessage())
.contains(
"cannot compute package piece for finalizer macro //pkg:finalize defined by"
+ " //pkg:my_finalizer.bzl%my_finalizer: error in package piece for macro"
+ " //pkg:failing_macro defined by //pkg:fail_macro.bzl%fail_macro");
assertThat(getPackagePiece("pkg", "failing_macro").containsErrors()).isTrue();
assertContainsEventsInOrder(
"""
Traceback (most recent call last):
\tFile "/workspace/pkg/BUILD", line 5, column 11, in <toplevel>
\t\tfail_macro(name = "failing_macro")
\tFile "/workspace/pkg/fail_macro.bzl", line 5, column 1, in fail_macro
\t\tfail_macro = macro(implementation = _impl)
\tFile "/workspace/pkg/fail_macro.bzl", line 3, column 9, in _impl
\t\tfail("fail fail fail")
Error in fail: fail fail fail\
""",
"cannot compute package piece for finalizer macro //pkg:finalize defined by"
+ " //pkg:my_finalizer.bzl%my_finalizer");
}
@Test
public void finalizers_notEvaluated_ifNameConflictBetweenNonFinalizerPackagePieces()
throws Exception {
scratch.file(
"pkg/my_finalizer.bzl",
"""
def _impl(name, visibility):
native.filegroup(name = name + "_saw_rules", srcs = native.existing_rules())
my_finalizer = macro(implementation = _impl, finalizer = True)
""");
scratch.file(
"pkg/name_conflict_macro.bzl",
"""
def _impl(name, suffix, visibility):
native.cc_library(name = name + suffix, visibility = visibility)
name_conflict_macro = macro(
implementation = _impl,
attrs = {"suffix": attr.string(configurable = False)},
)
""");
scratch.file(
"pkg/BUILD",
"""
load(":my_finalizer.bzl", "my_finalizer")
load(":name_conflict_macro.bzl", "name_conflict_macro")
my_finalizer(name = "finalize")
cc_library(name = "top_level_rule")
name_conflict_macro(name = "top", suffix = "_level_rule")
""");
reporter.removeHandler(failFastHandler);
PackagePiece.ForMacro finalizerPiece = getPackagePiece("pkg", "finalize");
assertThat(finalizerPiece.containsErrors()).isTrue();
assertThat(finalizerPiece.getTargets()).isEmpty();
assertThat(finalizerPiece.getFailureDetail().getMessage())
.contains(
"cannot compute package piece for finalizer macro //pkg:finalize defined by"
+ " //pkg:my_finalizer.bzl%my_finalizer: cc_library rule 'top_level_rule' conflicts"
+ " with existing cc_library rule");
// Note that individual non-finalizer package pieces are not in error - the conflict is in the
// NonFinalizerPackagePiecesValue.
getPackagePieceWithoutErrors("pkg", "top");
assertContainsEventsInOrder(
"""
Traceback (most recent call last):
\tFile "/workspace/pkg/BUILD", line 5, column 20, in <toplevel>
\t\tname_conflict_macro(name = "top", suffix = "_level_rule")
\tFile "/workspace/pkg/name_conflict_macro.bzl", line 4, column 1, in name_conflict_macro
\t\tname_conflict_macro = macro(
\tFile "/workspace/pkg/name_conflict_macro.bzl", line 2, column 22, in _impl
\t\tnative.cc_library(name = name + suffix, visibility = visibility)
Error: cc_library rule 'top_level_rule' conflicts with existing cc_library rule, defined at /workspace/pkg/BUILD:4:11\
""",
"cannot compute package piece for finalizer macro //pkg:finalize defined by"
+ " //pkg:my_finalizer.bzl%my_finalizer");
}
}