/*
* Copyright 2013-present Facebook, Inc.
*
* 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.facebook.buck.rules;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import com.facebook.buck.artifact_cache.ArtifactCache;
import com.facebook.buck.artifact_cache.ArtifactInfo;
import com.facebook.buck.artifact_cache.CacheReadMode;
import com.facebook.buck.artifact_cache.NoopArtifactCache;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.DefaultBuckEventBus;
import com.facebook.buck.io.BorrowablePath;
import com.facebook.buck.io.MorePathsForTests;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildId;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetFactory;
import com.facebook.buck.testutil.FakeProjectFilesystem;
import com.facebook.buck.testutil.MoreAsserts;
import com.facebook.buck.testutil.Zip;
import com.facebook.buck.testutil.integration.TemporaryPaths;
import com.facebook.buck.timing.DefaultClock;
import com.facebook.buck.timing.FakeClock;
import com.facebook.buck.util.cache.DefaultFileHashCache;
import com.facebook.buck.util.cache.FileHashCache;
import com.facebook.buck.util.cache.StackedFileHashCache;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.HashCode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class BuildInfoRecorderTest {
@Rule public ExpectedException thrown = ExpectedException.none();
@Rule public TemporaryPaths tmp = new TemporaryPaths();
private static final BuildTarget BUILD_TARGET = BuildTargetFactory.newInstance("//foo:bar");
@Test
public void testAddMetadataMultipleValues() {
BuildInfoRecorder buildInfoRecorder = createBuildInfoRecorder(new FakeProjectFilesystem());
buildInfoRecorder.addMetadata("foo", ImmutableList.of("bar", "biz", "baz"));
assertEquals("[\"bar\",\"biz\",\"baz\"]", buildInfoRecorder.getMetadataFor("foo"));
}
@Test
public void testWriteMetadataToDisk() throws IOException {
FakeProjectFilesystem filesystem = new FakeProjectFilesystem();
BuildInfoStore store = new FilesystemBuildInfoStore(filesystem);
BuildInfoRecorder buildInfoRecorder = createBuildInfoRecorder(filesystem);
buildInfoRecorder.addMetadata("key1", "value1");
buildInfoRecorder.writeMetadataToDisk(/* clearExistingMetadata */ true);
OnDiskBuildInfo onDiskBuildInfo = new DefaultOnDiskBuildInfo(BUILD_TARGET, filesystem, store);
assertOnDiskBuildInfoHasMetadata(onDiskBuildInfo, "key1", "value1");
buildInfoRecorder = createBuildInfoRecorder(filesystem);
buildInfoRecorder.addMetadata("key2", "value2");
buildInfoRecorder.writeMetadataToDisk(/* clearExistingMetadata */ false);
onDiskBuildInfo = new DefaultOnDiskBuildInfo(BUILD_TARGET, filesystem, store);
assertOnDiskBuildInfoHasMetadata(onDiskBuildInfo, "key1", "value1");
assertOnDiskBuildInfoHasMetadata(onDiskBuildInfo, "key2", "value2");
buildInfoRecorder = createBuildInfoRecorder(filesystem);
buildInfoRecorder.addMetadata("key3", "value3");
buildInfoRecorder.writeMetadataToDisk(/* clearExistingMetadata */ true);
onDiskBuildInfo = new DefaultOnDiskBuildInfo(BUILD_TARGET, filesystem, store);
assertOnDiskBuildInfoHasMetadata(onDiskBuildInfo, "key3", "value3");
assertOnDiskBuildInfoDoesNotHaveMetadata(onDiskBuildInfo, "key1");
assertOnDiskBuildInfoDoesNotHaveMetadata(onDiskBuildInfo, "key2");
// Verify build metadata gets written correctly.
buildInfoRecorder = createBuildInfoRecorder(filesystem);
buildInfoRecorder.addBuildMetadata("build", "metadata");
buildInfoRecorder.writeMetadataToDisk(/* clearExistingMetadata */ true);
onDiskBuildInfo = new DefaultOnDiskBuildInfo(BUILD_TARGET, filesystem, store);
assertOnDiskBuildInfoHasMetadata(onDiskBuildInfo, "build", "metadata");
// Verify additional info build metadata always gets written.
buildInfoRecorder = createBuildInfoRecorder(filesystem);
buildInfoRecorder.writeMetadataToDisk(/* clearExistingMetadata */ true);
onDiskBuildInfo = new DefaultOnDiskBuildInfo(BUILD_TARGET, filesystem, store);
assertTrue(onDiskBuildInfo.getValue(BuildInfo.MetadataKey.ADDITIONAL_INFO).isPresent());
}
@Test
public void testCannotRecordArtifactWithAbsolutePath() {
Path absPath = MorePathsForTests.rootRelativePath("some/absolute/path.txt");
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage(
String.format(BuildInfoRecorder.ABSOLUTE_PATH_ERROR_FORMAT, BUILD_TARGET, absPath));
FakeProjectFilesystem filesystem = new FakeProjectFilesystem();
BuildInfoRecorder buildInfoRecorder = createBuildInfoRecorder(filesystem);
buildInfoRecorder.recordArtifact(absPath);
}
@Test
public void testPerformUploadToArtifactCache() throws IOException, InterruptedException {
FakeProjectFilesystem filesystem = new FakeProjectFilesystem();
BuildInfoRecorder buildInfoRecorder = createBuildInfoRecorder(filesystem);
BuckEventBus bus = new DefaultBuckEventBus(new FakeClock(0), new BuildId("BUILD"));
final byte[] contents = "contents".getBytes();
Path file = Paths.get("file");
filesystem.writeBytesToPath(contents, file);
buildInfoRecorder.recordArtifact(file);
Path dir = Paths.get("dir");
filesystem.mkdirs(dir);
filesystem.writeBytesToPath(contents, dir.resolve("file"));
buildInfoRecorder.recordArtifact(dir);
// Record some metadata.
buildInfoRecorder.addMetadata("metadata", "metadata");
// Record some build metadata.
buildInfoRecorder.addBuildMetadata("build-metadata", "build-metadata");
buildInfoRecorder.writeMetadataToDisk(true);
final AtomicBoolean stored = new AtomicBoolean(false);
final ArtifactCache cache =
new NoopArtifactCache() {
@Override
public CacheReadMode getCacheReadMode() {
return CacheReadMode.READWRITE;
}
@Override
public ListenableFuture<Void> store(ArtifactInfo info, BorrowablePath output) {
stored.set(true);
// Verify the build metadata.
assertThat(
info.getMetadata().get("build-metadata"), Matchers.equalTo("build-metadata"));
// Verify zip contents
try (Zip zip = new Zip(output.getPath(), /* forWriting */ false)) {
assertEquals(
ImmutableSet.of(
"",
"dir/",
"buck-out/",
"buck-out/bin/",
"buck-out/bin/foo/",
"buck-out/bin/foo/.bar/",
"buck-out/bin/foo/.bar/metadata/"),
zip.getDirNames());
assertEquals(
ImmutableSet.of("dir/file", "file", "buck-out/bin/foo/.bar/metadata/metadata"),
zip.getFileNames());
assertArrayEquals(contents, zip.readFully("file"));
assertArrayEquals(contents, zip.readFully("dir/file"));
} catch (IOException e) {
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
}
return Futures.immediateFuture(null);
}
};
buildInfoRecorder.performUploadToArtifactCache(ImmutableSet.of(new RuleKey("aa")), cache, bus);
assertTrue(stored.get());
}
@Test
public void testGetOutputSize() throws IOException {
FakeProjectFilesystem filesystem = new FakeProjectFilesystem();
BuildInfoRecorder buildInfoRecorder = createBuildInfoRecorder(filesystem);
byte[] contents = "contents".getBytes();
Path file = Paths.get("file");
filesystem.writeBytesToPath(contents, file);
buildInfoRecorder.recordArtifact(file);
Path dir = Paths.get("dir");
filesystem.mkdirs(dir);
filesystem.writeBytesToPath(contents, dir.resolve("file1"));
filesystem.writeBytesToPath(contents, dir.resolve("file2"));
buildInfoRecorder.recordArtifact(dir);
assertEquals(3 * contents.length, buildInfoRecorder.getOutputSize());
}
@Test
public void testGetOutputHash() throws IOException {
FakeProjectFilesystem filesystem = new FakeProjectFilesystem();
FileHashCache fileHashCache =
new StackedFileHashCache(
ImmutableList.of(DefaultFileHashCache.createDefaultFileHashCache(filesystem)));
BuildInfoRecorder buildInfoRecorder = createBuildInfoRecorder(filesystem);
byte[] contents = "contents".getBytes();
Path file = Paths.get("file");
filesystem.writeBytesToPath(contents, file);
buildInfoRecorder.recordArtifact(file);
Path dir = Paths.get("dir");
filesystem.mkdirs(dir);
filesystem.writeBytesToPath(contents, dir.resolve("file1"));
filesystem.writeBytesToPath(contents, dir.resolve("file2"));
buildInfoRecorder.recordArtifact(dir);
fileHashCache.invalidateAll();
HashCode current = buildInfoRecorder.getOutputHash(fileHashCache);
// Test that getting the hash again results in the same hashcode.
fileHashCache.invalidateAll();
assertEquals(current, buildInfoRecorder.getOutputHash(fileHashCache));
// Verify that changing a file changes the hash.
filesystem.writeContentsToPath("something else", file);
fileHashCache.invalidateAll();
HashCode updated = buildInfoRecorder.getOutputHash(fileHashCache);
assertNotEquals(current, updated);
// Verify that changing a file under a directory changes the hash.
filesystem.writeContentsToPath("something else", dir.resolve("file1"));
current = updated;
fileHashCache.invalidateAll();
updated = buildInfoRecorder.getOutputHash(fileHashCache);
assertNotEquals(current, updated);
// Test that adding a file updates the hash.
Path added = Paths.get("added");
filesystem.writeBytesToPath(contents, added);
buildInfoRecorder.recordArtifact(added);
current = updated;
fileHashCache.invalidateAll();
updated = buildInfoRecorder.getOutputHash(fileHashCache);
assertNotEquals(current, updated);
// Test that adding a file under a recorded directory updates the hash.
Path addedUnderDir = dir.resolve("added");
filesystem.writeBytesToPath(contents, addedUnderDir);
current = updated;
fileHashCache.invalidateAll();
updated = buildInfoRecorder.getOutputHash(fileHashCache);
assertNotEquals(current, updated);
}
private static void assertOnDiskBuildInfoHasMetadata(
OnDiskBuildInfo onDiskBuildInfo, String key, String value) {
MoreAsserts.assertOptionalValueEquals(
String.format("BuildInfoRecorder must record '%s:%s' to the filesystem.", key, value),
value,
onDiskBuildInfo.getValue(key));
}
private static void assertOnDiskBuildInfoDoesNotHaveMetadata(
OnDiskBuildInfo onDiskBuildInfo, String key) {
assertFalse(
String.format("BuildInfoRecorder should have cleared this metadata key: %s", key),
onDiskBuildInfo.getValue(key).isPresent());
}
private static BuildInfoRecorder createBuildInfoRecorder(ProjectFilesystem filesystem) {
return new BuildInfoRecorder(
BUILD_TARGET,
filesystem,
new FilesystemBuildInfoStore(filesystem),
new DefaultClock(),
new BuildId(),
ImmutableMap.of());
}
}