blob: d4a7c044de4eac180df6cd199b6cc3737456cccd [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.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.ActionLookupData;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.devtools.build.lib.skyframe.ActionTemplateExpansionValue;
import com.google.devtools.build.lib.skyframe.ActionTemplateExpansionValue.ActionTemplateExpansionKey;
import com.google.devtools.build.lib.testutil.SkyframeExecutorTestHelper;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.util.io.RecordingOutErr;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import com.google.testing.junit.testparameterinjector.TestParameters;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(TestParameterInjector.class)
public final class StarlarkMapActionTemplateTest extends BuildIntegrationTestCase {
@Before
public void setUp() throws Exception {
addOptions("--experimental_allow_map_directory");
write(
"test/BUILD",
"""
load(":my_rule.bzl", "my_rule")
my_rule(name = "target", data = ":data.txt", data2 = ":data2.txt", tool = ":genrule_tool")
genrule(
name = "genrule_tool",
outs = ["tool"],
executable = True,
cmd = "echo 'cat $$@ > $$1' > $@",
)
""");
write(
"test/my_rule.bzl",
"""
load(":rule_def.bzl", "rule_impl")
my_rule = rule(
implementation = rule_impl,
attrs = {
"append_data": attr.bool(default = True),
"data": attr.label(allow_single_file = True),
"data2": attr.label(allow_single_file = True),
"tool": attr.label(cfg = "exec", executable = True),
},
)
""");
write(
"test/helpers.bzl",
"""
def create_seed_dir(ctx, dir_name, start, end):
input_dir = ctx.actions.declare_directory(ctx.attr.name + "_" + dir_name)
ctx.actions.run_shell(
mnemonic = "SeedData",
outputs = [input_dir],
command = "for i in {%d..%d}; do echo $i > %s/%s_f$i; done" % (
start, end, input_dir.path, dir_name
),
)
return input_dir
def unused_impl(template_ctx, **kwargs):
pass
def simple_map_impl(template_ctx, input_directories, output_directories, tools, **kwargs):
for f1 in input_directories["input_dir"].children:
o1 = template_ctx.declare_file(
f1.basename + ".out", directory = output_directories["output_dir"])
args = template_ctx.args()
args.add_all([o1, f1])
template_ctx.run(
inputs = [f1],
outputs = [o1],
executable = tools["tool"],
arguments = [args],
)
def append_data_impl(
template_ctx,
input_directories,
output_directories,
additional_inputs,
tools,
additional_params):
data = additional_inputs["data"]
for f1 in input_directories["input_dir"].children:
o1 = template_ctx.declare_file(
f1.basename + ".out", directory = output_directories["output_dir"])
args = template_ctx.args()
args.add_all([o1, f1])
if additional_params["append_data"]:
args.add(data)
template_ctx.run(
inputs = [f1, data],
outputs = [o1],
executable = tools["tool"],
arguments = [args],
)
def zip_and_combine_impl(
template_ctx,
input_directories,
output_directories,
tools,
**kwargs):
input_dir1 = input_directories["input_dir1"]
input_dir2 = input_directories["input_dir2"]
for f1, f2 in zip(input_dir1.children, input_dir2.children):
o1 = template_ctx.declare_file(
f1.basename + "_" + f2.basename + ".out",
directory = output_directories["output_dir"])
args = template_ctx.args()
args.add_all([o1, f1, f2])
template_ctx.run(
inputs = [f1, f2],
outputs = [o1],
executable = tools["tool"],
arguments = [args],
)
def split_directory_impl(
template_ctx,
input_directories,
output_directories,
tools,
**kwargs):
input_dir = input_directories["input_dir"]
for i, f1 in enumerate(input_dir.children):
output_dir_key = "output_dir1" if i % 2 == 0 else "output_dir2"
o1 = template_ctx.declare_file(
f1.basename + ".out",
directory = output_directories[output_dir_key])
args = template_ctx.args()
args.add_all([o1, f1])
template_ctx.run(
inputs = [f1],
outputs = [o1],
executable = tools["tool"],
arguments = [args],
)
""");
write("test/data.txt", "some data");
write("test/data2.txt", "other data");
}
@Test
public void doSimpleMappingWithAdditionalInputsAndParams() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir", "append_data_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = append_data_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
additional_params = {
"append_data": ctx.attr.append_data,
},
additional_inputs = {
"data": ctx.file.data,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""");
buildTarget("//test:target");
SpecialArtifact outputTree = assertTreeBuilt("test/target_output_dir");
assertTreeContainsFileWithContents(outputTree, "input_dir_f1.out", "1", "some data");
assertTreeContainsFileWithContents(outputTree, "input_dir_f2.out", "2", "some data");
assertTreeContainsFileWithContents(outputTree, "input_dir_f3.out", "3", "some data");
}
@Test
public void multipleInputDirectories() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir", "zip_and_combine_impl")
def rule_impl(ctx):
input_dir1 = create_seed_dir(ctx, "input_dir1", 1, 3)
input_dir2 = create_seed_dir(ctx, "input_dir2", 4, 6)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = zip_and_combine_impl,
input_directories = {
"input_dir1": input_dir1,
"input_dir2": input_dir2,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""");
buildTarget("//test:target");
SpecialArtifact outputTree = assertTreeBuilt("test/target_output_dir");
assertTreeContainsFileWithContents(outputTree, "input_dir1_f1_input_dir2_f4.out", "1", "4");
assertTreeContainsFileWithContents(outputTree, "input_dir1_f2_input_dir2_f5.out", "2", "5");
assertTreeContainsFileWithContents(outputTree, "input_dir1_f3_input_dir2_f6.out", "3", "6");
}
@Test
public void multipleOutputDirectories() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir", "split_directory_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 4)
output_dir1 = ctx.actions.declare_directory(ctx.attr.name + "_output_dir1")
output_dir2 = ctx.actions.declare_directory(ctx.attr.name + "_output_dir2")
ctx.actions.map_directory(
implementation = split_directory_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir1": output_dir1,
"output_dir2": output_dir2,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir1, output_dir2]))]
""");
buildTarget("//test:target");
SpecialArtifact outputTree1 = assertTreeBuilt("test/target_output_dir1");
SpecialArtifact outputTree2 = assertTreeBuilt("test/target_output_dir2");
assertTreeContainsFileWithContents(outputTree1, "input_dir_f1.out", "1");
assertTreeContainsFileWithContents(outputTree1, "input_dir_f3.out", "3");
assertTreeContainsFileWithContents(outputTree2, "input_dir_f2.out", "2");
assertTreeContainsFileWithContents(outputTree2, "input_dir_f4.out", "4");
}
@Test
public void outputDirectoriesCanBeChainedToSubsequentMapDirectoryCalls() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir", "append_data_impl", "zip_and_combine_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir1", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = append_data_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
additional_params = {
"append_data": ctx.attr.append_data,
},
additional_inputs = {
"data": ctx.file.data,
},
)
input_dir2 = create_seed_dir(ctx, "input_dir2", 4, 6)
output_dir2 = ctx.actions.declare_directory(ctx.attr.name + "_output_dir2")
ctx.actions.map_directory(
implementation = zip_and_combine_impl,
input_directories = {
"input_dir1": output_dir,
"input_dir2": input_dir2,
},
output_directories = {
"output_dir": output_dir2,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir, output_dir2]))]
""");
buildTarget("//test:target");
SpecialArtifact outputTree = assertTreeBuilt("test/target_output_dir");
assertTreeContainsFileWithContents(outputTree, "input_dir1_f1.out", "1", "some data");
assertTreeContainsFileWithContents(outputTree, "input_dir1_f2.out", "2", "some data");
assertTreeContainsFileWithContents(outputTree, "input_dir1_f3.out", "3", "some data");
buildTarget("//test:target");
SpecialArtifact outputTree2 = assertTreeBuilt("test/target_output_dir2");
assertTreeContainsFileWithContents(
outputTree2, "input_dir1_f1.out_input_dir2_f4.out", "1", "some data", "4");
assertTreeContainsFileWithContents(
outputTree2, "input_dir1_f2.out_input_dir2_f5.out", "2", "some data", "5");
assertTreeContainsFileWithContents(
outputTree2, "input_dir1_f3.out_input_dir2_f6.out", "3", "some data", "6");
}
@Test
public void executionRequirementsPropagatedToExpandedActions() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir", "simple_map_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = simple_map_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
execution_requirements = {
"local": "1",
}
)
return [DefaultInfo(files = depset([output_dir]))]
""");
buildTarget("//test:target");
SpecialArtifact outputTree = assertTreeBuilt("test/target_output_dir");
TreeFileArtifact treeFileArtifact1 = getTreeFileArtifact(outputTree, "input_dir_f1.out", 0);
TreeFileArtifact treeFileArtifact2 = getTreeFileArtifact(outputTree, "input_dir_f2.out", 1);
TreeFileArtifact treeFileArtifact3 = getTreeFileArtifact(outputTree, "input_dir_f3.out", 2);
SpawnAction action1 = (SpawnAction) getGeneratingAction(treeFileArtifact1);
SpawnAction action2 = (SpawnAction) getGeneratingAction(treeFileArtifact2);
SpawnAction action3 = (SpawnAction) getGeneratingAction(treeFileArtifact3);
assertThat(action1.getExecutionInfo()).containsEntry("local", "1");
assertThat(action2.getExecutionInfo()).containsEntry("local", "1");
assertThat(action3.getExecutionInfo()).containsEntry("local", "1");
}
@Test
public void actionEnvironmentPropagatedToExpandedActions() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir", "simple_map_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = simple_map_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
env = {
"SOME_ENV": "ENV_VALUE",
}
)
return [DefaultInfo(files = depset([output_dir]))]
""");
buildTarget("//test:target");
SpecialArtifact outputTree = assertTreeBuilt("test/target_output_dir");
TreeFileArtifact treeFileArtifact1 = getTreeFileArtifact(outputTree, "input_dir_f1.out", 0);
TreeFileArtifact treeFileArtifact2 = getTreeFileArtifact(outputTree, "input_dir_f2.out", 1);
TreeFileArtifact treeFileArtifact3 = getTreeFileArtifact(outputTree, "input_dir_f3.out", 2);
SpawnAction action1 = (SpawnAction) getGeneratingAction(treeFileArtifact1);
SpawnAction action2 = (SpawnAction) getGeneratingAction(treeFileArtifact2);
SpawnAction action3 = (SpawnAction) getGeneratingAction(treeFileArtifact3);
assertThat(action1.getEnvironment().getFixedEnv()).containsEntry("SOME_ENV", "ENV_VALUE");
assertThat(action2.getEnvironment().getFixedEnv()).containsEntry("SOME_ENV", "ENV_VALUE");
assertThat(action3.getEnvironment().getFixedEnv()).containsEntry("SOME_ENV", "ENV_VALUE");
}
@Test
// Only boolean integer and strings are allowed in additional_params.
public void allowedAdditionalParams(@TestParameter({"1", "True", "\"some string\""}) String value)
throws Exception {
write(
"test/rule_def.bzl",
String.format(
"""
load(":helpers.bzl", "create_seed_dir", "simple_map_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = simple_map_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
additional_params = {
"some_key": %s,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""",
value));
buildTarget("//test:target");
SpecialArtifact outputTree = assertTreeBuilt("test/target_output_dir");
assertTreeContainsFileWithContents(outputTree, "input_dir_f1.out", "1");
assertTreeContainsFileWithContents(outputTree, "input_dir_f2.out", "2");
assertTreeContainsFileWithContents(outputTree, "input_dir_f3.out", "3");
}
@Test
@TestParameters("{value: '[]', errorType: 'list'}")
@TestParameters("{value: '(1, 2)', errorType: 'tuple'}")
@TestParameters("{value: 'ctx.file.data', errorType: 'File'}")
@TestParameters("{value: 'depset()', errorType: 'depset'}")
@TestParameters("{value: '{}', errorType: 'dict'}")
@TestParameters("{value: 'set()', errorType: 'set'}")
// Only boolean integer and strings are allowed in additional_params.
public void disallowedAdditionalParams(String value, String errorType) throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
String.format(
"""
load(":helpers.bzl", "create_seed_dir", "unused_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = unused_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
additional_params = {
"some_key": %s,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""",
value));
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(ViewCreationFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1())
.contains(
String.format(
"Expected one of [bool, int, string]; but got %s in additional_params['some_key']",
errorType));
}
@Test
@TestParameters("{inputs: '{\"input_dir\": input_dir}', outputs: '{}', errorType: 'output'}")
@TestParameters("{inputs: '{}', outputs: '{\"output_dir\": output_dir}', errorType: 'input'}")
public void emptyInputOrOutputDirectoriesNotAllowed(
String inputs, String outputs, String errorType) throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
String.format(
"""
load(":helpers.bzl", "create_seed_dir", "unused_impl")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = unused_impl,
input_directories = %s,
output_directories = %s,
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""",
inputs, outputs));
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(ViewCreationFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1())
.contains(String.format("actions.map_directory() requires at least one %s.", errorType));
}
@Test
public void failingImplementation() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir")
def failing_impl(template_ctx, **kwargs):
fail("This is a test failure.")
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = failing_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""");
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(BuildFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1()).contains("This is a test failure.");
}
@Test
public void cannotDeclareFileInNonOutputDirectory() throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir")
def wrong_declare_file_impl(template_ctx, input_directories, **kwargs):
template_ctx.declare_file("child", directory = input_directories["input_dir"].directory)
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = wrong_declare_file_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""");
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(BuildFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1())
.containsMatch(
"Cannot declare file `child` in non-output directory File.*test/target_input_dir");
}
@Test
public void actionConflicts_conflictingOutputsInSameDirectory() throws Exception {
// Don't check serialization here, since the action conflict only occurs during execution,
// but serialization checks end up throwing (due to action conflicts) before we get there.
write(
"test/rule_def.bzl",
"""
load(":helpers.bzl", "create_seed_dir")
def conflict_impl(template_ctx, output_directories, tools, **kwargs):
output_dir = output_directories["output_dir"]
for i in range(2):
o1 = template_ctx.declare_file("child", directory = output_dir)
template_ctx.run(
inputs = [],
outputs = [o1],
executable = tools["tool"],
)
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = conflict_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""");
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(BuildFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1())
.contains(
"ERROR: file 'test/target_output_dir/child' is generated by these conflicting"
+ " actions:");
}
@Test
@TestParameters("{output: 'input_dir.directory', path: 'test/target_input_dir'}")
@TestParameters("{output: 'input_dir.children[0]', path: 'test/target_input_dir/input_dir_f1'}")
@TestParameters("{output: 'output_dir', path: 'test/target_output_dir'}")
@TestParameters("{output: 'tool.executable', path: 'test/tool'}")
@TestParameters("{output: 'some_file', path: 'test/some_file'}")
public void actionConflicts_conflictingOutputsFromOtherContext(String output, String path)
throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
String.format(
"""
load(":helpers.bzl", "create_seed_dir")
def conflict_impl(
template_ctx,
input_directories,
output_directories,
tools,
additional_inputs,
**kwargs):
output_dir = output_directories["output_dir"]
input_dir = input_directories["input_dir"]
tool = tools["tool"]
some_file = additional_inputs["some_file"]
template_ctx.run(
inputs = [],
outputs = [%s],
executable = tools["tool"],
progress_message = "some conflicting action",
)
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
some_file = ctx.actions.declare_file("some_file")
ctx.actions.write(output = some_file, content = "some content")
ctx.actions.map_directory(
implementation = conflict_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
additional_inputs = {
"some_file": some_file,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""",
output));
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(BuildFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1())
.containsMatch(
String.format(
"action 'some conflicting action' has conflicting output '.*%s' that is an output"
+ " of another action, thus causing an action conflict.",
path));
}
@Test
@TestParameters("{value: '1', repr: '1'}")
@TestParameters("{value: 'True', repr: 'True'}")
@TestParameters("{value: '[1]', repr: '\\[1\\]'}")
@TestParameters("{value: '(1, 2)', repr: '\\(1, 2\\)'}")
public void implementationWithNonNoneReturnValueDisallowed(String value, String repr)
throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
String.format(
"""
load(":helpers.bzl", "create_seed_dir")
def non_none_impl(template_ctx, input_directories, **kwargs):
return %s
def rule_impl(ctx):
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = non_none_impl,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""",
value));
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(BuildFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1())
.containsMatch(
String.format(
"actions.map_directory\\(\\) implementation non_none_impl at .* may not return a"
+ " non-None value \\(got %s\\)",
repr));
}
@Test
public void nonTopLevelImplementationsDisallowed(
@TestParameter({"non_top_level_impl", "lambda_impl"}) String implementation)
throws Exception {
SkyframeExecutorTestHelper.process(getSkyframeExecutor());
write(
"test/rule_def.bzl",
String.format(
"""
load(":helpers.bzl", "create_seed_dir")
def rule_impl(ctx):
def non_top_level_impl(template_ctx, **kwargs):
pass
lambda_impl = lambda template_ctx, **kwargs: None
input_dir = create_seed_dir(ctx, "input_dir", 1, 3)
output_dir = ctx.actions.declare_directory(ctx.attr.name + "_output_dir")
ctx.actions.map_directory(
implementation = %s,
input_directories = {
"input_dir": input_dir,
},
output_directories = {
"output_dir": output_dir,
},
tools = {
"tool": ctx.attr.tool.files_to_run,
},
)
return [DefaultInfo(files = depset([output_dir]))]
""",
implementation));
RecordingOutErr recordingOutErr = new RecordingOutErr();
this.outErr = recordingOutErr;
assertThrows(ViewCreationFailedException.class, () -> buildTarget("//test:target"));
assertThat(recordingOutErr.errAsLatin1())
.containsMatch(
"Error in map_directory: to avoid unintended retention of analysis data structures,"
+ " the function \\(declared at .*/test/rule_def.bzl:.*\\) must be declared by a"
+ " top-level def statement");
}
private SpecialArtifact assertTreeBuilt(String rootRelativePath) throws Exception {
ImmutableList<Artifact> artifacts = getArtifacts("//test:target");
Optional<Artifact> maybeTree =
artifacts.stream()
.filter(a -> a.getRootRelativePathString().equals(rootRelativePath))
.findFirst();
assertThat(maybeTree).isPresent();
return (SpecialArtifact) maybeTree.get();
}
private TreeFileArtifact getTreeFileArtifact(
SpecialArtifact tree, String relativeFilePath, int actionIndex) {
// The actionIndex of the ActionTemplateExpansionKey should correspond to the actionIndex of the
// StarlarkMapActionTemplate instance.
ActionTemplateExpansionKey key =
ActionTemplateExpansionValue.key(
tree.getArtifactOwner(), tree.getGeneratingActionKey().getActionIndex());
TreeFileArtifact treeFileArtifact =
TreeFileArtifact.createTemplateExpansionOutput(tree, relativeFilePath, key);
// OTOH, the actionIndex of the TreeFileArtifact's ActionLookupData should correspond to the
// actionIndex of the action (created with template_ctx) that generated the file.
treeFileArtifact.setGeneratingActionKey(ActionLookupData.create(key, actionIndex));
return treeFileArtifact;
}
private void assertTreeContainsFileWithContents(
SpecialArtifact tree, String relativeFilePath, String... expectedContents) throws Exception {
Path execRoot = directories.getExecRoot(TestConstants.WORKSPACE_NAME);
Path path = execRoot.getRelative(tree.getExecPath().getChild(relativeFilePath));
assertThat(path.exists()).isTrue();
String actualContents = new String(FileSystemUtils.readContentAsLatin1(path));
for (String expected : expectedContents) {
assertThat(actualContents).contains(expected);
}
}
}