/*
* 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.testutil;
import com.facebook.buck.io.DefaultProjectFilesystemDelegate;
import com.facebook.buck.io.MoreFiles;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.timing.Clock;
import com.facebook.buck.timing.FakeClock;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.environment.Platform;
import com.facebook.buck.util.sha1.Sha1HashCode;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.file.CopyOption;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotLinkException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
// TODO(natthu): Implement methods that throw UnsupportedOperationException.
public class FakeProjectFilesystem extends ProjectFilesystem {
private static final Random RANDOM = new Random();
private static final Path DEFAULT_ROOT = Paths.get(".").toAbsolutePath().normalize();
private static final BasicFileAttributes DEFAULT_FILE_ATTRIBUTES =
new BasicFileAttributes() {
@Override
@Nullable
public FileTime lastModifiedTime() {
return null;
}
@Override
@Nullable
public FileTime lastAccessTime() {
return null;
}
@Override
@Nullable
public FileTime creationTime() {
return null;
}
@Override
public boolean isRegularFile() {
return true;
}
@Override
public boolean isDirectory() {
return false;
}
@Override
public boolean isSymbolicLink() {
return false;
}
@Override
public boolean isOther() {
return false;
}
@Override
public long size() {
return 0;
}
@Override
@Nullable
public Object fileKey() {
return null;
}
};
private static final BasicFileAttributes DEFAULT_DIR_ATTRIBUTES =
new BasicFileAttributes() {
@Override
@Nullable
public FileTime lastModifiedTime() {
return null;
}
@Override
@Nullable
public FileTime lastAccessTime() {
return null;
}
@Override
@Nullable
public FileTime creationTime() {
return null;
}
@Override
public boolean isRegularFile() {
return false;
}
@Override
public boolean isDirectory() {
return true;
}
@Override
public boolean isSymbolicLink() {
return false;
}
@Override
public boolean isOther() {
return false;
}
@Override
public long size() {
return 0;
}
@Override
@Nullable
public Object fileKey() {
return null;
}
};
private final Map<Path, byte[]> fileContents;
private final Map<Path, ImmutableSet<FileAttribute<?>>> fileAttributes;
private final Map<Path, FileTime> fileLastModifiedTimes;
private final Map<Path, Path> symLinks;
private final Set<Path> directories;
private final Clock clock;
/**
* @return A project filesystem in a temp directory that will be deleted recursively on jvm exit.
*/
public static ProjectFilesystem createRealTempFilesystem() {
final Path tempDir;
try {
tempDir = Files.createTempDirectory("pfs");
} catch (IOException e) {
throw new RuntimeException(e);
}
Runtime.getRuntime()
.addShutdownHook(
new Thread(
() -> {
try {
MoreFiles.deleteRecursively(tempDir);
} catch (IOException e) { // NOPMD
// Swallow. At least we tried, right?
}
}));
return new FakeProjectFilesystem(tempDir);
}
public static ProjectFilesystem createJavaOnlyFilesystem() throws InterruptedException {
return createJavaOnlyFilesystem("/opt/src/buck");
}
public static ProjectFilesystem createJavaOnlyFilesystem(String rootPath)
throws InterruptedException {
boolean isWindows = Platform.detect() == Platform.WINDOWS;
Configuration configuration = isWindows ? Configuration.windows() : Configuration.unix();
rootPath = isWindows ? "C:" + rootPath : rootPath;
FileSystem vfs = Jimfs.newFileSystem(configuration);
Path root = vfs.getPath(rootPath);
try {
Files.createDirectories(root);
} catch (IOException e) {
throw new RuntimeException(e);
}
return new ProjectFilesystem(root) {
@Override
public Path resolve(Path path) {
// Avoid resolving paths from different Java FileSystems.
return super.resolve(path.toString());
}
};
}
public FakeProjectFilesystem() {
this(DEFAULT_ROOT);
}
public FakeProjectFilesystem(Path root) {
this(new FakeClock(0), root, ImmutableSet.of());
}
public FakeProjectFilesystem(Clock clock) {
this(clock, DEFAULT_ROOT, ImmutableSet.of());
}
public FakeProjectFilesystem(Set<Path> files) {
this(new FakeClock(0), DEFAULT_ROOT, files);
}
public FakeProjectFilesystem(Clock clock, Path root, Set<Path> files) {
// For testing, we always use a DefaultProjectFilesystemDelegate so that the logic being
// exercised is always the same, even if a test using FakeProjectFilesystem is used on EdenFS.
super(root, new DefaultProjectFilesystemDelegate(root));
// We use LinkedHashMap to preserve insertion order, so the
// behavior of this test is consistent across versions. (It also lets
// us write tests which explicitly test iterating over entries in
// different orders.)
fileContents = new LinkedHashMap<>();
fileLastModifiedTimes = new LinkedHashMap<>();
FileTime modifiedTime = FileTime.fromMillis(clock.currentTimeMillis());
for (Path file : files) {
fileContents.put(file, new byte[0]);
fileLastModifiedTimes.put(file, modifiedTime);
}
fileAttributes = new LinkedHashMap<>();
symLinks = new LinkedHashMap<>();
directories = new LinkedHashSet<>();
directories.add(Paths.get(""));
for (Path file : files) {
Path dir = file.getParent();
while (dir != null) {
directories.add(dir);
dir = dir.getParent();
}
}
this.clock = Preconditions.checkNotNull(clock);
// Generally, tests don't care whether files exist.
ignoreValidityOfPaths = true;
}
@Override
protected boolean shouldVerifyConstructorArguments() {
return false;
}
public FakeProjectFilesystem setIgnoreValidityOfPaths(boolean shouldIgnore) {
this.ignoreValidityOfPaths = shouldIgnore;
return this;
}
protected byte[] getFileBytes(Path path) {
return Preconditions.checkNotNull(fileContents.get(MorePaths.normalize(path)));
}
private void rmFile(Path path) {
fileContents.remove(MorePaths.normalize(path));
fileAttributes.remove(MorePaths.normalize(path));
fileLastModifiedTimes.remove(MorePaths.normalize(path));
}
public ImmutableSet<FileAttribute<?>> getFileAttributesAtPath(Path path) {
return Preconditions.checkNotNull(fileAttributes.get(path));
}
public void clear() {
fileContents.clear();
fileAttributes.clear();
fileLastModifiedTimes.clear();
symLinks.clear();
directories.clear();
}
public BasicFileAttributes readBasicAttributes(Path pathRelativeToProjectRoot)
throws IOException {
if (!exists(pathRelativeToProjectRoot)) {
throw new NoSuchFileException(pathRelativeToProjectRoot.toString());
}
return isFile(pathRelativeToProjectRoot)
? FakeFileAttributes.forFileWithSize(pathRelativeToProjectRoot, 0)
: FakeFileAttributes.forDirectory(pathRelativeToProjectRoot);
}
@Override
public <A extends BasicFileAttributes> A readAttributes(
Path pathRelativeToProjectRoot, Class<A> type, LinkOption... options) throws IOException {
if (type == BasicFileAttributes.class) {
return type.cast(readBasicAttributes(pathRelativeToProjectRoot));
}
throw new UnsupportedOperationException("cannot mock instance of: " + type);
}
@Override
public boolean exists(Path path, LinkOption... options) {
return isFile(path) || isDirectory(path);
}
@Override
public long getFileSize(Path path) throws IOException {
if (!exists(path)) {
throw new NoSuchFileException(path.toString());
}
return getFileBytes(path).length;
}
@Override
public void deleteFileAtPath(Path path) throws IOException {
if (exists(path)) {
rmFile(path);
} else {
throw new NoSuchFileException(path.toString());
}
}
@Override
public boolean deleteFileAtPathIfExists(Path path) throws IOException {
if (exists(path)) {
rmFile(path);
return true;
} else {
return false;
}
}
@Override
public boolean isFile(Path path, LinkOption... options) {
return fileContents.containsKey(MorePaths.normalize(path));
}
@Override
public boolean isHidden(Path path) throws IOException {
return isFile(path) && path.getFileName().toString().startsWith(".");
}
@Override
public boolean isDirectory(Path path, LinkOption... linkOptions) {
return directories.contains(MorePaths.normalize(path));
}
@Override
public boolean isExecutable(Path child) {
return false;
}
/** Does not support symlinks. */
@Override
public final ImmutableCollection<Path> getDirectoryContents(final Path pathRelativeToProjectRoot)
throws IOException {
Preconditions.checkState(isDirectory(pathRelativeToProjectRoot));
return FluentIterable.from(fileContents.keySet())
.append(directories)
.filter(
input -> {
if (input.equals(Paths.get(""))) {
return false;
}
return MorePaths.getParentOrEmpty(input).equals(pathRelativeToProjectRoot);
})
.toSortedList(Comparator.naturalOrder());
}
@Override
public ImmutableSortedSet<Path> getMtimeSortedMatchingDirectoryContents(
final Path pathRelativeToProjectRoot, String globPattern) throws IOException {
Preconditions.checkState(isDirectory(pathRelativeToProjectRoot));
final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + globPattern);
return fileContents
.keySet()
.stream()
.filter(
i ->
i.getParent().equals(pathRelativeToProjectRoot)
&& pathMatcher.matches(i.getFileName()))
// Sort them in reverse order.
.sorted(
(f0, f1) -> {
try {
return getLastModifiedTimeFetcher()
.getLastModifiedTime(f1)
.compareTo(getLastModifiedTimeFetcher().getLastModifiedTime(f0));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.collect(MoreCollectors.toImmutableSortedSet());
}
@Override
public void walkFileTree(Path searchRoot, FileVisitor<Path> fileVisitor) throws IOException {
walkRelativeFileTree(searchRoot, fileVisitor);
}
@Override
public FileTime getLastModifiedTime(Path path) throws IOException {
Path normalizedPath = MorePaths.normalize(path);
if (!exists(normalizedPath)) {
throw new NoSuchFileException(path.toString());
}
return Preconditions.checkNotNull(fileLastModifiedTimes.get(normalizedPath));
}
@Override
public Path setLastModifiedTime(Path path, FileTime time) throws IOException {
Path normalizedPath = MorePaths.normalize(path);
if (!exists(normalizedPath)) {
throw new NoSuchFileException(path.toString());
}
fileLastModifiedTimes.put(normalizedPath, time);
return normalizedPath;
}
@Override
public void deleteRecursivelyIfExists(Path path) throws IOException {
Path normalizedPath = MorePaths.normalize(path);
for (Iterator<Path> iterator = fileContents.keySet().iterator(); iterator.hasNext(); ) {
Path subPath = iterator.next();
if (subPath.startsWith(normalizedPath)) {
fileAttributes.remove(MorePaths.normalize(subPath));
fileLastModifiedTimes.remove(MorePaths.normalize(subPath));
iterator.remove();
}
}
for (Iterator<Path> iterator = symLinks.keySet().iterator(); iterator.hasNext(); ) {
Path subPath = iterator.next();
if (subPath.startsWith(normalizedPath)) {
iterator.remove();
}
}
fileLastModifiedTimes.remove(path);
directories.remove(path);
}
@Override
public void mkdirs(Path path) throws IOException {
for (Path parent = path; parent != null; parent = parent.getParent()) {
directories.add(parent);
fileLastModifiedTimes.put(parent, FileTime.fromMillis(clock.currentTimeMillis()));
}
}
@Override
public Path createNewFile(Path path) {
writeBytesToPath(new byte[0], path);
return path;
}
@Override
public void writeLinesToPath(Iterable<String> lines, Path path, FileAttribute<?>... attrs) {
StringBuilder builder = new StringBuilder();
if (!Iterables.isEmpty(lines)) {
Joiner.on('\n').appendTo(builder, lines);
builder.append('\n');
}
writeContentsToPath(builder.toString(), path, attrs);
}
@Override
public void writeContentsToPath(String contents, Path path, FileAttribute<?>... attrs) {
writeBytesToPath(contents.getBytes(Charsets.UTF_8), path, attrs);
}
@Override
public void writeBytesToPath(byte[] bytes, Path path, FileAttribute<?>... attrs) {
Path normalizedPath = MorePaths.normalize(path);
fileContents.put(normalizedPath, Preconditions.checkNotNull(bytes));
fileAttributes.put(normalizedPath, ImmutableSet.copyOf(attrs));
Path directory = normalizedPath.getParent();
while (directory != null) {
directories.add(directory);
directory = directory.getParent();
}
fileLastModifiedTimes.put(normalizedPath, FileTime.fromMillis(clock.currentTimeMillis()));
}
@Override
public OutputStream newFileOutputStream(
final Path pathRelativeToProjectRoot, final FileAttribute<?>... attrs) throws IOException {
return new ByteArrayOutputStream() {
@Override
public void close() throws IOException {
super.close();
writeToMap();
}
@Override
public void flush() throws IOException {
super.flush();
writeToMap();
}
private void writeToMap() throws IOException {
writeBytesToPath(toByteArray(), pathRelativeToProjectRoot, attrs);
}
};
}
/** Does not support symlinks. */
@Override
public InputStream newFileInputStream(Path pathRelativeToProjectRoot) throws IOException {
byte[] contents = fileContents.get(normalizePathToProjectRoot(pathRelativeToProjectRoot));
return new ByteArrayInputStream(contents);
}
private Path normalizePathToProjectRoot(Path pathRelativeToProjectRoot)
throws NoSuchFileException {
if (!exists(pathRelativeToProjectRoot)) {
throw new NoSuchFileException(pathRelativeToProjectRoot.toString());
}
return MorePaths.normalize(pathRelativeToProjectRoot);
}
@Override
public void copyToPath(final InputStream inputStream, Path path, CopyOption... options)
throws IOException {
writeBytesToPath(ByteStreams.toByteArray(inputStream), path);
}
/** Does not support symlinks. */
@Override
public Optional<String> readFileIfItExists(Path path) {
if (!exists(path)) {
return Optional.empty();
}
return Optional.of(new String(getFileBytes(path), Charsets.UTF_8));
}
/** Does not support symlinks. */
@Override
public Optional<String> readFirstLine(Path path) {
List<String> lines;
try {
lines = readLines(path);
} catch (IOException e) {
return Optional.empty();
}
return Optional.ofNullable(Iterables.get(lines, 0, null));
}
/** Does not support symlinks. */
@Override
public List<String> readLines(Path path) throws IOException {
Optional<String> contents = readFileIfItExists(path);
if (!contents.isPresent() || contents.get().isEmpty()) {
return ImmutableList.of();
}
String content = contents.get();
content = content.endsWith("\n") ? content.substring(0, content.length() - 1) : content;
return Splitter.on('\n').splitToList(content);
}
@Override
public Manifest getJarManifest(Path path) throws IOException {
try (JarInputStream jar = new JarInputStream(newFileInputStream(path))) {
Manifest result = jar.getManifest();
if (result != null) {
return result;
}
// JarInputStream will only find the manifest if it's the first entry, but we have code that
// puts it elsewhere. We must search. Fortunately, this is test code! So we can be slow!
JarEntry entry;
while ((entry = jar.getNextJarEntry()) != null) {
if (JarFile.MANIFEST_NAME.equals(entry.getName())) {
result = new Manifest();
result.read(jar);
return result;
}
}
}
return null;
}
/** Does not support symlinks. */
@Override
public Sha1HashCode computeSha1(Path pathRelativeToProjectRootOrJustAbsolute) throws IOException {
if (!exists(pathRelativeToProjectRootOrJustAbsolute)) {
throw new NoSuchFileException(pathRelativeToProjectRootOrJustAbsolute.toString());
}
// Because this class is a fake, the file contents may not be available as a stream, so we load
// all of the contents into memory as a byte[] and then hash them.
byte[] fileContents = getFileBytes(pathRelativeToProjectRootOrJustAbsolute);
HashCode hashCode = Hashing.sha1().newHasher().putBytes(fileContents).hash();
return Sha1HashCode.fromHashCode(hashCode);
}
@Override
public void copy(Path source, Path target, CopySourceMode sourceMode) throws IOException {
Path normalizedSourcePath = MorePaths.normalize(source);
Path normalizedTargetPath = MorePaths.normalize(target);
switch (sourceMode) {
case FILE:
ImmutableSet<FileAttribute<?>> attrs = fileAttributes.get(normalizedSourcePath);
writeBytesToPath(
fileContents.get(normalizedSourcePath),
normalizedTargetPath,
attrs.toArray(new FileAttribute[attrs.size()]));
break;
case DIRECTORY_CONTENTS_ONLY:
case DIRECTORY_AND_CONTENTS:
throw new UnsupportedOperationException();
}
}
/**
* TODO(natthu): (1) Also traverse the directories. (2) Do not ignore return value of {@code
* fileVisitor}.
*/
@Override
public final void walkRelativeFileTree(
Path path, EnumSet<FileVisitOption> visitOptions, FileVisitor<Path> fileVisitor)
throws IOException {
if (!isDirectory(path)) {
fileVisitor.visitFile(path, DEFAULT_FILE_ATTRIBUTES);
return;
}
ImmutableCollection<Path> ents = getDirectoryContents(path);
for (Path ent : ents) {
if (!isDirectory(ent)) {
FileVisitResult result = fileVisitor.visitFile(ent, DEFAULT_FILE_ATTRIBUTES);
if (result == FileVisitResult.SKIP_SIBLINGS) {
return;
}
} else {
FileVisitResult result = fileVisitor.preVisitDirectory(ent, DEFAULT_DIR_ATTRIBUTES);
if (result == FileVisitResult.SKIP_SIBLINGS) {
return;
}
if (result != FileVisitResult.SKIP_SUBTREE) {
walkRelativeFileTree(ent, fileVisitor);
fileVisitor.postVisitDirectory(ent, null);
}
}
}
}
@Override
public void copyFolder(Path source, Path target) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void copyFile(Path source, Path target) throws IOException {
writeContentsToPath(readFileIfItExists(source).get(), target);
}
@Override
public void createSymLink(Path symLink, Path realFile, boolean force) throws IOException {
if (!force) {
if (fileContents.containsKey(symLink) || directories.contains(symLink)) {
throw new FileAlreadyExistsException(symLink.toString());
}
} else {
rmFile(symLink);
deleteRecursivelyIfExists(symLink);
}
symLinks.put(symLink, realFile);
}
@Override
public Set<PosixFilePermission> getPosixFilePermissions(Path path) throws IOException {
return ImmutableSet.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.OTHERS_READ);
}
@Override
public boolean isSymLink(Path path) {
return symLinks.containsKey(path);
}
@Override
public Path readSymLink(Path path) throws IOException {
Path target = symLinks.get(path);
if (target == null) {
throw new NotLinkException(path.toString());
}
return target;
}
@Override
public void touch(Path fileToTouch) throws IOException {
if (exists(fileToTouch)) {
setLastModifiedTime(fileToTouch, FileTime.fromMillis(clock.currentTimeMillis()));
} else {
createNewFile(fileToTouch);
}
}
@Override
public Path createTempFile(
Path directory, String prefix, String suffix, FileAttribute<?>... attrs) throws IOException {
Path path;
do {
String str = new BigInteger(130, RANDOM).toString(32);
path = directory.resolve(prefix + str + suffix);
} while (exists(path));
touch(path);
return path;
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
fileContents.put(MorePaths.normalize(target), fileContents.remove(MorePaths.normalize(source)));
fileAttributes.put(
MorePaths.normalize(target), fileAttributes.remove(MorePaths.normalize(source)));
fileLastModifiedTimes.put(
MorePaths.normalize(target), fileLastModifiedTimes.remove(MorePaths.normalize(source)));
}
}