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