blob: 507da64e3c9882560abc6af20b05ef23cb11a418 [file] [log] [blame]
// Copyright 2023 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.actions;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.NULL_ACTION_OWNER;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.AbstractAction;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionExecutionContext.LostInputsCheck;
import com.google.devtools.build.lib.actions.ActionExecutionException;
import com.google.devtools.build.lib.actions.ActionInputPrefetcher;
import com.google.devtools.build.lib.actions.ActionResult;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.DiscoveredModulesPruner;
import com.google.devtools.build.lib.actions.Executor;
import com.google.devtools.build.lib.actions.ThreadStateReceiver;
import com.google.devtools.build.lib.analysis.util.ActionTester;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.BazelModuleContext;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.RepositoryMapping;
import com.google.devtools.build.lib.events.StoredEventHandler;
import com.google.devtools.build.lib.exec.BinTools;
import com.google.devtools.build.lib.exec.util.TestExecutorBuilder;
import com.google.devtools.build.lib.skyframe.WorkspaceStatusValue;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.SyscallCache;
import net.starlark.java.eval.Module;
import net.starlark.java.eval.Mutability;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkFunction;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.syntax.FileOptions;
import net.starlark.java.syntax.ParserInput;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests {@link BuildInfoFileWriteAction}. */
@RunWith(JUnit4.class)
public class BuildInfoFileWriteActionTest extends BuildViewTestCase {
private Artifact outputFile;
private Path outputPath;
private ActionExecutionContext context;
private Executor executor;
private static Object exec(String... lines) throws Exception {
try (Mutability mutability = Mutability.create("test")) {
StarlarkThread thread = new StarlarkThread(mutability, StarlarkSemantics.DEFAULT);
return Starlark.execFile(
ParserInput.fromLines(lines),
FileOptions.DEFAULT,
Module.withPredeclaredAndData(
StarlarkSemantics.DEFAULT,
ImmutableMap.of(),
BazelModuleContext.create(
Label.parseCanonicalUnchecked("//test:label"),
RepositoryMapping.ALWAYS_FALLBACK,
"test/label.bzl",
/* loads= */ ImmutableList.of(),
/* bzlTransitiveDigest= */ new byte[0])),
thread);
}
}
@Before
public final void createOutputFile() throws Exception {
outputFile = getBinArtifactWithNoOwner("output.txt");
outputPath = outputFile.getPath();
outputPath.getParentDirectory().createDirectoryAndParents();
}
@Before
public final void createExecutorAndContext() throws Exception {
BinTools binTools = BinTools.forUnitTesting(directories, analysisMock.getEmbeddedTools());
executor = new TestExecutorBuilder(fileSystem, directories, binTools).build();
context =
new ActionExecutionContext(
executor,
/* inputMetadataProvider= */ null,
ActionInputPrefetcher.NONE,
actionKeyContext,
/* outputMetadataStore= */ null,
/* rewindingEnabled= */ false,
LostInputsCheck.NONE,
new FileOutErr(),
new StoredEventHandler(),
/* clientEnv= */ ImmutableMap.of(),
/* topLevelFilesets= */ ImmutableMap.of(),
/* artifactExpander= */ null,
/* actionFileSystem= */ null,
/* skyframeDepsResult= */ null,
DiscoveredModulesPruner.DEFAULT,
SyscallCache.NO_CACHE,
ThreadStateReceiver.NULL_INSTANCE);
}
@Test
public void execute_writesToOutputFile() throws Exception {
scratch.file(
"input.txt", //
"name test_name",
"client test_client");
scratch.file(
"template.txt", //
"#define NAME {NAME}",
"#define CLIENT {CLIENT}");
Object starlarkFuncObject =
exec(
"def t(d):",
" r = {}",
" r[\"{NAME}\"] = d[\"name\"] + \"_foo\"",
" r[\"{CLIENT}\"] = d[\"client\"] + \"_c\"",
" return r",
"t");
String expected = "#define NAME test_name_foo\n" + "#define CLIENT test_client_c\n";
AbstractAction action =
new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
getSourceArtifact("input.txt", WorkspaceStatusValue.BUILD_INFO_KEY),
outputFile,
(StarlarkFunction) starlarkFuncObject,
getSourceArtifact("template.txt"),
false,
StarlarkSemantics.DEFAULT);
ActionResult actionResult = action.execute(context);
String actual = new String(FileSystemUtils.readContentAsLatin1(outputPath));
assertThat(actionResult.spawnResults()).isEmpty();
assertThat(action.getOutputs()).containsExactly(outputFile);
assertThat(actual).isEqualTo(expected);
}
@Test
public void keyMissing_templatePartiallyExpanded() throws Exception {
scratch.file(
"input.txt", //
"name test_name",
"client test_client");
scratch.file(
"template.txt", //
"#define NAME {NAME}",
"#define CLIENT {CLIENT_MISSING}");
Object starlarkFuncObject =
exec(
"def t(d):",
" r = {}",
" r[\"{NAME}\"] = d[\"name\"] + \"_foo\"",
" r[\"{CLIENT}\"] = d[\"client\"] + \"_c\"",
" return r",
"t");
String expected = "#define NAME test_name_foo\n" + "#define CLIENT {CLIENT_MISSING}\n";
AbstractAction action =
new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
getSourceArtifact("input.txt", WorkspaceStatusValue.BUILD_INFO_KEY),
outputFile,
(StarlarkFunction) starlarkFuncObject,
getSourceArtifact("template.txt"),
false,
StarlarkSemantics.DEFAULT);
ActionResult actionResult = action.execute(context);
String actual = new String(FileSystemUtils.readContentAsLatin1(outputPath));
assertThat(actionResult.spawnResults()).isEmpty();
assertThat(action.getOutputs()).containsExactly(outputFile);
assertThat(actual).isEqualTo(expected);
}
private enum BuildInfoFileWriteActionAttributes {
INPUT,
TEMPLATE,
IS_VOLATILE,
}
@Test
public void actionInputsVary_checkComputeKeyResults() throws Exception {
scratch.file("input1.txt", "");
scratch.file("template1.txt", "");
scratch.file("input2.txt", "");
scratch.file("template2.txt", "");
Object starlarkFuncObject =
exec(
"def t(d):",
" for i in range(1, 10):",
" for j in range (1, 10):",
" a = 5",
" return {}",
"t");
ActionTester.runTest(
BuildInfoFileWriteActionAttributes.class,
new ActionTester.ActionCombinationFactory<BuildInfoFileWriteActionAttributes>() {
@Override
public Action generate(
ImmutableSet<BuildInfoFileWriteActionAttributes> attributesToFlip) {
return new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
attributesToFlip.contains(BuildInfoFileWriteActionAttributes.INPUT)
? getSourceArtifact("input1.txt", WorkspaceStatusValue.BUILD_INFO_KEY)
: getSourceArtifact("input2.txt", WorkspaceStatusValue.BUILD_INFO_KEY),
outputFile,
(StarlarkFunction) starlarkFuncObject,
attributesToFlip.contains(BuildInfoFileWriteActionAttributes.TEMPLATE)
? getSourceArtifact("template1.txt")
: getSourceArtifact("template2.txt"),
attributesToFlip.contains(BuildInfoFileWriteActionAttributes.IS_VOLATILE),
StarlarkSemantics.DEFAULT);
}
},
actionKeyContext);
}
@Test
public void wrongKeyRead_exceptionThrown() throws Exception {
scratch.file(
"input.txt", //
"name test_name",
"extra_client test_client");
scratch.file(
"template.txt", //
"#define NAME {NAME}",
"#define CLIENT {CLIENT}");
Object starlarkFuncObject =
exec(
"def t(d):",
" r = {}",
" r[\"{NAME}\"] = d[\"name\"] + \"_foo\"",
" r[\"{CLIENT}\"] = d[\"client\"] + \"_c\"",
" return r",
"t");
AbstractAction action =
new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
getSourceArtifact("input.txt", WorkspaceStatusValue.BUILD_INFO_KEY),
outputFile,
(StarlarkFunction) starlarkFuncObject,
getSourceArtifact("template.txt"),
false,
StarlarkSemantics.DEFAULT);
assertThat(assertThrows(ActionExecutionException.class, () -> action.execute(context)))
.hasMessageThat()
.contains("key \"client\" not found in dictionary");
}
@Test
public void callbackReturnValueInvalidType_exceptionThrown() throws Exception {
scratch.file("input.txt", "");
scratch.file("template.txt", "");
Object starlarkFuncObject = exec("def t(d):", " return [2, 5]", "t");
AbstractAction action =
new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
getSourceArtifact("input.txt", WorkspaceStatusValue.BUILD_INFO_KEY),
outputFile,
(StarlarkFunction) starlarkFuncObject,
getSourceArtifact("template.txt"),
false,
StarlarkSemantics.DEFAULT);
assertThat(assertThrows(ActionExecutionException.class, () -> action.execute(context)))
.hasMessageThat()
.contains(
"BuildInfo translation callback function is expected to return dict of strings to"
+ " strings, could not convert return value to Java type: got list for"
+ " 'substitution_dict', want dict");
}
@Test
public void callbackReturnDictContainsInvalidType_exceptionThrown() throws Exception {
scratch.file("input.txt", "");
scratch.file("template.txt", "");
Object starlarkFuncObject = exec("def t(d):", " return {'a': 'b', 'c': 5}", "t");
AbstractAction action =
new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
getSourceArtifact("input.txt", WorkspaceStatusValue.BUILD_INFO_KEY),
outputFile,
(StarlarkFunction) starlarkFuncObject,
getSourceArtifact("template.txt"),
false,
StarlarkSemantics.DEFAULT);
assertThat(assertThrows(ActionExecutionException.class, () -> action.execute(context)))
.hasMessageThat()
.contains(
"could not convert return value to Java type: got dict<string, int> for"
+ " 'substitution_dict', want dict<string, string>");
}
@Test
public void callbackFails_exceptionThrown() throws Exception {
scratch.file("input.txt", "");
scratch.file("template.txt", "");
Object starlarkFuncObject = exec("def t(d):", " fail('starlark error')", "t");
AbstractAction action =
new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
getSourceArtifact("input.txt", WorkspaceStatusValue.BUILD_INFO_KEY),
outputFile,
(StarlarkFunction) starlarkFuncObject,
getSourceArtifact("template.txt"),
false,
StarlarkSemantics.DEFAULT);
assertThat(assertThrows(ActionExecutionException.class, () -> action.execute(context)))
.hasMessageThat()
.contains("Error in fail: starlark error");
}
@Test
public void wrongArtifactOwnerOnInputSourceFile_exceptionThrown() throws Exception {
scratch.file("input.txt", "");
scratch.file("template.txt", "");
Object starlarkFuncObject = exec("def t(d):", " pass", "t");
assertThat(
assertThrows(
IllegalArgumentException.class,
() ->
new BuildInfoFileWriteAction(
NULL_ACTION_OWNER,
// Set no artifact owner.
getSourceArtifact("input.txt"),
outputFile,
(StarlarkFunction) starlarkFuncObject,
getSourceArtifact("template.txt"),
false,
StarlarkSemantics.DEFAULT)))
.hasMessageThat()
.contains(
"input artifact of BuildInfoFileWriteAction must be one of workspace status artifacts:"
+ " ctx.info_file or ctx.version_file");
}
}