blob: a7d26a88619bc165c9a267353d415e0848fbf5e5 [file] [log] [blame]
// Copyright 2018 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.actions;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import com.google.auto.value.AutoValue;
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.ArtifactRoot.RootType;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.bugreport.BugReporter;
import com.google.devtools.build.lib.clock.BlazeClock;
import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Random;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
/** Unit test for {@link ActionInputMap}. */
@RunWith(TestParameterInjector.class)
public final class ActionInputMapTest {
// small hint to stress the map
private final ActionInputMap map = new ActionInputMap(BugReporter.defaultInstance(), 1);
private ArtifactRoot artifactRoot;
@Before
public void createArtifactRoot() {
FileSystem fs = new InMemoryFileSystem(BlazeClock.instance(), DigestHashFunction.SHA256);
artifactRoot =
ArtifactRoot.asDerivedRoot(fs.getPath("/execroot"), RootType.Output, "bazel-out");
}
@Test
public void basicPutAndLookup() {
assertThat(put("/abc/def", 5)).isTrue();
assertThat(map.sizeForDebugging()).isEqualTo(1);
assertContains("/abc/def", 5);
assertThat(map.getMetadata(PathFragment.create("blah"))).isNull();
assertThat(map.getInput("blah")).isNull();
}
@Test
public void put_ignoresSubsequentPuts() {
assertThat(put("/abc/def", 5)).isTrue();
assertThat(map.sizeForDebugging()).isEqualTo(1);
assertThat(put("/abc/def", 6)).isFalse();
assertThat(map.sizeForDebugging()).isEqualTo(1);
assertThat(put("/ghi/jkl", 7)).isTrue();
assertThat(map.sizeForDebugging()).isEqualTo(2);
assertThat(put("/ghi/jkl", 8)).isFalse();
assertThat(map.sizeForDebugging()).isEqualTo(2);
assertContains("/abc/def", 5);
assertContains("/ghi/jkl", 7);
}
@Test
public void clear_removesAllElements() {
ActionInput input1 = new TestInput("/abc/def");
ActionInput input2 = new TestInput("/ghi/jkl");
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact treeChild = TreeFileArtifact.createTreeOutput(tree, "child");
map.putWithNoDepOwner(input1, TestMetadata.create(1));
map.putWithNoDepOwner(input2, TestMetadata.create(2));
map.putTreeArtifact(
tree,
TreeArtifactValue.newBuilder(tree).putChild(treeChild, TestMetadata.create(3)).build(),
/*depOwner=*/ null);
// Sanity check
assertThat(map.sizeForDebugging()).isEqualTo(3);
map.clear();
assertThat(map.sizeForDebugging()).isEqualTo(0);
assertDoesNotContain(input1);
assertDoesNotContain(input2);
assertDoesNotContain(tree);
assertDoesNotContain(treeChild);
}
@Test
public void putTreeArtifact_addsEmptyTreeArtifact() {
SpecialArtifact tree = createTreeArtifact("tree");
map.putTreeArtifact(tree, TreeArtifactValue.empty(), /*depOwner=*/ null);
assertThat(map.sizeForDebugging()).isEqualTo(1);
assertContainsEqualMetadata(tree, TreeArtifactValue.empty().getMetadata());
}
@Test
public void putTreeArtifact_addsTreeArtifactAndAllChildren() {
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact child1 = TreeFileArtifact.createTreeOutput(tree, "child1");
FileArtifactValue child1Metadata = TestMetadata.create(1);
TreeFileArtifact child2 = TreeFileArtifact.createTreeOutput(tree, "child2");
FileArtifactValue child2Metadata = TestMetadata.create(2);
TreeArtifactValue treeValue =
TreeArtifactValue.newBuilder(tree)
.putChild(child1, child1Metadata)
.putChild(child2, child2Metadata)
.build();
map.putTreeArtifact(tree, treeValue, /*depOwner=*/ null);
assertThat(map.sizeForDebugging()).isEqualTo(1);
assertContainsEqualMetadata(tree, treeValue.getMetadata());
assertContainsSameInstance(child1, child1Metadata);
assertContainsSameInstance(child2, child2Metadata);
}
@Test
public void putTreeArtifact_mixedTreeAndFiles_addsTreeAndChildren() {
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact child = TreeFileArtifact.createTreeOutput(tree, "child");
FileArtifactValue childMetadata = TestMetadata.create(1);
ActionInput file = ActionInputHelper.fromPath("file");
FileArtifactValue fileMetadata = TestMetadata.create(2);
map.putWithNoDepOwner(file, fileMetadata);
TreeArtifactValue treeValue =
TreeArtifactValue.newBuilder(tree).putChild(child, childMetadata).build();
map.putTreeArtifact(tree, treeValue, /*depOwner=*/ null);
assertContainsEqualMetadata(tree, treeValue.getMetadata());
assertContainsSameInstance(child, childMetadata);
assertContainsSameInstance(file, fileMetadata);
}
@Test
public void putTreeArtifact_multipleTrees_addsAllTreesAndChildren() {
SpecialArtifact tree1 = createTreeArtifact("tree1");
TreeFileArtifact tree1Child = TreeFileArtifact.createTreeOutput(tree1, "child");
FileArtifactValue tree1ChildMetadata = TestMetadata.create(1);
SpecialArtifact tree2 = createTreeArtifact("tree2");
TreeFileArtifact tree2Child = TreeFileArtifact.createTreeOutput(tree2, "child");
FileArtifactValue tree2ChildMetadata = TestMetadata.create(2);
TreeArtifactValue tree1Value =
TreeArtifactValue.newBuilder(tree1).putChild(tree1Child, tree1ChildMetadata).build();
TreeArtifactValue tree2Value =
TreeArtifactValue.newBuilder(tree2).putChild(tree2Child, tree2ChildMetadata).build();
map.putTreeArtifact(tree1, tree1Value, /*depOwner=*/ null);
map.putTreeArtifact(tree2, tree2Value, /*depOwner=*/ null);
assertContainsEqualMetadata(tree1, tree1Value.getMetadata());
assertContainsSameInstance(tree1Child, tree1ChildMetadata);
assertContainsEqualMetadata(tree2, tree2Value.getMetadata());
assertContainsSameInstance(tree2Child, tree2ChildMetadata);
}
@Test
public void putTreeArtifact_multipleTreesUnderSameDirectory_addsAllTrees() {
SpecialArtifact tree1 = createTreeArtifact("dir/tree1");
SpecialArtifact tree2 = createTreeArtifact("dir/tree2");
SpecialArtifact tree3 = createTreeArtifact("dir/tree3");
map.putTreeArtifact(tree1, TreeArtifactValue.empty(), /*depOwner=*/ null);
map.putTreeArtifact(tree2, TreeArtifactValue.empty(), /*depOwner=*/ null);
map.putTreeArtifact(tree3, TreeArtifactValue.empty(), /*depOwner=*/ null);
assertThat(map.getInput(tree1.getExecPathString())).isEqualTo(tree1);
assertThat(map.getInput(tree2.getExecPathString())).isEqualTo(tree2);
assertThat(map.getInput(tree3.getExecPathString())).isEqualTo(tree3);
}
@Test
public void putTreeArtifact_afterPutTreeArtifactWithSameExecPath_doesNothing() {
SpecialArtifact tree1 = createTreeArtifact("tree");
SpecialArtifact tree2 = createTreeArtifact("tree");
TreeFileArtifact tree2File = TreeFileArtifact.createTreeOutput(tree2, "file");
TreeArtifactValue tree2Value =
TreeArtifactValue.newBuilder(tree2).putChild(tree2File, TestMetadata.create(1)).build();
map.putTreeArtifact(tree1, TreeArtifactValue.empty(), /*depOwner=*/ null);
map.putTreeArtifact(tree2, tree2Value, /*depOwner=*/ null);
assertContainsEqualMetadata(tree1, TreeArtifactValue.empty().getMetadata());
assertThat(map.getMetadata(tree2)).isEqualTo(TreeArtifactValue.empty().getMetadata());
assertDoesNotContain(tree2File);
}
@Test
public void putTreeArtifact_sameExecPathAsARegularFile_fails() {
SpecialArtifact tree = createTreeArtifact("tree");
ActionInput file = ActionInputHelper.fromPath(tree.getExecPath());
map.put(file, TestMetadata.create(1), /*depOwner=*/ null);
assertThrows(
IllegalArgumentException.class,
() -> map.putTreeArtifact(tree, TreeArtifactValue.empty(), /*depOwner=*/ null));
}
private enum PutOrder {
DECLARED,
REVERSE {
@Override
void runPuts(Runnable put1, Runnable put2) {
super.runPuts(put2, put1);
}
};
void runPuts(Runnable put1, Runnable put2) {
put1.run();
put2.run();
}
}
@Test
public void putTreeArtifact_nestedFile_returnsNestedFileFromExecPath(
@TestParameter PutOrder putOrder) {
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact treeFile = TreeFileArtifact.createTreeOutput(tree, "file");
FileArtifactValue treeFileMetadata = TestMetadata.create(1);
ActionInput file = ActionInputHelper.fromPath(treeFile.getExecPath());
FileArtifactValue fileMetadata = TestMetadata.create(1); // identical to `tree/file` file.
TreeArtifactValue treeValue =
TreeArtifactValue.newBuilder(tree).putChild(treeFile, treeFileMetadata).build();
putOrder.runPuts(
() -> map.put(file, fileMetadata, /*depOwner=*/ null),
() -> map.putTreeArtifact(tree, treeValue, /*depOwner=*/ null));
assertThat(map.getMetadata(file)).isSameInstanceAs(fileMetadata);
assertThat(map.getMetadata(treeFile)).isSameInstanceAs(treeFileMetadata);
assertThat(map.getMetadata(treeFile.getExecPath())).isSameInstanceAs(fileMetadata);
assertThat(map.getInput(treeFile.getExecPathString())).isSameInstanceAs(file);
}
@Test
public void putTreeArtifact_nestedTree_returnsOuterEntriesForOverlappingFiles(
@TestParameter PutOrder putOrder) {
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact onlyOuterTreeFile = TreeFileArtifact.createTreeOutput(tree, "file1");
FileArtifactValue onlyOuterTreeFileMetadata = TestMetadata.create(1);
TreeFileArtifact treeFile = TreeFileArtifact.createTreeOutput(tree, "dir/file2");
FileArtifactValue treeFileMetadata = TestMetadata.create(2);
TreeArtifactValue treeValue =
TreeArtifactValue.newBuilder(tree)
.putChild(treeFile, treeFileMetadata)
.putChild(onlyOuterTreeFile, onlyOuterTreeFileMetadata)
.build();
SpecialArtifact nestedTree = createTreeArtifact("tree/dir");
TreeFileArtifact nestedTreeFile = TreeFileArtifact.createTreeOutput(nestedTree, "file2");
FileArtifactValue nestedTreeFileMetadata = TestMetadata.create(2); // Same as treeFileMetadata.
TreeArtifactValue nestedTreeValue =
TreeArtifactValue.newBuilder(nestedTree)
.putChild(nestedTreeFile, nestedTreeFileMetadata)
.build();
putOrder.runPuts(
() -> map.putTreeArtifact(tree, treeValue, /*depOwner=*/ null),
() -> map.putTreeArtifact(nestedTree, nestedTreeValue, /*depOwner=*/ null));
assertContainsEqualMetadata(tree, treeValue.getMetadata());
assertContainsEqualMetadata(nestedTree, nestedTreeValue.getMetadata());
assertThat(map.getMetadata(treeFile)).isSameInstanceAs(treeFileMetadata);
assertThat(map.getMetadata(nestedTreeFile)).isSameInstanceAs(nestedTreeFileMetadata);
assertThat(map.getMetadata(treeFile.getExecPath())).isSameInstanceAs(treeFileMetadata);
assertThat(map.getInput(treeFile.getExecPathString())).isSameInstanceAs(treeFile);
assertContainsSameInstance(onlyOuterTreeFile, onlyOuterTreeFileMetadata);
}
@Test
public void putTreeArtifact_omittedTree_addsEntryWithNoChildren() {
SpecialArtifact tree = createTreeArtifact("tree");
map.putTreeArtifact(tree, TreeArtifactValue.OMITTED_TREE_MARKER, /*depOwner=*/ null);
assertContainsSameInstance(tree, FileArtifactValue.OMITTED_FILE_MARKER);
}
@Test
public void put_treeFileArtifact_addsEntry() {
TreeFileArtifact treeFile =
TreeFileArtifact.createTreeOutput(createTreeArtifact("tree"), "file");
FileArtifactValue metadata = TestMetadata.create(1);
map.put(treeFile, metadata, /*depOwner=*/ null);
assertContainsSameInstance(treeFile, metadata);
}
@Test
public void put_sameExecPathAsATree_fails() {
SpecialArtifact tree = createTreeArtifact("tree");
ActionInput file = ActionInputHelper.fromPath(tree.getExecPath());
FileArtifactValue fileMetadata = TestMetadata.create(1);
map.putTreeArtifact(tree, TreeArtifactValue.empty(), /*depOwner=*/ null);
assertThrows(
IllegalArgumentException.class, () -> map.put(file, fileMetadata, /*depOwner=*/ null));
}
@Test
public void put_treeArtifact_fails() {
SpecialArtifact tree = createTreeArtifact("tree");
FileArtifactValue metadata = TestMetadata.create(1);
assertThrows(IllegalArgumentException.class, () -> map.put(tree, metadata, /*depOwner=*/ null));
}
@Test
public void getMetadata_actionInputWithTreeExecPath_returnsTreeArtifactEntries() {
SpecialArtifact tree = createTreeArtifact("tree");
map.putTreeArtifact(tree, TreeArtifactValue.empty(), /*depOwner=*/ null);
ActionInput input = ActionInputHelper.fromPath(tree.getExecPath());
assertThat(map.getMetadata(input)).isEqualTo(TreeArtifactValue.empty().getMetadata());
}
@Test
public void getMetadata_actionInputWithTreeFileExecPath_returnsTreeArtifactEntries() {
BugReporter bugReporter = mock(BugReporter.class);
ArgumentCaptor<Throwable> exceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
doNothing().when(bugReporter).sendBugReport(exceptionCaptor.capture());
ActionInputMap inputMap = new ActionInputMap(bugReporter, /*sizeHint=*/ 1);
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact treeFile = TreeFileArtifact.createTreeOutput(tree, "file");
FileArtifactValue treeFileMetadata = TestMetadata.create(1);
TreeArtifactValue treeValue =
TreeArtifactValue.newBuilder(tree).putChild(treeFile, treeFileMetadata).build();
inputMap.putTreeArtifact(tree, treeValue, /*depOwner=*/ null);
ActionInput input = ActionInputHelper.fromPath(treeFile.getExecPath());
FileArtifactValue metadata = inputMap.getMetadata(input);
assertThat(metadata).isSameInstanceAs(treeFileMetadata);
assertThat(exceptionCaptor.getValue()).isInstanceOf(IllegalArgumentException.class);
assertThat(exceptionCaptor.getValue())
.hasMessageThat()
.isEqualTo("Tree artifact file: 'bazel-out/tree/file' referred to as an action input");
}
@Test
public void getMetadata_artifactWithTreeFileExecPath_returnsNull() {
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact treeFile = TreeFileArtifact.createTreeOutput(tree, "file");
TreeArtifactValue treeValue =
TreeArtifactValue.newBuilder(tree).putChild(treeFile, TestMetadata.create(1)).build();
map.putTreeArtifact(tree, treeValue, /*depOwner=*/ null);
Artifact artifact =
ActionsTestUtil.createArtifactWithExecPath(artifactRoot, treeFile.getExecPath());
// Even though we could match the artifact by exec path, it was not registered as a nested
// artifact -- only the tree file was.
assertThat(map.getMetadata(artifact)).isNull();
}
@Test
public void getMetadata_missingFileWithinTree_returnsNull() {
SpecialArtifact tree = createTreeArtifact("tree");
map.putTreeArtifact(
tree,
TreeArtifactValue.newBuilder(tree)
.putChild(TreeFileArtifact.createTreeOutput(tree, "file"), TestMetadata.create(1))
.build(),
/*depOwner=*/ null);
TreeFileArtifact nonexistentTreeFile = TreeFileArtifact.createTreeOutput(tree, "nonexistent");
assertDoesNotContain(nonexistentTreeFile);
}
@Test
public void getMetadata_treeFileUnderOmittedParent_fails() {
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact child = TreeFileArtifact.createTreeOutput(tree, "file");
map.putTreeArtifact(tree, TreeArtifactValue.OMITTED_TREE_MARKER, /*depOwner=*/ null);
assertThrows(IllegalArgumentException.class, () -> map.getMetadata(child));
}
@Test
public void getMetadata_treeFileUnderFile_fails() {
SpecialArtifact tree = createTreeArtifact("tree");
TreeFileArtifact child = TreeFileArtifact.createTreeOutput(tree, "file");
ActionInput file = ActionInputHelper.fromPath(tree.getExecPath());
map.put(file, TestMetadata.create(1), /*depOwner=*/ null);
assertThrows(IllegalArgumentException.class, () -> map.getMetadata(child));
}
@Test
public void getters_missingTree_returnNull() {
map.putTreeArtifact(createTreeArtifact("tree"), TreeArtifactValue.empty(), /*depOwner=*/ null);
SpecialArtifact otherTree = createTreeArtifact("other");
assertDoesNotContain(otherTree);
assertDoesNotContain(TreeFileArtifact.createTreeOutput(otherTree, "child"));
}
@Test
public void stress() {
ArrayList<TestEntry> data = new ArrayList<>();
{
Random rng = new Random();
HashSet<TestInput> deduper = new HashSet<>();
for (int i = 0; i < 100000; ++i) {
byte[] bytes = new byte[80];
rng.nextBytes(bytes);
for (int j = 0; j < bytes.length; ++j) {
bytes[j] &= ((byte) 0x7f);
}
TestInput nextInput = new TestInput(new String(bytes, US_ASCII));
if (deduper.add(nextInput)) {
data.add(new TestEntry(nextInput, TestMetadata.create(i)));
}
}
}
for (int iteration = 0; iteration < 20; ++iteration) {
map.clear();
Collections.shuffle(data);
for (int i = 0; i < data.size(); ++i) {
TestEntry entry = data.get(i);
assertThat(map.putWithNoDepOwner(entry.input, entry.metadata)).isTrue();
}
assertThat(map.sizeForDebugging()).isEqualTo(data.size());
for (int i = 0; i < data.size(); ++i) {
TestEntry entry = data.get(i);
assertThat(map.getMetadata(entry.input)).isEqualTo(entry.metadata);
}
}
}
private boolean put(String execPath, int value) {
return map.putWithNoDepOwner(new TestInput(execPath), TestMetadata.create(value));
}
private void assertContains(String execPath, int value) {
assertThat(map.getMetadata(new TestInput(execPath))).isEqualTo(TestMetadata.create(value));
assertThat(map.getMetadata(PathFragment.create(execPath)))
.isEqualTo(TestMetadata.create(value));
assertThat(map.getInput(execPath)).isEqualTo(new TestInput(execPath));
}
private void assertDoesNotContain(ActionInput input) {
assertThat(map.getMetadata(input)).isNull();
assertThat(map.getMetadata(input.getExecPath())).isNull();
assertThat(map.getInput(input.getExecPathString())).isNull();
}
private void assertContainsEqualMetadata(ActionInput input, FileArtifactValue metadata) {
assertThat(map.getMetadata(input)).isEqualTo(metadata);
assertThat(map.getMetadata(input.getExecPath())).isEqualTo(metadata);
assertThat(map.getInput(input.getExecPathString())).isSameInstanceAs(input);
}
private void assertContainsSameInstance(ActionInput input, FileArtifactValue metadata) {
assertThat(map.getMetadata(input)).isSameInstanceAs(metadata);
assertThat(map.getMetadata(input.getExecPath())).isSameInstanceAs(metadata);
assertThat(map.getInput(input.getExecPathString())).isSameInstanceAs(input);
}
private static class TestEntry {
public final TestInput input;
public final TestMetadata metadata;
public TestEntry(TestInput input, TestMetadata metadata) {
this.input = input;
this.metadata = metadata;
}
}
private static class TestInput implements ActionInput {
private final PathFragment fragment;
public TestInput(String fragment) {
this.fragment = PathFragment.create(fragment);
}
@Override
public boolean isSymlink() {
return false;
}
@Override
public PathFragment getExecPath() {
return fragment;
}
@Override
public String getExecPathString() {
return fragment.toString();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof TestInput)) {
return false;
}
if (this == other) {
return true;
}
return fragment.equals(((TestInput) other).fragment);
}
@Override
public int hashCode() {
return fragment.hashCode();
}
}
private SpecialArtifact createTreeArtifact(String relativeExecPath) {
return ActionsTestUtil.createTreeArtifactWithGeneratingAction(
artifactRoot, artifactRoot.getExecPath().getRelative(relativeExecPath));
}
@AutoValue
abstract static class TestMetadata extends FileArtifactValue {
abstract int id();
static TestMetadata create(int id) {
return new AutoValue_ActionInputMapTest_TestMetadata(id);
}
@Override
public FileStateType getType() {
return FileStateType.REGULAR_FILE;
}
@Override
public byte[] getDigest() {
return DigestHashFunction.SHA256.getHashFunction().hashInt(id()).asBytes();
}
@Override
public long getSize() {
return id();
}
@Override
public long getModifiedTime() {
throw new UnsupportedOperationException();
}
@Override
public boolean wasModifiedSinceDigest(Path path) {
throw new UnsupportedOperationException();
}
@Override
public boolean isRemote() {
return false;
}
@Override
public FileContentsProxy getContentsProxy() {
throw new UnsupportedOperationException();
}
}
}