blob: e05464299f3c80af0b059178f4f611a305ef21fb [file] [log] [blame]
// Copyright 2015 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 static com.google.devtools.build.lib.skyframe.FileArtifactValue.create;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.Action.MiddlemanType;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.MissingInputFileException;
import com.google.devtools.build.lib.actions.Root;
import com.google.devtools.build.lib.actions.util.TestAction.DummyAction;
import com.google.devtools.build.lib.events.NullEventHandler;
import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
import com.google.devtools.build.lib.skyframe.ActionLookupValue.ActionLookupKey;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.util.BlazeClock;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
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.devtools.build.skyframe.EvaluationResult;
import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator;
import com.google.devtools.build.skyframe.MemoizingEvaluator;
import com.google.devtools.build.skyframe.RecordingDifferencer;
import com.google.devtools.build.skyframe.SequentialBuildDriver;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
/**
* Tests for {@link ArtifactFunction}.
*/
// Doesn't actually need any particular Skyframe, but is only relevant to Skyframe full mode.
@RunWith(JUnit4.class)
public class ArtifactFunctionTest {
private static final SkyKey OWNER_KEY = new SkyKey(SkyFunctions.ACTION_LOOKUP, "OWNER");
private static final ActionLookupKey ALL_OWNER = new SingletonActionLookupKey();
private Set<Action> actions;
private boolean fastDigest = false;
private RecordingDifferencer differencer = new RecordingDifferencer();
private SequentialBuildDriver driver;
private MemoizingEvaluator evaluator;
private Path root;
private TimestampGranularityMonitor tsgm = new TimestampGranularityMonitor(BlazeClock.instance());
@Before
public final void setUp() throws Exception {
setupRoot(new CustomInMemoryFs());
AtomicReference<PathPackageLocator> pkgLocator =
new AtomicReference<>(PathPackageLocator.EMPTY);
ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(pkgLocator);
differencer = new RecordingDifferencer();
evaluator =
new InMemoryMemoizingEvaluator(
ImmutableMap.of(
SkyFunctions.FILE_STATE, new FileStateFunction(tsgm, externalFilesHelper),
SkyFunctions.FILE, new FileFunction(pkgLocator, tsgm, externalFilesHelper),
SkyFunctions.ARTIFACT, new ArtifactFunction(Predicates.<PathFragment>alwaysFalse()),
SkyFunctions.ACTION_EXECUTION, new SimpleActionExecutionFunction()),
differencer);
driver = new SequentialBuildDriver(evaluator);
PrecomputedValue.BUILD_ID.set(differencer, UUID.randomUUID());
actions = new HashSet<>();
}
private void setupRoot(CustomInMemoryFs fs) {
root = fs.getPath(TestUtils.tmpDir());
}
private void assertFileArtifactValueMatches(boolean expectDigest) throws Throwable {
Artifact output = createDerivedArtifact("output");
Path path = output.getPath();
file(path, "contents");
assertValueMatches(path.stat(), expectDigest ? path.getMD5Digest() : null, evaluateFAN(output));
}
@Test
public void testBasicArtifact() throws Throwable {
fastDigest = false;
assertFileArtifactValueMatches(/*expectDigest=*/ true);
}
@Test
public void testBasicArtifactWithXattr() throws Throwable {
fastDigest = true;
assertFileArtifactValueMatches(/*expectDigest=*/ true);
}
@Test
public void testMissingNonMandatoryArtifact() throws Throwable {
Artifact input = createSourceArtifact("input1");
assertNotNull(evaluateArtifactValue(input, /*mandatory=*/ false));
}
@Test
public void testMissingMandatoryArtifact() throws Throwable {
Artifact input = createSourceArtifact("input1");
try {
evaluateArtifactValue(input, /*mandatory=*/ true);
fail();
} catch (MissingInputFileException ex) {
// Expected.
}
}
@Test
public void testMiddlemanArtifact() throws Throwable {
Artifact output = createDerivedArtifact("output");
Artifact input1 = createSourceArtifact("input1");
Artifact input2 = createDerivedArtifact("input2");
Action action =
new DummyAction(
ImmutableList.of(input1, input2), output, MiddlemanType.AGGREGATING_MIDDLEMAN);
// Overwrite default generating action with this one.
for (Iterator<Action> it = actions.iterator(); it.hasNext(); ) {
if (it.next().getOutputs().contains(output)) {
it.remove();
break;
}
}
actions.add(action);
file(input2.getPath(), "contents");
file(input1.getPath(), "source contents");
evaluate(
Iterables.toArray(
ArtifactValue.mandatoryKeys(ImmutableSet.of(input2, input1, input2)), SkyKey.class));
ArtifactValue value = evaluateArtifactValue(output);
assertThat(((AggregatingArtifactValue) value).getInputs())
.containsExactly(Pair.of(input1, create(input1)), Pair.of(input2, create(input2)));
}
@Test
public void testIOException() throws Exception {
fastDigest = false;
final IOException exception = new IOException("beep");
setupRoot(
new CustomInMemoryFs() {
@Override
public byte[] getMD5Digest(Path path) throws IOException {
throw exception;
}
});
Artifact artifact = createDerivedArtifact("no-read");
writeFile(artifact.getPath(), "content");
try {
create(createDerivedArtifact("no-read"));
fail();
} catch (IOException e) {
assertSame(exception, e);
}
}
/**
* Tests that ArtifactFunction rethrows transitive {@link IOException}s as
* {@link MissingInputFileException}s.
*/
@Test
public void testIOException_EndToEnd() throws Throwable {
final IOException exception = new IOException("beep");
setupRoot(
new CustomInMemoryFs() {
@Override
public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
if (path.getBaseName().equals("bad")) {
throw exception;
}
return super.stat(path, followSymlinks);
}
});
try {
evaluateArtifactValue(createSourceArtifact("bad"));
fail();
} catch (MissingInputFileException e) {
assertThat(e.getMessage()).contains(exception.getMessage());
}
}
@Test
public void testNoMtimeIfNonemptyFile() throws Exception {
Artifact artifact = createDerivedArtifact("no-digest");
Path path = artifact.getPath();
writeFile(path, "hello"); //Non-empty file.
FileArtifactValue value = create(artifact);
assertArrayEquals(path.getMD5Digest(), value.getDigest());
try {
value.getModifiedTime();
fail("mtime for non-empty file should not be stored.");
} catch (IllegalStateException e) {
// Expected.
}
}
@Test
public void testDirectory() throws Exception {
Artifact artifact = createDerivedArtifact("dir");
Path path = artifact.getPath();
FileSystemUtils.createDirectoryAndParents(path);
path.setLastModifiedTime(1L);
FileArtifactValue value = create(artifact);
assertNull(value.getDigest());
assertEquals(1L, value.getModifiedTime());
}
// Empty files need to store their mtimes, so touching an empty file
// can be used to trigger rebuilds.
@Test
public void testEmptyFile() throws Exception {
Artifact artifact = createDerivedArtifact("empty");
Path path = artifact.getPath();
writeFile(path, "");
path.setLastModifiedTime(1L);
FileArtifactValue value = create(artifact);
assertArrayEquals(path.getMD5Digest(), value.getDigest());
assertEquals(1L, value.getModifiedTime());
assertEquals(0L, value.getSize());
}
@Test
public void testEquality() throws Exception {
Artifact artifact1 = createDerivedArtifact("artifact1");
Artifact artifact2 = createDerivedArtifact("artifact2");
Artifact diffDigest = createDerivedArtifact("diffDigest");
Artifact diffMtime = createDerivedArtifact("diffMtime");
Artifact empty1 = createDerivedArtifact("empty1");
Artifact empty2 = createDerivedArtifact("empty2");
Artifact empty3 = createDerivedArtifact("empty3");
Artifact dir1 = createDerivedArtifact("dir1");
Artifact dir2 = createDerivedArtifact("dir2");
Artifact dir3 = createDerivedArtifact("dir3");
Path path1 = artifact1.getPath();
Path path2 = artifact2.getPath();
Path digestPath = diffDigest.getPath();
Path mtimePath = diffMtime.getPath();
writeFile(artifact1.getPath(), "content");
writeFile(artifact2.getPath(), "content");
path1.setLastModifiedTime(0);
path2.setLastModifiedTime(0);
writeFile(diffDigest.getPath(), "1234567"); // Same size as artifact1.
digestPath.setLastModifiedTime(0);
writeFile(mtimePath, "content");
mtimePath.setLastModifiedTime(1);
Path emptyPath1 = empty1.getPath();
Path emptyPath2 = empty2.getPath();
Path emptyPath3 = empty3.getPath();
writeFile(emptyPath1, "");
writeFile(emptyPath2, "");
writeFile(emptyPath3, "");
emptyPath1.setLastModifiedTime(0L);
emptyPath2.setLastModifiedTime(1L);
emptyPath3.setLastModifiedTime(1L);
Path dirPath1 = dir1.getPath();
Path dirPath2 = dir2.getPath();
Path dirPath3 = dir3.getPath();
FileSystemUtils.createDirectoryAndParents(dirPath1);
FileSystemUtils.createDirectoryAndParents(dirPath2);
FileSystemUtils.createDirectoryAndParents(dirPath3);
dirPath1.setLastModifiedTime(0L);
dirPath2.setLastModifiedTime(1L);
dirPath3.setLastModifiedTime(1L);
EqualsTester equalsTester = new EqualsTester();
equalsTester
.addEqualityGroup(create(artifact1), create(artifact2), create(diffMtime))
.addEqualityGroup(create(empty1))
.addEqualityGroup(create(empty2), create(empty3))
.addEqualityGroup(create(dir1))
.addEqualityGroup(create(dir2), create(dir3))
.testEquals();
}
private void file(Path path, String contents) throws Exception {
FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
writeFile(path, contents);
}
private Artifact createSourceArtifact(String path) {
return new Artifact(new PathFragment(path), Root.asSourceRoot(root));
}
private Artifact createDerivedArtifact(String path) {
PathFragment execPath = new PathFragment("out").getRelative(path);
Path fullPath = root.getRelative(execPath);
Artifact output =
new Artifact(
fullPath, Root.asDerivedRoot(root, root.getRelative("out")), execPath, ALL_OWNER);
actions.add(new DummyAction(ImmutableList.<Artifact>of(), output));
return output;
}
private void assertValueMatches(FileStatus file, byte[] digest, FileArtifactValue value)
throws IOException {
assertEquals(file.getSize(), value.getSize());
if (digest == null) {
assertNull(value.getDigest());
assertEquals(file.getLastModifiedTime(), value.getModifiedTime());
} else {
assertArrayEquals(digest, value.getDigest());
}
}
private FileArtifactValue evaluateFAN(Artifact artifact) throws Throwable {
return ((FileArtifactValue) evaluateArtifactValue(artifact));
}
private ArtifactValue evaluateArtifactValue(Artifact artifact) throws Throwable {
return evaluateArtifactValue(artifact, /*isMandatory=*/ true);
}
private ArtifactValue evaluateArtifactValue(Artifact artifact, boolean mandatory)
throws Throwable {
SkyKey key = ArtifactValue.key(artifact, mandatory);
EvaluationResult<ArtifactValue> result = evaluate(ImmutableList.of(key).toArray(new SkyKey[0]));
if (result.hasError()) {
throw result.getError().getException();
}
return result.get(key);
}
private void setGeneratingActions() {
if (evaluator.getExistingValueForTesting(OWNER_KEY) == null) {
differencer.inject(ImmutableMap.of(OWNER_KEY, new ActionLookupValue(actions)));
}
}
private <E extends SkyValue> EvaluationResult<E> evaluate(SkyKey... keys)
throws InterruptedException {
setGeneratingActions();
return driver.evaluate(
Arrays.asList(keys), /*keepGoing=*/
false,
SkyframeExecutor.DEFAULT_THREAD_COUNT,
NullEventHandler.INSTANCE);
}
private static void writeFile(Path path, String contents) throws IOException {
FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
FileSystemUtils.writeContentAsLatin1(path, contents);
}
private static class SingletonActionLookupKey extends ActionLookupKey {
@Override
SkyKey getSkyKey() {
return OWNER_KEY;
}
@Override
SkyFunctionName getType() {
throw new UnsupportedOperationException();
}
}
/** Value Builder for actions that just stats and stores the output file (which must exist). */
private class SimpleActionExecutionFunction implements SkyFunction {
@Override
public SkyValue compute(SkyKey skyKey, Environment env) {
Map<Artifact, FileValue> artifactData = new HashMap<>();
Action action = (Action) skyKey.argument();
Artifact output = Iterables.getOnlyElement(action.getOutputs());
FileArtifactValue value;
if (action.getActionType() == MiddlemanType.NORMAL) {
try {
FileValue fileValue = ActionMetadataHandler.fileValueFromArtifact(output, null, tsgm);
artifactData.put(output, fileValue);
value = FileArtifactValue.create(output, fileValue);
} catch (IOException e) {
throw new IllegalStateException(e);
}
} else {
value = FileArtifactValue.DEFAULT_MIDDLEMAN;
}
return new ActionExecutionValue(artifactData, ImmutableMap.of(output, value));
}
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
}
/** InMemoryFileSystem that can pretend to do a fast digest. */
private class CustomInMemoryFs extends InMemoryFileSystem {
@Override
protected String getFastDigestFunctionType(Path path) {
return fastDigest ? "MD5" : null;
}
@Override
protected byte[] getFastDigest(Path path) throws IOException {
return fastDigest ? getMD5Digest(path) : null;
}
}
}