| // Copyright 2022 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.truth.Truth.assertThat; |
| |
| 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.ActionLookupData; |
| import com.google.devtools.build.lib.actions.ActionLookupKey; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.Artifact.ArchivedTreeArtifact; |
| import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact; |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifactType; |
| import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact; |
| import com.google.devtools.build.lib.actions.ArtifactRoot; |
| import com.google.devtools.build.lib.actions.ArtifactRoot.RootType; |
| import com.google.devtools.build.lib.actions.FileArtifactValue; |
| import com.google.devtools.build.lib.actions.FileContentsProxy; |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; |
| import com.google.devtools.build.lib.collect.nestedset.Order; |
| import com.google.devtools.build.lib.testutil.Scratch; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| @RunWith(JUnit4.class) |
| public final class RewoundActionOutputCheckerTest { |
| private static final FileArtifactValue VALUE_1_REMOTE = |
| FileArtifactValue.createForRemoteFile( |
| /* digest= */ new byte[0], /* size= */ 0, /* locationIndex= */ 1); |
| private static final FileArtifactValue VALUE_2_REMOTE = |
| FileArtifactValue.createForRemoteFile( |
| /* digest= */ new byte[0], /* size= */ 0, /* locationIndex= */ 2); |
| |
| private static final ActionLookupKey KEY = ActionsTestUtil.NULL_ARTIFACT_OWNER; |
| private static final ActionLookupData ACTION_LOOKUP_DATA_1 = ActionLookupData.create(KEY, 1); |
| |
| private static final ArtifactRoot OUTPUT_ROOT = |
| ArtifactRoot.asDerivedRoot(new Scratch().resolve("/execroot"), RootType.OUTPUT, "out"); |
| |
| @Test |
| public void regeneratedContent_ignoresLocationAndProxy() { |
| Artifact original = output("file", ACTION_LOOKUP_DATA_1); |
| Artifact regenerated = output("file", ACTION_LOOKUP_DATA_1); |
| var before = createWithArtifactData(ImmutableMap.of(original, VALUE_1_REMOTE)); |
| var after = |
| createWithArtifactData( |
| ImmutableMap.of( |
| regenerated, |
| FileArtifactValue.createForNormalFile( |
| new byte[0], new FileContentsProxy(1, 2, 3), 0))); |
| |
| assertThat(changedOutput(after, before)).isNull(); |
| } |
| |
| @Test |
| public void regeneratedContent_detectsChangedSecondaryOutput() { |
| Artifact primary = output("primary"); |
| Artifact secondary = output("secondary"); |
| var before = |
| createWithArtifactData(ImmutableMap.of(primary, VALUE_1_REMOTE, secondary, VALUE_1_REMOTE)); |
| var after = |
| createWithArtifactData( |
| ImmutableMap.of( |
| primary, |
| VALUE_2_REMOTE, |
| secondary, |
| FileArtifactValue.createForRemoteFile(new byte[] {1}, 0, 1))); |
| |
| assertThat(changedOutput(after, before)).isEqualTo(secondary.getExecPathString()); |
| } |
| |
| @Test |
| public void regeneratedContent_detectsChangedSizeAndType() { |
| Artifact file = output("file"); |
| var before = createWithArtifactData(ImmutableMap.of(file, VALUE_1_REMOTE)); |
| for (FileArtifactValue changed : |
| ImmutableList.of( |
| FileArtifactValue.createForRemoteFile(new byte[0], 1, 1), |
| FileArtifactValue.createForDirectoryWithHash(new byte[0]))) { |
| assertThat(changedOutput(createWithArtifactData(ImmutableMap.of(file, changed)), before)) |
| .isEqualTo(file.getExecPathString()); |
| } |
| } |
| |
| @Test |
| public void regeneratedContent_detectsOutputMissingFromPreviousValue() { |
| Artifact first = output("first"); |
| Artifact second = output("second"); |
| var one = createWithArtifactData(ImmutableMap.of(first, VALUE_1_REMOTE)); |
| var two = |
| createWithArtifactData(ImmutableMap.of(first, VALUE_1_REMOTE, second, VALUE_1_REMOTE)); |
| |
| assertThat(changedOutput(two, one)).isEqualTo(second.getExecPathString()); |
| } |
| |
| @Test |
| public void regeneratedContent_comparesTreeChildrenByContent() { |
| SpecialArtifact tree = tree("tree"); |
| TreeFileArtifact child = TreeFileArtifact.createTreeOutput(tree, "child"); |
| var before = |
| createWithTreeArtifactData( |
| ImmutableMap.of( |
| tree, TreeArtifactValue.newBuilder(tree).putChild(child, VALUE_1_REMOTE).build())); |
| var materialized = |
| createWithTreeArtifactData( |
| ImmutableMap.of( |
| tree, |
| TreeArtifactValue.newBuilder(tree) |
| .putChild(child, FileArtifactValue.createForNormalFile(new byte[0], null, 0)) |
| .build())); |
| var changed = |
| createWithTreeArtifactData( |
| ImmutableMap.of( |
| tree, |
| TreeArtifactValue.newBuilder(tree) |
| .putChild(child, FileArtifactValue.createForRemoteFile(new byte[] {1}, 0, 1)) |
| .build())); |
| var empty = createWithTreeArtifactData(ImmutableMap.of(tree, TreeArtifactValue.empty())); |
| |
| assertThat(changedOutput(materialized, before)).isNull(); |
| assertThat(changedOutput(changed, before)).isEqualTo(child.getExecPathString()); |
| assertThat(changedOutput(empty, before)).isEqualTo(child.getExecPathString()); |
| assertThat(changedOutput(before, empty)).isEqualTo(child.getExecPathString()); |
| } |
| |
| @Test |
| public void regeneratedContent_distinguishesEmptyTreeFromMissingMetadata() { |
| SpecialArtifact tree = tree("tree"); |
| var empty = createWithTreeArtifactData(ImmutableMap.of(tree, TreeArtifactValue.empty())); |
| var missingMetadata = createWithArtifactData(ImmutableMap.of(output("other"), VALUE_1_REMOTE)); |
| |
| TreeArtifactValue regenerated = TreeArtifactValue.empty(); |
| |
| assertThat( |
| RewoundActionOutputChecker.getChangedOutput( |
| tree, missingMetadata, regenerated.getMetadata(), regenerated)) |
| .isEqualTo(tree.getExecPathString()); |
| assertThat( |
| RewoundActionOutputChecker.getChangedOutput( |
| tree, empty, regenerated.getMetadata(), regenerated)) |
| .isNull(); |
| } |
| |
| @Test |
| public void regeneratedContent_ignoresTreeArchive() { |
| SpecialArtifact tree = tree("tree"); |
| TreeFileArtifact child = TreeFileArtifact.createTreeOutput(tree, "child"); |
| var before = |
| createWithTreeArtifactData( |
| ImmutableMap.of( |
| tree, TreeArtifactValue.newBuilder(tree).putChild(child, VALUE_1_REMOTE).build())); |
| var after = |
| TreeArtifactValue.newBuilder(tree) |
| .putChild(child, VALUE_2_REMOTE) |
| .setArchivedRepresentation( |
| ArchivedTreeArtifact.createForTree(tree), |
| FileArtifactValue.createForRemoteFile(new byte[] {2}, 100, 1)) |
| .build(); |
| |
| assertThat( |
| RewoundActionOutputChecker.getChangedOutput(tree, before, after.getMetadata(), after)) |
| .isNull(); |
| } |
| |
| @Test |
| public void regeneratedContent_detectsChangedSymlinkTarget() throws Exception { |
| Artifact symlink = output("symlink"); |
| var path = symlink.getPath(); |
| path.getParentDirectory().createDirectoryAndParents(); |
| path.createSymbolicLink(PathFragment.create("first")); |
| var before = |
| createWithArtifactData( |
| ImmutableMap.of(symlink, FileArtifactValue.createForUnresolvedSymlink(path))); |
| path.delete(); |
| path.createSymbolicLink(PathFragment.create("second")); |
| var after = |
| createWithArtifactData( |
| ImmutableMap.of(symlink, FileArtifactValue.createForUnresolvedSymlink(path))); |
| |
| assertThat(changedOutput(after, before)).isEqualTo(symlink.getExecPathString()); |
| } |
| |
| /** |
| * Checks every output of the regenerated action, like {@code SkyframeActionExecutor} does. A |
| * regenerated action always produces metadata for all of its declared outputs. |
| */ |
| private static String changedOutput(ActionExecutionValue after, ActionExecutionValue before) { |
| for (Artifact output : |
| ImmutableSet.<Artifact>builder() |
| .addAll(after.getAllFileValues().keySet()) |
| .addAll(after.getAllTreeArtifactValues().keySet()) |
| .build()) { |
| TreeArtifactValue tree = after.getAllTreeArtifactValues().get(output); |
| String changed = |
| RewoundActionOutputChecker.getChangedOutput( |
| output, |
| before, |
| tree != null ? tree.getMetadata() : after.getAllFileValues().get(output), |
| tree); |
| if (changed != null) { |
| return changed; |
| } |
| } |
| return null; |
| } |
| |
| private static ActionExecutionValue createWithArtifactData( |
| ImmutableMap<Artifact, FileArtifactValue> artifactData) { |
| return ActionExecutionValue.create( |
| /* artifactData= */ artifactData, |
| /* treeArtifactData= */ ImmutableMap.of(), |
| /* richArtifactData= */ null, |
| /* discoveredModules= */ NestedSetBuilder.emptySet(Order.STABLE_ORDER)); |
| } |
| |
| private static ActionExecutionValue createWithTreeArtifactData( |
| ImmutableMap<Artifact, TreeArtifactValue> treeArtifactData) { |
| return ActionExecutionValue.create( |
| /* artifactData= */ ImmutableMap.of(), |
| treeArtifactData, |
| /* richArtifactData= */ null, |
| /* discoveredModules= */ NestedSetBuilder.emptySet(Order.STABLE_ORDER)); |
| } |
| |
| private static DerivedArtifact output(String rootRelativePath) { |
| return output(rootRelativePath, ACTION_LOOKUP_DATA_1); |
| } |
| |
| private static DerivedArtifact output( |
| String rootRelativePath, ActionLookupData generatingAction) { |
| DerivedArtifact result = |
| DerivedArtifact.create( |
| OUTPUT_ROOT, |
| OUTPUT_ROOT.getExecPath().getRelative(rootRelativePath), |
| generatingAction.getActionLookupKey()); |
| result.setGeneratingActionKey(generatingAction); |
| return result; |
| } |
| |
| private static SpecialArtifact tree(String rootRelativePath) { |
| SpecialArtifact result = |
| SpecialArtifact.create( |
| OUTPUT_ROOT, |
| OUTPUT_ROOT.getExecPath().getRelative(rootRelativePath), |
| KEY, |
| SpecialArtifactType.TREE); |
| result.setGeneratingActionKey(ACTION_LOOKUP_DATA_1); |
| return result; |
| } |
| } |