| // 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"); |
| } |
| } |