Skip to content

Commit

Permalink
tachyon-fuse: initial unit testing:
Browse files Browse the repository at this point in the history
- small refactoring to facilitate tests
- initial isolation unit tests for TachyonFuseFs
  • Loading branch information
Andrea Reale committed Dec 8, 2015
1 parent 154ca84 commit 3de9ece
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 11 deletions.
5 changes: 5 additions & 0 deletions fuse/pom.xml
Expand Up @@ -49,6 +49,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
5 changes: 4 additions & 1 deletion fuse/src/main/java/tachyon/fuse/TachyonFuse.java
Expand Up @@ -31,6 +31,8 @@

import tachyon.Constants;
import tachyon.client.ClientContext;
import tachyon.client.file.TachyonFileSystem;
import tachyon.client.file.TachyonFileSystem.TachyonFileSystemFactory;
import tachyon.conf.TachyonConf;

/**
Expand All @@ -47,7 +49,8 @@ public static void main(String[] args) {
System.exit(1);
}

final TachyonFuseFs fs = new TachyonFuseFs(sTachyonConf, opts);
final TachyonFileSystem tfs = TachyonFileSystemFactory.get();
final TachyonFuseFs fs = new TachyonFuseFs(sTachyonConf, tfs, opts);
final List<String> fuseOpts = opts.getFuseOpts();
// Force direct_io in FUSE: writes and reads bypass the kernel page
// cache and go directly to tachyon. This avoids extra memory copies
Expand Down
24 changes: 14 additions & 10 deletions fuse/src/main/java/tachyon/fuse/TachyonFuseFs.java
Expand Up @@ -33,7 +33,6 @@
import tachyon.TachyonURI;
import tachyon.client.file.TachyonFile;
import tachyon.client.file.TachyonFileSystem;
import tachyon.client.file.TachyonFileSystem.TachyonFileSystemFactory;
import tachyon.conf.TachyonConf;
import tachyon.exception.FileAlreadyExistsException;
import tachyon.exception.FileDoesNotExistException;
Expand Down Expand Up @@ -67,7 +66,6 @@ final class TachyonFuseFs extends FuseStubFS {

private final TachyonConf mTachyonConf;
private final TachyonFileSystem mTFS;
private final Path mMountPoint;
// base path within Tachyon namespace that is used for FUSE operations
// For example, if tachyon-fuse is mounted in /mnt/tachyon and mTachyonRootPath
// is /users/foo, then an operation on /mnt/tachyon/bar will be translated on
Expand All @@ -81,11 +79,10 @@ final class TachyonFuseFs extends FuseStubFS {
private final Map<Long, OpenFileEntry> mOpenFiles;
private long mNextOpenFileId;

TachyonFuseFs(TachyonConf conf, TachyonFuseOptions opts) {
TachyonFuseFs(TachyonConf conf, TachyonFileSystem tfs, TachyonFuseOptions opts) {
super();
mTachyonConf = conf;
mTFS = TachyonFileSystemFactory.get();
mMountPoint = Paths.get(opts.getMountPoint());
mTFS = tfs;
mTachyonMaster = mTachyonConf.get(Constants.MASTER_ADDRESS);
mTachyonRootPath = Paths.get(opts.getTachyonRoot());
mNextOpenFileId = 0L;
Expand All @@ -96,8 +93,6 @@ final class TachyonFuseFs extends FuseStubFS {
.maximumSize(maxCachedPaths)
.build(new PathCacheLoader());

Preconditions.checkArgument(mMountPoint.isAbsolute(),
"mount point should be an absolute path");
Preconditions.checkArgument(mTachyonRootPath.isAbsolute(),
"tachyon root path should be absolute");
}
Expand All @@ -111,10 +106,10 @@ final class TachyonFuseFs extends FuseStubFS {
*/
@Override
public int create(String path, @mode_t long mode, FuseFileInfo fi) {
// mode is ignored in tachyon-fuse
final TachyonURI turi = mPathResolverCache.getUnchecked(path);
final int flags = fi.flags.get();
LOG.trace("create({}, {}) [Tachyon: {}]", path, Integer.toHexString(flags), turi);
// LOG.warn("{}: mode is ignored in tachyon-fuse", path);
final int openFlag = flags & 3;
if (openFlag != O_WRONLY.intValue()) {
OpenFlags flag = OpenFlags.valueOf(openFlag);
Expand Down Expand Up @@ -646,14 +641,23 @@ private int rmInternal(String path, boolean mustBeFile) {
return 0;
}

/**
* Exposed for testing
*/
LoadingCache<String, TachyonURI> getPathResolverCache() {
return mPathResolverCache;
}

/**
* Resolves a FUSE path into a TachyonURI and possibly keeps it int cache
*/
private class PathCacheLoader extends CacheLoader<String, TachyonURI> {
@Override
public TachyonURI load(String fusePath) {
final Path p = Paths.get(fusePath);
final Path tpath = mTachyonRootPath.resolve(p);
// fusePath is guaranteed to always be an absolute path (i.e., starts
// with a fwd slash) - relative to the FUSE mount point
final String relPath = fusePath.substring(1);
final Path tpath = mTachyonRootPath.resolve(relPath);

return new TachyonURI(mTachyonMaster + tpath.toString());
}
Expand Down
246 changes: 246 additions & 0 deletions fuse/src/test/java/tachyon/fuse/TachyonFuseFsTest.java
@@ -0,0 +1,246 @@
/*
* Licensed to the University of California, Berkeley under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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 tachyon.fuse;

import java.util.Collections;
import java.util.List;

import com.google.common.cache.LoadingCache;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import tachyon.Constants;
import tachyon.TachyonURI;
import tachyon.client.file.FileInStream;
import tachyon.client.file.FileOutStream;
import tachyon.client.file.TachyonFile;
import tachyon.client.file.TachyonFileSystem;
import tachyon.conf.TachyonConf;
import tachyon.thrift.FileInfo;

import jnr.ffi.Pointer;
import jnr.ffi.Runtime;
import ru.serce.jnrfuse.ErrorCodes;
import ru.serce.jnrfuse.struct.FuseFileInfo;

import static jnr.constants.platform.OpenFlags.O_RDONLY;
import static jnr.constants.platform.OpenFlags.O_RDWR;
import static jnr.constants.platform.OpenFlags.O_WRONLY;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;

/**
* Isolation tests for TachyonFuseFs.
*/
// TODO(andreareale): this test suit should be completed
public class TachyonFuseFsTest {

private static final String TEST_MASTER_ADDRESS = "tachyon://localhost:19998";
private static final String TEST_ROOT_PATH = "/t/root";
private static final TachyonURI BASE_EXPECTED_URI =
new TachyonURI(TEST_MASTER_ADDRESS + TEST_ROOT_PATH);

private TachyonFuseFs mFuseFs;
private TachyonFileSystem mTFS;
private FuseFileInfo mFileInfo;

@Before
public void setUp() throws Exception {
TachyonConf conf = new TachyonConf();
conf.set(Constants.MASTER_ADDRESS, TEST_MASTER_ADDRESS);
conf.set(Constants.FUSE_PATHCACHE_SIZE, "0");

final List<String> empty = Collections.emptyList();
TachyonFuseOptions opts = new TachyonFuseOptions(
"/doesnt/matter", TEST_ROOT_PATH, false, empty);

mTFS = mock(TachyonFileSystem.class);
mFuseFs = new TachyonFuseFs(conf, mTFS, opts);
mFileInfo = allocateNativeFileInfo();
}

@Test
public void testCreate() throws Exception {
mFileInfo.flags.set(O_WRONLY.intValue());
mFuseFs.create("/foo/bar", 0, mFileInfo);
TachyonURI expectedPath = BASE_EXPECTED_URI.join("/foo/bar");
verify(mTFS).getOutStream(expectedPath);
}

@Test
public void testCreateWrongFlags() throws Exception {
mFileInfo.flags.set(O_RDONLY.intValue());
int ret = mFuseFs.create("/foo/bar", 0, mFileInfo);
verifyZeroInteractions(mTFS);
assertEquals("Expected invalid access", -ErrorCodes.EACCES(), ret);

mFileInfo.flags.set(O_RDWR.intValue());
ret = mFuseFs.create("/foo/bar", 0, mFileInfo);
verifyZeroInteractions(mTFS);
assertEquals("Expected invalid access", -ErrorCodes.EACCES(), ret);
}

@Test
public void testFlush() throws Exception {
FileOutStream fos = mock(FileOutStream.class);
TachyonURI anyURI = any();
when(mTFS.getOutStream(anyURI)).thenReturn(fos);

// open a file
mFileInfo.flags.set(O_WRONLY.intValue());
mFuseFs.create("/foo/bar", 0, mFileInfo);

//then call flush into it
mFuseFs.flush("/foo/bar", mFileInfo);
verify(fos).flush();
}

@Test
public void testMkdir() throws Exception {
mFuseFs.mkdir("/foo/bar", -1);
verify(mTFS).mkdir(BASE_EXPECTED_URI.join("/foo/bar"));
}

@Test
public void testOpen() throws Exception {
// mocks set-up
TachyonURI expectedPath = BASE_EXPECTED_URI.join("/foo/bar");
TachyonFile fake = new TachyonFile(42L);
FileInfo fi = new FileInfo();
fi.isFolder = false;

when(mTFS.openIfExists(expectedPath)).thenReturn(fake);
when(mTFS.getInfo(fake)).thenReturn(fi);
mFileInfo.flags.set(O_RDONLY.intValue());

// actual test
mFuseFs.open("/foo/bar", mFileInfo);
verify(mTFS).openIfExists(expectedPath);
verify(mTFS).getInStream(fake);
}

@Test
public void testOpenWrongFlags() throws Exception {
mFileInfo.flags.set(O_RDWR.intValue());

// actual test
int ret = mFuseFs.open("/foo/bar", mFileInfo);
verifyZeroInteractions(mTFS);
assertEquals("Should return an access error", -ErrorCodes.EACCES(), ret);

mFileInfo.flags.set(O_WRONLY.intValue());
ret = mFuseFs.open("/foo/bar", mFileInfo);
verifyZeroInteractions(mTFS);
assertEquals("Should return an access error", -ErrorCodes.EACCES(), ret);
}

// TODO(andreareale): Mockito cannot mock the final FileInStream class, while
// if I use PowerMock (and PowerMockRunner) the FuseStubFS constructor
// starts to complain. Commenting out, but this should be fixed
@Ignore
@Test
public void testRead() throws Exception {
// mocks set-up
TachyonURI expectedPath = BASE_EXPECTED_URI.join("/foo/bar");
TachyonFile fake = new TachyonFile(42L);
FileInfo fi = new FileInfo();
fi.isFolder = false;

when(mTFS.openIfExists(expectedPath)).thenReturn(fake);
when(mTFS.getInfo(fake)).thenReturn(fi);

FileInStream fakeInStream = mock(FileInStream.class);
when(fakeInStream.read(any(byte[].class),0,4)).then(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
byte[] myDest = (byte[])invocationOnMock.getArguments()[0];
for (byte i = 0; i < 4; i++) {
myDest[i] = i;
}
return 4;
}
});
when(mTFS.getInStream(fake)).thenReturn(fakeInStream);
mFileInfo.flags.set(O_RDONLY.intValue());

// prepare something to read to it
Runtime r = Runtime.getSystemRuntime();
Pointer ptr = r.getMemoryManager().allocateTemporary(4, true);

// actual test
mFuseFs.open("/foo/bar", mFileInfo);

mFuseFs.read("/foo/bar", ptr, 4, 0, mFileInfo);
final byte[] dst = new byte[4];
ptr.get(0, dst, 0, 4);
final byte[] expected = new byte[] {0, 1, 2, 3};

assertArrayEquals("Source and dst data should be equal", expected, dst);

}

@Test
public void testWrite() throws Exception {
FileOutStream fos = mock(FileOutStream.class);
TachyonURI anyURI = any();
when(mTFS.getOutStream(anyURI)).thenReturn(fos);

// open a file
mFileInfo.flags.set(O_WRONLY.intValue());
mFuseFs.create("/foo/bar", 0, mFileInfo);

// prepare something to write into it
Runtime r = Runtime.getSystemRuntime();
Pointer ptr = r.getMemoryManager().allocateTemporary(4, true);
byte[] expected = {42, -128, 1, 3};
ptr.put(0, expected, 0, 4);

mFuseFs.write("/foo/bar", ptr, 4, 0, mFileInfo);

verify(fos).write(expected);
}

@Test
public void testPathTranslation() throws Exception {
final LoadingCache<String, TachyonURI> resolver =
mFuseFs.getPathResolverCache();

TachyonURI expected = new TachyonURI(TEST_MASTER_ADDRESS + TEST_ROOT_PATH);
TachyonURI actual = resolver.apply("/");
Assert.assertEquals("/ should resolve to " + expected, expected, actual);

expected = new TachyonURI(TEST_MASTER_ADDRESS + TEST_ROOT_PATH + "/home/foo");
actual = resolver.apply("/home/foo");
Assert.assertEquals("/home/foo should resolve to " + expected, expected, actual);
}

// Allocate native memory for a FuseFileInfo data struct and return its pointer
private FuseFileInfo allocateNativeFileInfo() {
final Runtime runtime = Runtime.getSystemRuntime();
final Pointer pt = runtime.getMemoryManager().allocateTemporary(36, true);
return FuseFileInfo.of(pt);
}
}

0 comments on commit 3de9ece

Please sign in to comment.