/*
* Copyright 2015 Daniel Gredler
*
* 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 net.gredler.testfs;
import java.io.IOException;
import java.nio.file.AccessMode;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <p>
* Test file system builder. A test file system simply wraps the default file system, allowing the user to
* simulate the existence or non-existence of certain individual files.
*
* <p>
* For example, the following test file system behaves exactly like the default file system, with all reads
* and writes passing through to the default file system:
*
* <pre>{@code
* FileSystem fs = new TestFS().create();
* }</pre>
*
* <p>
* The following test file system also behaves exactly like the default file system, except that the files
* <code>/test/file1.log</code> and <code>/test/file2.log</code> don't appear to exist, even if in reality
* they do exist:
*
* <pre>{@code
* FileSystem fs = new TestFS()
* .removingFiles("/test/file1.log", "/test/file2.log")
* .create();
* }</pre>
*
* <p>
* The following test file system behaves exactly like the default file system, but simulates a read-only
* temporary directory:
*
* <pre>{@code
* String tempDir = System.getProperty("java.io.tmpdir");
* FileSystem fs = new TestFS()
* .alteringPermissions(tempDir, Permissions.R_X)
* .create();
* }</pre>
*
* <p>
* The following test file system behaves exactly like the default file system, except that the files
* <code>/test/file1.log</code> and <code>/test/file2.log</code> exist but aren't writable, regardless of
* whether or not they exist in reality, and regardless of whether or not they are writable in reality.
* If either simulated file is read from, the read will be delegated to the file at
* <code>src/test/resources/my.log</code>.
*
* <pre>{@code
* FileSystem fs = new TestFS()
* .addingFile("/test/file1.log", "src/test/resources/my.log", Permissions.R_X)
* .addingFile("/test/file2.log", "src/test/resources/my.log", Permissions.R_X)
* .create();
* }</pre>
*/
public final class TestFS {
/**
* Paths to files that should not exist in the test file system,
* regardless of whether or not they actually exist.
*/
private final List< String > removedPaths;
/**
* Target paths for files that should exist in the test file system,
* regardless of whether or not they actually exist.
*/
private final Map< String, String > addedPathTargets;
/**
* Permissions for files that should exist in the test file system,
* regardless of whether or not they actually exist.
*/
private final Map< String, Permissions > addedPathPermissions;
/**
* Access to these files using these access modes should trigger
* an {@link IOException}.
*/
private final Map< String, Set< AccessMode > > exceptionPaths;
/**
* Creates a new test file system builder.
*/
public TestFS() {
removedPaths = new ArrayList<>();
addedPathTargets = new HashMap<>();
addedPathPermissions = new HashMap<>();
exceptionPaths = new HashMap<>();
}
/**
* Simulates the existence of the specified file path, regardless of whether or not it actually exists.
*
* @param path the path of the file that should exist in the context of the test file system
* @param targetPath path to an existing file to which reads and other file system operations will be delegated
* @return this test file system builder, ready for further customization
* @throws IllegalArgumentException if the specified target file does not exist
*/
public TestFS addingFile(String path, String targetPath) {
return addingFile(path, targetPath, Permissions.RWX);
}
/**
* Simulates the existence of the specified file path, regardless of whether or not it actually exists.
*
* @param path the path of the file that should exist in the context of the test file system
* @param targetPath path to an existing file to which reads and other file system operations will be delegated
* @param permissions custom read/write/execute permissions to apply to the simulated file
* @return this test file system builder, ready for further customization
* @throws IllegalArgumentException if the specified target file does not exist
*/
public TestFS addingFile(String path, String targetPath, Permissions permissions) {
Path target = FileSystems.getDefault().getPath(targetPath);
if (!Files.exists(target)) {
throw new IllegalArgumentException(targetPath + " must exist, but does not");
}
addedPathTargets.put(path, targetPath);
addedPathPermissions.put(path, permissions);
return this;
}
/**
* Simulates the non-existence of the specified file paths, regardless of whether or not they actually exist.
*
* @param paths the paths of the files that should not exist in the context of the test file system
* @return this test file system builder, ready for further customization
*/
public TestFS removingFiles(String... paths) {
removedPaths.addAll(Arrays.asList(paths));
return this;
}
/**
* Simulates the specified permissions on the specified file.
*
* @param path the path of the file whose permissions are to be simulated
* @param permissions the simulated permissions to use for the specified file
* @return this test file system builder, ready for further customization
* @throws IllegalArgumentException if the specified file does not exist
*/
public TestFS alteringPermissions(String path, Permissions permissions) {
addingFile(path, path, permissions);
return this;
}
/**
* Simulates an {@link IOException} whenever the contents of the specified files are read.
*
* @param paths the paths of the files which are to trigger an {@link IOException} on read
* @return this test file system builder, ready for further customization
*/
public TestFS throwingExceptionOnRead(String... paths) {
return throwingException(paths, AccessMode.READ);
}
/**
* Simulates an {@link IOException} whenever the specified files are written to.
*
* @param paths the paths of the files which are to trigger an {@link IOException} on write
* @return this test file system builder, ready for further customization
*/
public TestFS throwingExceptionOnWrite(String... paths) {
return throwingException(paths, AccessMode.WRITE);
}
/**
* Simulates an {@link IOException} whenever the specified files are read from or written to.
*
* @param paths the paths of the files which are to trigger an {@link IOException} on read or write
* @param mode the access mode that should trigger the {@link IOException} (read or write)
* @return this test file system builder, ready for further customization
*/
private TestFS throwingException(String[] paths, AccessMode mode) {
for (String path : paths) {
Set< AccessMode > modes = exceptionPaths.get(path);
if (modes == null) {
modes = new HashSet<>(3);
exceptionPaths.put(path, modes);
}
modes.add(mode);
}
return this;
}
/**
* Creates and returns the test file system.
*
* @return the test file system
*/
public FileSystem create() {
FileSystem fs = FileSystems.getDefault();
TestFileSystemProvider p = new TestFileSystemProvider(fs, removedPaths,
addedPathTargets, addedPathPermissions, exceptionPaths);
return new TestFileSystem(fs, p);
}
/**
* Alias for {@link #create()}; particularly useful to enthusiastic Southerners.
*
* @return the test file system
*/
public FileSystem gitErDone() {
return create();
}
}