blob: e7dd54a2ed39b5ce462a4a4b5c8bf6761a506159 [file] [log] [blame]
/*
* 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.ProjectFilesystem;
import com.facebook.buck.timing.Clock;
import com.facebook.buck.timing.FakeClock;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.file.CopyOption;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitor;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
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.util.Collection;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Nullable;
// TODO(natthu): Implement methods that throw UnsupportedOperationException.
public class FakeProjectFilesystem extends ProjectFilesystem {
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 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;
public FakeProjectFilesystem() {
this(new FakeClock(0), Paths.get(".").toFile(), ImmutableSet.<Path>of());
}
// We accept a File here since that's what's returned by TemporaryFolder.
public FakeProjectFilesystem(File root) {
this(new FakeClock(0), root, ImmutableSet.<Path>of());
}
public FakeProjectFilesystem(Clock clock) {
this(clock, Paths.get(".").toFile(), ImmutableSet.<Path>of());
}
public FakeProjectFilesystem(Set<Path> files) {
this(new FakeClock(0), Paths.get(".").toFile(), files);
}
public FakeProjectFilesystem(Clock clock, File root, Set<Path> files) {
super(root.toPath());
fileContents = Maps.newHashMap();
for (Path file : files) {
fileContents.put(file, new byte[0]);
}
fileAttributes = Maps.newHashMap();
fileLastModifiedTimes = Maps.newHashMap();
symLinks = Maps.newHashMap();
directories = Sets.newHashSet();
this.clock = Preconditions.checkNotNull(clock);
// Generally, tests don't care whether files exist.
ignoreValidityOfPaths = true;
}
public FakeProjectFilesystem setIgnoreValidityOfPaths(boolean shouldIgnore) {
this.ignoreValidityOfPaths = shouldIgnore;
return this;
}
private byte[] getFileBytes(Path path) {
return Preconditions.checkNotNull(fileContents.get(path.normalize()));
}
private void rmFile(Path path) {
fileContents.remove(path.normalize());
fileAttributes.remove(path.normalize());
fileLastModifiedTimes.remove(path.normalize());
}
public ImmutableSet<FileAttribute<?>> getFileAttributesAtPath(Path path) {
return Preconditions.checkNotNull(fileAttributes.get(path));
}
@Override
public <A extends BasicFileAttributes> A readAttributes(
Path pathRelativeToProjectRoot,
Class<A> type,
LinkOption... options) throws IOException {
// Converting FileAttribute to BasicFileAttributes sub-interfaces is
// really annoying. Let's just not do it.
throw new UnsupportedOperationException();
}
@Override
public boolean exists(Path path) {
return isFile(path) || isDirectory(path);
}
@Override
public long getFileSize(Path path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path.toString());
}
return getFileBytes(path).length;
}
@Override
public boolean deleteFileAtPath(Path path) {
if (exists(path)) {
rmFile(path);
}
return true;
}
@Override
public Properties readPropertiesFile(Path pathToPropertiesFile) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public boolean isFile(Path path) {
return fileContents.containsKey(path.normalize());
}
@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(path.normalize());
}
/**
* Does not support symlinks.
*/
@Override
public ImmutableCollection<Path> getDirectoryContents(final Path pathRelativeToProjectRoot)
throws IOException {
Preconditions.checkState(isDirectory(pathRelativeToProjectRoot));
return FluentIterable.from(fileContents.keySet()).filter(
new Predicate<Path>() {
@Override
public boolean apply(Path input) {
return input.getParent().equals(pathRelativeToProjectRoot);
}
})
.toList();
}
@Override
public ImmutableSortedSet<Path> getSortedMatchingDirectoryContents(
final Path pathRelativeToProjectRoot,
String globPattern)
throws IOException {
Preconditions.checkState(isDirectory(pathRelativeToProjectRoot));
final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + globPattern);
return FluentIterable.from(fileContents.keySet()).filter(
new Predicate<Path>() {
@Override
public boolean apply(Path input) {
return input.getParent().equals(pathRelativeToProjectRoot) &&
pathMatcher.matches(input.getFileName());
}
})
.toSortedSet(Ordering
.natural()
.onResultOf(new Function<Path, FileTime>() {
@Override
public FileTime apply(Path path) {
try {
return getLastModifiedTimeFetcher().getLastModifiedTime(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
})
.compound(Ordering.natural())
.reverse());
}
@Override
public void walkFileTree(Path root, FileVisitor<Path> fileVisitor) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public long getLastModifiedTime(Path path) throws IOException {
Path normalizedPath = path.normalize();
if (!exists(normalizedPath)) {
throw new NoSuchFileException(path.toString());
}
return Preconditions.checkNotNull(fileLastModifiedTimes.get(normalizedPath)).toMillis();
}
@Override
public void rmdir(Path path) throws IOException {
Path normalizedPath = path.normalize();
for (Iterator<Path> iterator = fileContents.keySet().iterator(); iterator.hasNext();) {
Path subPath = iterator.next();
if (subPath.startsWith(normalizedPath)) {
fileAttributes.remove(subPath.normalize());
fileLastModifiedTimes.remove(subPath.normalize());
iterator.remove();
}
}
fileLastModifiedTimes.remove(path);
directories.remove(path);
}
@Override
public void mkdirs(Path path) throws IOException {
for (int i = 0; i < path.getNameCount(); i++) {
Path subpath = path.subpath(0, i + 1);
directories.add(subpath);
fileLastModifiedTimes.put(subpath, FileTime.fromMillis(clock.currentTimeMillis()));
}
}
@Override
public void writeLinesToPath(
Iterable<String> lines,
Path path,
FileAttribute<?>... attrs) throws IOException {
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) throws IOException {
writeBytesToPath(contents.getBytes(Charsets.UTF_8), path, attrs);
}
@Override
public void writeBytesToPath(
byte[] bytes,
Path path,
FileAttribute<?>... attrs) throws IOException {
Path normalizedPath = path.normalize();
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 {
if (!exists(pathRelativeToProjectRoot)) {
throw new NoSuchFileException(pathRelativeToProjectRoot.toString());
}
return new ByteArrayInputStream(fileContents.get(pathRelativeToProjectRoot.normalize()));
}
@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.absent();
}
return Optional.of(new String(getFileBytes(path), Charsets.UTF_8));
}
/**
* Does not support symlinks.
*/
@Override
public Optional<Reader> getReaderIfFileExists(Path path) {
Optional<String> content = readFileIfItExists(path);
if (!content.isPresent()) {
return Optional.absent();
}
return Optional.of((Reader) new StringReader(content.get()));
}
/**
* Does not support symlinks.
*/
@Override
public Optional<String> readFirstLine(Path path) {
List<String> lines;
try {
lines = readLines(path);
} catch (IOException e) {
return Optional.absent();
}
return Optional.fromNullable(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);
}
/**
* Does not support symlinks.
*/
@Override
public String computeSha1(Path path) throws IOException {
if (!exists(path)) {
throw new FileNotFoundException(path.toString());
}
return Hashing.sha1().hashBytes(getFileBytes(path)).toString();
}
/**
* TODO(natthu): (1) Also traverse the directories. (2) Do not ignore return value of
* {@code fileVisitor}.
*/
@Override
public void walkRelativeFileTree(
Path path,
EnumSet<FileVisitOption> visitOptions,
FileVisitor<Path> fileVisitor) throws IOException {
for (Path file : filesUnderPath(path)) {
fileVisitor.visitFile(file, DEFAULT_FILE_ATTRIBUTES);
}
}
public void touch(Path path) throws IOException {
writeContentsToPath("", path);
}
private Collection<Path> filesUnderPath(final Path dirPath) {
return Collections2.filter(fileContents.keySet(), new Predicate<Path>() {
@Override
public boolean apply(Path input) {
return input.startsWith(dirPath);
}
});
}
@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 source, Path target, boolean force) throws IOException {
if (!force) {
if (fileContents.containsKey(source) || directories.contains(source)) {
throw new FileAlreadyExistsException(source.toString());
}
} else {
rmFile(source);
rmdir(source);
}
symLinks.put(source, target);
}
@Override
public boolean isSymLink(Path path) throws IOException {
return symLinks.containsKey(path);
}
@Override
public void createZip(
Collection<Path> pathsToIncludeInZip,
File out,
ImmutableMap<Path, String> additionalFileContents) throws IOException {
throw new UnsupportedOperationException();
}
}