You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by rm...@apache.org on 2014/11/25 16:21:19 UTC

svn commit: r1641632 [1/2] - in /lucene/dev/trunk/lucene: core/src/test/org/apache/lucene/mockfile/ test-framework/ test-framework/src/java/org/apache/lucene/mockfile/ test-framework/src/java/org/apache/lucene/util/

Author: rmuir
Date: Tue Nov 25 15:21:18 2014
New Revision: 1641632

URL: http://svn.apache.org/r1641632
Log:
LUCENE-6072: use mockfilesystem in tests

Added:
    lucene/dev/trunk/lucene/core/src/test/org/apache/lucene/mockfile/
    lucene/dev/trunk/lucene/core/src/test/org/apache/lucene/mockfile/TestMockFilesystems.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/DisableFsyncFS.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterAsynchronousFileChannel.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterDirectoryStream.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileChannel.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileStore.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystem.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystemProvider.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterInputStream2.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterOutputStream2.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterPath.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSecureDirectoryStream.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSeekableByteChannel.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/HandleTrackingFS.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/LeakFS.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/VerboseFS.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/WindowsFS.java   (with props)
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/package.html   (with props)
Modified:
    lucene/dev/trunk/lucene/test-framework/build.xml
    lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/util/TestRuleTemporaryFilesCleanup.java

Added: lucene/dev/trunk/lucene/core/src/test/org/apache/lucene/mockfile/TestMockFilesystems.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/core/src/test/org/apache/lucene/mockfile/TestMockFilesystems.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/core/src/test/org/apache/lucene/mockfile/TestMockFilesystems.java (added)
+++ lucene/dev/trunk/lucene/core/src/test/org/apache/lucene/mockfile/TestMockFilesystems.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,205 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.lucene.util.InfoStream;
+import org.apache.lucene.util.LuceneTestCase;
+
+public class TestMockFilesystems extends LuceneTestCase {
+  
+  public void testLeakInputStream() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new LeakFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("stillopen"));
+    file.write(5);
+    file.close();
+    InputStream leak = Files.newInputStream(wrapped.resolve("stillopen"));
+    try {
+      fs.close();
+      fail("should have gotten exception");
+    } catch (Exception e) {
+      assertTrue(e.getMessage().contains("file handle leaks"));
+    }
+    leak.close();
+  }
+  
+  public void testLeakOutputStream() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new LeakFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream leak = Files.newOutputStream(wrapped.resolve("leaky"));
+    try {
+      fs.close();
+      fail("should have gotten exception");
+    } catch (Exception e) {
+      assertTrue(e.getMessage().contains("file handle leaks"));
+    }
+    leak.close();
+  }
+  
+  public void testLeakFileChannel() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new LeakFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("stillopen"));
+    file.write(5);
+    file.close();
+    FileChannel leak = FileChannel.open(wrapped.resolve("stillopen"));
+    try {
+      fs.close();
+      fail("should have gotten exception");
+    } catch (Exception e) {
+      assertTrue(e.getMessage().contains("file handle leaks"));
+    }
+    leak.close();
+  }
+  
+  public void testLeakAsyncFileChannel() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new LeakFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("stillopen"));
+    file.write(5);
+    file.close();
+    AsynchronousFileChannel leak = AsynchronousFileChannel.open(wrapped.resolve("stillopen"));
+    try {
+      fs.close();
+      fail("should have gotten exception");
+    } catch (Exception e) {
+      assertTrue(e.getMessage().contains("file handle leaks"));
+    }
+    leak.close();
+  }
+  
+  public void testLeakByteChannel() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new LeakFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("stillopen"));
+    file.write(5);
+    file.close();
+    SeekableByteChannel leak = Files.newByteChannel(wrapped.resolve("stillopen"));
+    try {
+      fs.close();
+      fail("should have gotten exception");
+    } catch (Exception e) {
+      assertTrue(e.getMessage().contains("file handle leaks"));
+    }
+    leak.close();
+  }
+ 
+  public void testDeleteOpenFile() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new WindowsFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("stillopen"));
+    file.write(5);
+    file.close();
+    InputStream is = Files.newInputStream(wrapped.resolve("stillopen"));
+    try {
+      Files.delete(wrapped.resolve("stillopen"));
+      fail("should have gotten exception");
+    } catch (IOException e) {
+      assertTrue(e.getMessage().contains("access denied"));
+    }
+    is.close();
+  }
+  
+  public void testDeleteIfExistsOpenFile() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new WindowsFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("stillopen"));
+    file.write(5);
+    file.close();
+    InputStream is = Files.newInputStream(wrapped.resolve("stillopen"));
+    try {
+      Files.deleteIfExists(wrapped.resolve("stillopen"));
+      fail("should have gotten exception");
+    } catch (IOException e) {
+      assertTrue(e.getMessage().contains("access denied"));
+    }
+    is.close();
+  }
+  
+  public void testRenameOpenFile() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    FileSystem fs = new WindowsFS(dir.getFileSystem()).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("stillopen"));
+    file.write(5);
+    file.close();
+    InputStream is = Files.newInputStream(wrapped.resolve("stillopen"));
+    try {
+      Files.move(wrapped.resolve("stillopen"), wrapped.resolve("target"), StandardCopyOption.ATOMIC_MOVE);
+      fail("should have gotten exception");
+    } catch (IOException e) {
+      assertTrue(e.getMessage().contains("access denied"));
+    }
+    is.close();
+  }
+  
+  public void testVerboseWrite() throws IOException {
+    Path dir = FilterPath.unwrap(createTempDir());
+    final AtomicBoolean seenMessage = new AtomicBoolean(false);
+    InfoStream testStream = new InfoStream() {
+      @Override
+      public void close() throws IOException {}
+
+      @Override
+      public void message(String component, String message) {
+        if ("FS".equals(component) && message.startsWith("newOutputStream")) {
+          seenMessage.set(true);
+        }
+      }
+
+      @Override
+      public boolean isEnabled(String component) {
+        return true;
+      }
+    };
+    FileSystem fs = new VerboseFS(dir.getFileSystem(), testStream).getFileSystem(URI.create("file:///"));
+    Path wrapped = new FilterPath(dir, fs);
+    
+    OutputStream file = Files.newOutputStream(wrapped.resolve("output"));
+    assertTrue(seenMessage.get());
+    file.close();
+  }
+}

Modified: lucene/dev/trunk/lucene/test-framework/build.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/build.xml?rev=1641632&r1=1641631&r2=1641632&view=diff
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/build.xml (original)
+++ lucene/dev/trunk/lucene/test-framework/build.xml Tue Nov 25 15:21:18 2014
@@ -22,6 +22,11 @@
 
   <property name="build.dir" location="../build/test-framework"/>
 
+  <!-- file is part of the API -->
+  <property name="forbidden-base-excludes" value="
+    org/apache/lucene/mockfile/FilterPath.class
+  "/>
+
   <import file="../common-build.xml"/>
 
   <path id="classpath">

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/DisableFsyncFS.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/DisableFsyncFS.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/DisableFsyncFS.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/DisableFsyncFS.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,59 @@
+package org.apache.lucene.mockfile;
+
+import java.io.IOException;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.file.FileSystem;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileAttribute;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+/** 
+ * Disables actual calls to fsync.
+ * <p>
+ * All other filesystem operations are passed thru as normal.
+ */
+public class DisableFsyncFS extends FilterFileSystemProvider {
+  
+  /** 
+   * Create a new instance, wrapping {@code delegate}.
+   */
+  public DisableFsyncFS(FileSystem delegate) {
+    super("disablefsync://", delegate);
+  }
+
+  @Override
+  public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+    return new FilterFileChannel(super.newFileChannel(path, options, attrs)) {
+      @Override
+      public void force(boolean metaData) throws IOException {}
+    };
+  }
+
+  @Override
+  public AsynchronousFileChannel newAsynchronousFileChannel(Path path, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) throws IOException {
+    return new FilterAsynchronousFileChannel(super.newAsynchronousFileChannel(path, options, executor, attrs)) {
+      @Override
+      public void force(boolean metaData) throws IOException {}
+    };
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterAsynchronousFileChannel.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterAsynchronousFileChannel.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterAsynchronousFileChannel.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterAsynchronousFileChannel.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,112 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.CompletionHandler;
+import java.nio.channels.FileLock;
+import java.util.Objects;
+import java.util.concurrent.Future;
+
+/**  
+ * A {@code FilterAsynchronousFileChannel} contains another 
+ * {@code AsynchronousFileChannel}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterAsynchronousFileChannel extends AsynchronousFileChannel {
+  
+  /** 
+   * The underlying {@code AsynchronousFileChannel} instance. 
+   */
+  protected final AsynchronousFileChannel delegate;
+  
+  /**
+   * Construct a {@code FilterAsynchronousFileChannel} based on 
+   * the specified base channel.
+   * <p>
+   * Note that base channel is closed if this channel is closed.
+   * @param delegate specified base channel.
+   */
+  public FilterAsynchronousFileChannel(AsynchronousFileChannel delegate) {
+    this.delegate = Objects.requireNonNull(delegate);
+  }
+  
+  @Override
+  public void close() throws IOException {
+    delegate.close();
+  }
+
+  @Override
+  public boolean isOpen() {
+    return delegate.isOpen();
+  }
+
+  @Override
+  public long size() throws IOException {
+    return delegate.size();
+  }
+
+  @Override
+  public AsynchronousFileChannel truncate(long size) throws IOException {
+    delegate.truncate(size);
+    return this;
+  }
+
+  @Override
+  public void force(boolean metaData) throws IOException {
+    delegate.force(metaData);
+  }
+
+  @Override
+  public <A> void lock(long position, long size, boolean shared, A attachment, CompletionHandler<FileLock,? super A> handler) {
+    delegate.lock(position, size, shared, attachment, handler);
+  }
+
+  @Override
+  public Future<FileLock> lock(long position, long size, boolean shared) {
+    return delegate.lock(position, size, shared);
+  }
+
+  @Override
+  public FileLock tryLock(long position, long size, boolean shared) throws IOException {
+    return delegate.tryLock(position, size, shared);
+  }
+
+  @Override
+  public <A> void read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer,? super A> handler) {
+    delegate.read(dst, position, attachment, handler);
+  }
+
+  @Override
+  public Future<Integer> read(ByteBuffer dst, long position) {
+    return delegate.read(dst, position);
+  }
+
+  @Override
+  public <A> void write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer,? super A> handler) {
+    delegate.write(src, position, attachment, handler);
+  }
+
+  @Override
+  public Future<Integer> write(ByteBuffer src, long position) {
+    return delegate.write(src, position);
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterDirectoryStream.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterDirectoryStream.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterDirectoryStream.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterDirectoryStream.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,58 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**  
+ * A {@code FilterDirectoryStream} contains another 
+ * {@code DirectoryStream}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterDirectoryStream<T> implements DirectoryStream<T> {
+  
+  /** 
+   * The underlying {@code DirectoryStream} instance. 
+   */
+  protected final DirectoryStream<T> delegate;
+  
+  /**
+   * Construct a {@code FilterDirectoryStream} based on 
+   * the specified base stream.
+   * <p>
+   * Note that base stream is closed if this stream is closed.
+   * @param delegate specified base stream.
+   */
+  public FilterDirectoryStream(DirectoryStream<T> delegate) {
+    this.delegate = Objects.requireNonNull(delegate);
+  }
+
+  @Override
+  public void close() throws IOException {
+    delegate.close();
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    return delegate.iterator();
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileChannel.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileChannel.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileChannel.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileChannel.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,157 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOError;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.Objects;
+
+/**  
+ * A {@code FilterFileChannel} contains another 
+ * {@code FileChannel}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterFileChannel extends FileChannel {
+  
+  /** 
+   * The underlying {@code FileChannel} instance. 
+   */
+  protected final FileChannel delegate;
+  
+  /**
+   * Construct a {@code FilterFileChannel} based on 
+   * the specified base channel.
+   * <p>
+   * Note that base channel is closed if this channel is closed.
+   * @param delegate specified base channel.
+   */
+  public FilterFileChannel(FileChannel delegate) {
+    this.delegate = Objects.requireNonNull(delegate);
+  }
+
+  @Override
+  public int read(ByteBuffer dst) throws IOException {
+    return delegate.read(dst);
+  }
+
+  @Override
+  public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
+    return delegate.read(dsts, offset, length);
+  }
+
+  @Override
+  public int write(ByteBuffer src) throws IOException {
+    return delegate.write(src);
+  }
+
+  @Override
+  public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
+    return delegate.write(srcs, offset, length);
+  }
+
+  @Override
+  public long position() throws IOException {
+    return delegate.position();
+  }
+
+  @Override
+  public FileChannel position(long newPosition) throws IOException {
+    delegate.position(newPosition);
+    return this;
+  }
+
+  @Override
+  public long size() throws IOException {
+    return delegate.size();
+  }
+
+  @Override
+  public FileChannel truncate(long size) throws IOException {
+    delegate.truncate(size);
+    return this;
+  }
+
+  @Override
+  public void force(boolean metaData) throws IOException {
+    delegate.force(metaData);
+  }
+
+  @Override
+  public long transferTo(long position, long count, WritableByteChannel target) throws IOException {
+    return delegate.transferTo(position, count, target);
+  }
+
+  @Override
+  public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException {
+    return delegate.transferFrom(src, position, count);
+  }
+
+  @Override
+  public int read(ByteBuffer dst, long position) throws IOException {
+    return delegate.read(dst, position);
+  }
+
+  @Override
+  public int write(ByteBuffer src, long position) throws IOException {
+    return delegate.write(src, position);
+  }
+
+  @Override
+  public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
+    return delegate.map(mode, position, size);
+  }
+
+  @Override
+  public FileLock lock(long position, long size, boolean shared) throws IOException {
+    return delegate.lock(position, size, shared);
+  }
+
+  @Override
+  public FileLock tryLock(long position, long size, boolean shared) throws IOException {
+    return delegate.tryLock(position, size, shared);
+  }
+
+  @Override
+  protected void implCloseChannel() throws IOException {
+    // our only way to call delegate.implCloseChannel()
+    for (Class<?> clazz = delegate.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+      final Method method;
+      try {
+        method = clazz.getDeclaredMethod("implCloseChannel");
+      } catch (NoSuchMethodException e) {
+        continue;
+      }
+      try {
+        method.setAccessible(true);
+        method.invoke(delegate);
+        return;
+      } catch (ReflectiveOperationException e) {
+        throw new IOError(e);
+      }
+    }
+    throw new AssertionError();
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileStore.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileStore.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileStore.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileStore.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,109 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileStoreAttributeView;
+import java.util.Objects;
+
+/**  
+ * A {@code FilterFileStore} contains another 
+ * {@code FileStore}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterFileStore extends FileStore {
+  
+  /** 
+   * The underlying {@code FileStore} instance. 
+   */
+  protected final FileStore delegate;
+  
+  /**
+   * URI scheme used for this instance.
+   */
+  protected final String scheme;
+  
+  /**
+   * Construct a {@code FilterFileStore} based on 
+   * the specified base store.
+   * @param delegate specified base store.
+   * @param scheme URI scheme identifying this instance.
+   */
+  public FilterFileStore(FileStore delegate, String scheme) {
+    this.delegate = Objects.requireNonNull(delegate);
+    this.scheme = Objects.requireNonNull(scheme);
+  }
+
+  @Override
+  public String name() {
+    return delegate.name();
+  }
+
+  @Override
+  public String type() {
+    return scheme + "(" + delegate.type() + ")";
+  }
+
+  @Override
+  public String toString() {
+    return scheme + "(" + delegate.toString() + ")";
+  }
+
+  @Override
+  public boolean isReadOnly() {
+    return delegate.isReadOnly();
+  }
+
+  @Override
+  public long getTotalSpace() throws IOException {
+    return delegate.getTotalSpace();
+  }
+
+  @Override
+  public long getUsableSpace() throws IOException {
+    return delegate.getUsableSpace();
+  }
+
+  @Override
+  public long getUnallocatedSpace() throws IOException {
+    return delegate.getUnallocatedSpace();
+  }
+
+  @Override
+  public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
+    return delegate.supportsFileAttributeView(type);
+  }
+
+  @Override
+  public boolean supportsFileAttributeView(String name) {
+    return delegate.supportsFileAttributeView(name);
+  }
+
+  @Override
+  public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
+    return delegate.getFileStoreAttributeView(type);
+  }
+
+  @Override
+  public Object getAttribute(String attribute) throws IOException {
+    return delegate.getAttribute(attribute);
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystem.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystem.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystem.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystem.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,165 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Set;
+
+/**  
+ * A {@code FilterFileSystem} contains another 
+ * {@code FileSystem}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterFileSystem extends FileSystem {
+  
+  /**
+   * FileSystemProvider that created this FilterFileSystem
+   */
+  protected final FilterFileSystemProvider parent;
+  
+  /** 
+   * The underlying {@code FileSystem} instance. 
+   */
+  protected final FileSystem delegate;
+  
+  /**
+   * Construct a {@code FilterFileSystem} based on 
+   * the specified base filesystem.
+   * <p>
+   * Note that base filesystem is closed if this filesystem is closed,
+   * however the default filesystem provider will never be closed, it doesn't
+   * support that.
+   * @param delegate specified base channel.
+   */
+  public FilterFileSystem(FilterFileSystemProvider parent, FileSystem delegate) {
+    this.parent = Objects.requireNonNull(parent);
+    this.delegate = Objects.requireNonNull(delegate);
+  }
+
+  @Override
+  public FileSystemProvider provider() {
+    return parent;
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (delegate == FileSystems.getDefault()) {
+      // you can't close the default provider!
+      parent.onClose();
+    } else {
+      try (FileSystem d = delegate) {
+        parent.onClose();
+      }
+    }
+  }
+
+  @Override
+  public boolean isOpen() {
+    return delegate.isOpen();
+  }
+
+  @Override
+  public boolean isReadOnly() {
+    return delegate.isReadOnly();
+  }
+
+  @Override
+  public String getSeparator() {
+    return delegate.getSeparator();
+  }
+
+  @Override
+  public Iterable<Path> getRootDirectories() {
+    final Iterable<Path> roots = delegate.getRootDirectories();
+    return () -> {
+      final Iterator<Path> iterator = roots.iterator();
+      return new Iterator<Path>() {
+        @Override
+        public boolean hasNext() {
+          return iterator.hasNext();
+        }
+
+        @Override
+        public Path next() {
+          return new FilterPath(iterator.next(), FilterFileSystem.this);
+        }
+      };
+    };
+  }
+
+  @Override
+  public Iterable<FileStore> getFileStores() {
+    final Iterable<FileStore> fileStores = delegate.getFileStores();
+    return () -> {
+      final Iterator<FileStore> iterator = fileStores.iterator();
+      return new Iterator<FileStore>() {
+        @Override
+        public boolean hasNext() {
+          return iterator.hasNext();
+        }
+
+        @Override
+        public FileStore next() {
+          return new FilterFileStore(iterator.next(), parent.getScheme());
+        }
+      };
+    };
+  }
+
+  @Override
+  public Set<String> supportedFileAttributeViews() {
+    return delegate.supportedFileAttributeViews();
+  }
+
+  @Override
+  public Path getPath(String first, String... more) {
+    return new FilterPath(delegate.getPath(first, more), this);
+  }
+
+  @Override
+  public PathMatcher getPathMatcher(String syntaxAndPattern) {
+    final PathMatcher matcher = delegate.getPathMatcher(syntaxAndPattern);
+    return path -> {
+      if (path instanceof FilterPath) {
+        return matcher.matches(((FilterPath)path).delegate);
+      }
+      return false;
+    };
+  }
+
+  @Override
+  public UserPrincipalLookupService getUserPrincipalLookupService() {
+    return delegate.getUserPrincipalLookupService();
+  }
+
+  @Override
+  public WatchService newWatchService() throws IOException {
+    return delegate.newWatchService();
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystemProvider.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystemProvider.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystemProvider.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterFileSystemProvider.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,245 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+/**  
+ * A {@code FilterFileSystemProvider} contains another 
+ * {@code FileSystemProvider}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterFileSystemProvider extends FileSystemProvider {
+  
+  /** 
+   * The underlying {@code FileSystemProvider}. 
+   */
+  protected final FileSystemProvider delegate;
+  /** 
+   * The underlying {@code FileSystem} instance. 
+   */
+  protected final FileSystem fileSystem;
+  /** 
+   * The URI scheme for this provider.
+   */
+  protected final String scheme;
+  
+  /**
+   * Construct a {@code FilterFileSystemProvider} indicated by
+   * the specified {@code scheme} and wrapping functionality of the
+   * provider of the specified base filesystem.
+   * @param scheme URI scheme
+   * @param delegateInstance specified base filesystem.
+   */
+  public FilterFileSystemProvider(String scheme, FileSystem delegateInstance) {
+    this.scheme = Objects.requireNonNull(scheme);
+    Objects.requireNonNull(delegateInstance);
+    this.delegate = delegateInstance.provider();
+    this.fileSystem = new FilterFileSystem(this, delegateInstance);
+  }
+
+  @Override
+  public String getScheme() {
+    return scheme;
+  }
+
+  @Override
+  public FileSystem newFileSystem(URI uri, Map<String,?> env) throws IOException {
+    return fileSystem;
+  }
+  
+  @Override
+  public FileSystem newFileSystem(Path path, Map<String,?> env) throws IOException {
+    return fileSystem;
+  }
+
+  @Override
+  public FileSystem getFileSystem(URI uri) {
+    return fileSystem;
+  }
+
+  @Override
+  public Path getPath(URI uri) {
+    Path path = delegate.getPath(toDelegate(uri));
+    return new FilterPath(path, fileSystem);
+  }
+
+  @Override
+  public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+    delegate.createDirectory(toDelegate(dir), attrs);
+  }
+
+  @Override
+  public void delete(Path path) throws IOException {
+    delegate.delete(toDelegate(path));
+  }
+
+  @Override
+  public void copy(Path source, Path target, CopyOption... options) throws IOException {
+    delegate.copy(toDelegate(source), toDelegate(target), options);
+  }
+
+  @Override
+  public void move(Path source, Path target, CopyOption... options) throws IOException {
+    delegate.move(toDelegate(source), toDelegate(target), options);
+  }
+
+  @Override
+  public boolean isSameFile(Path path, Path path2) throws IOException {
+    return delegate.isSameFile(toDelegate(path), toDelegate(path2));
+  }
+
+  @Override
+  public boolean isHidden(Path path) throws IOException {
+    return delegate.isHidden(toDelegate(path));
+  }
+
+  @Override
+  public FileStore getFileStore(Path path) throws IOException {
+    return delegate.getFileStore(toDelegate(path));
+  }
+
+  @Override
+  public void checkAccess(Path path, AccessMode... modes) throws IOException {
+    delegate.checkAccess(toDelegate(path), modes);
+  }
+
+  @Override
+  public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
+    return delegate.getFileAttributeView(toDelegate(path), type, options);
+  }
+
+  @Override
+  public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
+    return delegate.readAttributes(toDelegate(path), type, options);
+  }
+
+  @Override
+  public Map<String,Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
+    return delegate.readAttributes(toDelegate(path), attributes, options);
+  }
+
+  @Override
+  public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
+    delegate.setAttribute(toDelegate(path), attribute, value, options);
+  }
+
+  @Override
+  public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
+    return delegate.newInputStream(toDelegate(path), options);
+  }
+
+  @Override
+  public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
+    return delegate.newOutputStream(toDelegate(path), options);
+  }
+
+  @Override
+  public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+    return delegate.newFileChannel(toDelegate(path), options, attrs);
+  }
+
+  @Override
+  public AsynchronousFileChannel newAsynchronousFileChannel(Path path, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) throws IOException {
+    return delegate.newAsynchronousFileChannel(toDelegate(path), options, executor, attrs);
+  }
+  
+  @Override
+  public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+    return delegate.newByteChannel(toDelegate(path), options, attrs);
+  }
+
+  @Override
+  public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
+    return delegate.newDirectoryStream(toDelegate(dir), filter);
+  }
+
+  @Override
+  public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
+    delegate.createSymbolicLink(toDelegate(link), toDelegate(target), attrs);
+  }
+
+  @Override
+  public void createLink(Path link, Path existing) throws IOException {
+    delegate.createLink(toDelegate(link), toDelegate(existing));
+  }
+
+  @Override
+  public boolean deleteIfExists(Path path) throws IOException {
+    return delegate.deleteIfExists(toDelegate(path));
+  }
+
+  @Override
+  public Path readSymbolicLink(Path link) throws IOException {
+    return delegate.readSymbolicLink(toDelegate(link));
+  }
+
+  private Path toDelegate(Path path) {
+    if (path instanceof FilterPath) {
+      return ((FilterPath) path).delegate;
+    }
+    return path;
+  }
+  
+  private URI toDelegate(URI uri) {
+    try {
+      return new URI(delegate.getScheme(), uri.getSchemeSpecificPart(), uri.getFragment());
+    } catch (URISyntaxException e) {
+      throw new IOError(e);
+    }
+  }
+  
+  /** 
+   * Override to trigger some behavior when the filesystem is closed.
+   * <p>
+   * This is always called for each FilterFileSystemProvider in the chain.
+   */
+  protected void onClose() {
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(" + delegate + ")";
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterInputStream2.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterInputStream2.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterInputStream2.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterInputStream2.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,100 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+/**  
+ * A {@code FilterInputStream2} contains another 
+ * {@code InputStream}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ * <p>
+ * Note: unlike {@link FilterInputStream} this class
+ * delegates every method by default. This means to transform
+ * {@code read} calls, you need to override multiple methods.
+ * On the other hand, it is less trappy: a simple implementation 
+ * that just overrides {@code close} will not force bytes to be 
+ * read one-at-a-time.
+ */
+public class FilterInputStream2 extends InputStream {
+  
+  /** 
+   * The underlying {@code InputStream} instance. 
+   */
+  protected final InputStream delegate;
+  
+  /**
+   * Construct a {@code FilterInputStream2} based on 
+   * the specified base stream.
+   * <p>
+   * Note that base stream is closed if this stream is closed.
+   * @param delegate specified base stream.
+   */
+  public FilterInputStream2(InputStream delegate) {
+    this.delegate = Objects.requireNonNull(delegate);
+  }
+
+  @Override
+  public int read() throws IOException {
+    return delegate.read();
+  }
+
+  @Override
+  public int read(byte[] b) throws IOException {
+    return delegate.read(b);
+  }
+
+  @Override
+  public int read(byte[] b, int off, int len) throws IOException {
+    return delegate.read(b, off, len);
+  }
+
+  @Override
+  public long skip(long n) throws IOException {
+    return delegate.skip(n);
+  }
+
+  @Override
+  public int available() throws IOException {
+    return delegate.available();
+  }
+
+  @Override
+  public void close() throws IOException {
+    delegate.close();
+  }
+
+  @Override
+  public synchronized void mark(int readlimit) {
+    delegate.mark(readlimit);
+  }
+
+  @Override
+  public synchronized void reset() throws IOException {
+    delegate.reset();
+  }
+
+  @Override
+  public boolean markSupported() {
+    return delegate.markSupported();
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterOutputStream2.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterOutputStream2.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterOutputStream2.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterOutputStream2.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,80 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Objects;
+
+/**  
+ * A {@code FilterOutputStream2} contains another 
+ * {@code OutputStream}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ * <p>
+ * Note: unlike {@link FilterOutputStream} this class
+ * delegates every method by default. This means to transform
+ * {@code write} calls, you need to override multiple methods.
+ * On the other hand, it is less trappy: a simple implementation 
+ * that just overrides {@code close} will not force bytes to be 
+ * written one-at-a-time.
+ */
+public class FilterOutputStream2 extends OutputStream {
+  
+  /** 
+   * The underlying {@code OutputStream} instance. 
+   */
+  protected final OutputStream delegate;
+  
+  /**
+   * Construct a {@code FilterOutputStream2} based on 
+   * the specified base stream.
+   * <p>
+   * Note that base stream is closed if this stream is closed.
+   * @param delegate specified base stream.
+   */
+  public FilterOutputStream2(OutputStream delegate) {
+    this.delegate = Objects.requireNonNull(delegate);
+  }
+  
+  @Override
+  public void write(byte[] b) throws IOException {
+    delegate.write(b);
+  }
+
+  @Override
+  public void write(byte[] b, int off, int len) throws IOException {
+    delegate.write(b, off, len);
+  }
+
+  @Override
+  public void flush() throws IOException {
+    delegate.flush();
+  }
+
+  @Override
+  public void close() throws IOException {
+    delegate.close();
+  }
+
+  @Override
+  public void write(int b) throws IOException {
+    delegate.write(b);
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterPath.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterPath.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterPath.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterPath.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,265 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent.Kind;
+import java.nio.file.WatchEvent.Modifier;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Iterator;
+
+/**  
+ * A {@code FilterPath} contains another 
+ * {@code Path}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterPath implements Path {
+  
+  /** 
+   * The underlying {@code Path} instance. 
+   */
+  protected final Path delegate;
+  
+  /** 
+   * The parent {@code FileSystem} for this path. 
+   */
+  protected final FileSystem fileSystem;
+  
+  /**
+   * Construct a {@code FilterPath} with parent
+   * {@code fileSystem}, based on the specified base path.
+   * @param delegate specified base path.
+   * @param fileSystem parent fileSystem.
+   */
+  public FilterPath(Path delegate, FileSystem fileSystem) {
+    this.delegate = delegate;
+    this.fileSystem = fileSystem;
+  }
+  
+  /** 
+   * Get the underlying wrapped path.
+   * @return wrapped path.
+   */
+  public Path getDelegate() {
+    return delegate;
+  }
+
+  @Override
+  public FileSystem getFileSystem() {
+    return fileSystem;
+  }
+
+  @Override
+  public boolean isAbsolute() {
+    return delegate.isAbsolute();
+  }
+
+  @Override
+  public Path getRoot() {
+    Path root = delegate.getRoot();
+    if (root == null) {
+      return null;
+    }
+    return new FilterPath(root, fileSystem);
+  }
+
+  @Override
+  public Path getFileName() {
+    Path fileName = delegate.getFileName();
+    if (fileName == null) {
+      return null;
+    }
+    return new FilterPath(fileName, fileSystem);
+  }
+
+  @Override
+  public Path getParent() {
+    Path parent = delegate.getParent();
+    if (parent == null) {
+      return null;
+    }
+    return new FilterPath(parent, fileSystem);
+  }
+
+  @Override
+  public int getNameCount() {
+    return delegate.getNameCount();
+  }
+
+  @Override
+  public Path getName(int index) {
+    return new FilterPath(delegate.getName(index), fileSystem);
+  }
+
+  @Override
+  public Path subpath(int beginIndex, int endIndex) {
+    return new FilterPath(delegate.subpath(beginIndex, endIndex), fileSystem);
+  }
+
+  @Override
+  public boolean startsWith(Path other) {
+    if (other instanceof FilterPath) {
+      FilterPath f = (FilterPath) other;
+      return fileSystem == f.fileSystem && delegate.startsWith(f.delegate);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean startsWith(String other) {
+    return delegate.startsWith(other);
+  }
+
+  @Override
+  public boolean endsWith(Path other) {
+    if (other instanceof FilterPath) {
+      FilterPath f = (FilterPath) other;
+      return fileSystem == f.fileSystem && delegate.endsWith(f.delegate);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean endsWith(String other) {
+    return delegate.startsWith(other);
+  }
+
+  @Override
+  public Path normalize() {
+    return new FilterPath(delegate.normalize(), fileSystem);
+  }
+
+  @Override
+  public Path resolve(Path other) {
+    if (other instanceof FilterPath) {
+      other = ((FilterPath)other).delegate;
+    }
+    return new FilterPath(delegate.resolve(other), fileSystem);
+  }
+
+  @Override
+  public Path resolve(String other) {
+    return new FilterPath(delegate.resolve(other), fileSystem);
+  }
+
+  @Override
+  public Path resolveSibling(Path other) {
+    if (other instanceof FilterPath) {
+      other = ((FilterPath)other).delegate;
+    }
+    return new FilterPath(delegate.resolveSibling(other), fileSystem);
+  }
+
+  @Override
+  public Path resolveSibling(String other) {
+    return new FilterPath(delegate.resolveSibling(other), fileSystem);
+  }
+
+  @Override
+  public Path relativize(Path other) {
+    if (other instanceof FilterPath) {
+      other = ((FilterPath)other).delegate;
+    }
+    return new FilterPath(delegate.relativize(other), fileSystem);
+  }
+
+  // TODO: should these methods not expose delegate result directly?
+  // it could allow code to "escape" the sandbox... 
+
+  @Override
+  public URI toUri() {
+    return delegate.toUri();
+  }
+  
+  @Override
+  public String toString() {
+    return delegate.toString();
+  }
+
+  @Override
+  public Path toAbsolutePath() {
+    return new FilterPath(delegate.toAbsolutePath(), fileSystem);
+  }
+
+  @Override
+  public Path toRealPath(LinkOption... options) throws IOException {
+    return new FilterPath(delegate.toRealPath(options), fileSystem);
+  }
+
+  @Override
+  public File toFile() {
+    // TODO: should we throw exception here?
+    return delegate.toFile();
+  }
+
+  @Override
+  public WatchKey register(WatchService watcher, Kind<?>[] events, Modifier... modifiers) throws IOException {
+    return delegate.register(watcher, events, modifiers);
+  }
+
+  @Override
+  public WatchKey register(WatchService watcher, Kind<?>... events) throws IOException {
+    return delegate.register(watcher, events);
+  }
+
+  @Override
+  public Iterator<Path> iterator() {
+    final Iterator<Path> iterator = delegate.iterator();
+    return new Iterator<Path>() {
+      @Override
+      public boolean hasNext() {
+        return iterator.hasNext();
+      }
+
+      @Override
+      public Path next() {
+        return new FilterPath(iterator.next(), fileSystem);
+      }
+    };
+  }
+
+  @Override
+  public int compareTo(Path other) {
+    if (other instanceof FilterPath) {
+      other = ((FilterPath)other).delegate;
+    }
+    return delegate.compareTo(other);
+  }
+  
+  /**
+   * Unwraps all {@code FilterPath}s, returning
+   * the innermost {@code Path}.
+   * <p>
+   * WARNING: this is exposed for testing only!
+   * @param path specified path.
+   * @return innermost Path instance
+   */
+  public static Path unwrap(Path path) {
+    while (path instanceof FilterPath) {
+      path = ((FilterPath)path).delegate;
+    }
+    return path;
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSecureDirectoryStream.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSecureDirectoryStream.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSecureDirectoryStream.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSecureDirectoryStream.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,98 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.util.Iterator;
+import java.util.Set;
+
+/**  
+ * A {@code FilterSecureDirectoryStream} contains another 
+ * {@code SecureDirectoryStream}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterSecureDirectoryStream<T> implements SecureDirectoryStream<T> {
+  
+  /** 
+   * The underlying {@code SecureDirectoryStream} instance. 
+   */
+  protected final SecureDirectoryStream<T> delegate;
+  
+  /**
+   * Construct a {@code FilterSecureDirectoryStream} based on 
+   * the specified base stream.
+   * <p>
+   * Note that base stream is closed if this stream is closed.
+   * @param delegate specified base stream.
+   */
+  public FilterSecureDirectoryStream(SecureDirectoryStream<T> delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    return delegate.iterator();
+  }
+
+  @Override
+  public void close() throws IOException {
+    delegate.close();
+  }
+
+  @Override
+  public SecureDirectoryStream<T> newDirectoryStream(T path, LinkOption... options) throws IOException {
+    return delegate.newDirectoryStream(path, options);
+  }
+
+  @Override
+  public SeekableByteChannel newByteChannel(T path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+    return delegate.newByteChannel(path, options, attrs);
+  }
+
+  @Override
+  public void deleteFile(T path) throws IOException {
+    delegate.deleteFile(path);
+  }
+
+  @Override
+  public void deleteDirectory(T path) throws IOException {
+    delegate.deleteDirectory(path);
+  }
+
+  @Override
+  public void move(T srcpath, SecureDirectoryStream<T> targetdir, T targetpath) throws IOException {
+    delegate.move(srcpath, targetdir, targetpath);
+  }
+
+  @Override
+  public <V extends FileAttributeView> V getFileAttributeView(Class<V> type) {
+    return delegate.getFileAttributeView(type);
+  }
+
+  @Override
+  public <V extends FileAttributeView> V getFileAttributeView(T path, Class<V> type, LinkOption... options) {
+    return delegate.getFileAttributeView(path, type, options);
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSeekableByteChannel.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSeekableByteChannel.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSeekableByteChannel.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/FilterSeekableByteChannel.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,89 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+
+/**  
+ * A {@code FilterSeekableByteChannel} contains another 
+ * {@code SeekableByteChannel}, which it uses as its basic 
+ * source of data, possibly transforming the data along the 
+ * way or providing additional functionality. 
+ */
+public class FilterSeekableByteChannel implements SeekableByteChannel {
+  
+  /** 
+   * The underlying {@code SeekableByteChannel} instance. 
+   */
+  protected final SeekableByteChannel delegate;
+  
+  /**
+   * Construct a {@code FilterSeekableByteChannel} based on 
+   * the specified base channel.
+   * <p>
+   * Note that base channel is closed if this channel is closed.
+   * @param delegate specified base channel.
+   */
+  public FilterSeekableByteChannel(SeekableByteChannel delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public boolean isOpen() {
+    return delegate.isOpen();
+  }
+
+  @Override
+  public void close() throws IOException {
+    delegate.close();
+  }
+
+  @Override
+  public int read(ByteBuffer dst) throws IOException {
+    return delegate.read(dst);
+  }
+
+  @Override
+  public int write(ByteBuffer src) throws IOException {
+    return delegate.write(src);
+  }
+
+  @Override
+  public long position() throws IOException {
+    return delegate.position();
+  }
+
+  @Override
+  public SeekableByteChannel position(long newPosition) throws IOException {
+    delegate.position(newPosition);
+    return this;
+  }
+
+  @Override
+  public long size() throws IOException {
+    return delegate.size();
+  }
+
+  @Override
+  public SeekableByteChannel truncate(long size) throws IOException {
+    delegate.truncate(size);
+    return this;
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/HandleTrackingFS.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/HandleTrackingFS.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/HandleTrackingFS.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/HandleTrackingFS.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,314 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.AsynchronousFileChannel;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.attribute.FileAttribute;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+
+/** 
+ * Base class for tracking file handles.
+ * <p>
+ * This class adds tracking to all streams/channels and 
+ * provides two hooks to handle file management:
+ * <ul>
+ *   <li>{@link #onOpen(Path, Object)}
+ *   <li>{@link #onClose(Path, Object)}
+ * </ul>
+ */
+public abstract class HandleTrackingFS extends FilterFileSystemProvider {
+  
+  /**
+   * Create a new instance, identified by {@code scheme} and passing
+   * through operations to {@code delegate}. 
+   * @param scheme URI scheme for this provider
+   * @param delegate delegate filesystem to wrap.
+   */
+  public HandleTrackingFS(String scheme, FileSystem delegate) {
+    super(scheme, delegate);
+  }
+  
+  /**
+   * Called when {@code path} is opened via {@code stream}. 
+   * @param path Path that was opened
+   * @param stream Stream or Channel opened against the path.
+   * @throws IOException if an I/O error occurs.
+   */
+  protected abstract void onOpen(Path path, Object stream) throws IOException;
+
+  
+  /**
+   * Called when {@code path} is closed via {@code stream}. 
+   * @param path Path that was closed
+   * @param stream Stream or Channel closed against the path.
+   * @throws IOException if an I/O error occurs.
+   */
+  protected abstract void onClose(Path path, Object stream) throws IOException;
+
+  @Override
+  public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
+    InputStream stream = new FilterInputStream2(super.newInputStream(path, options)) {
+      @Override
+      public void close() throws IOException {
+        onClose(path, this);
+        super.close();
+      }
+
+      @Override
+      public String toString() {
+        return "InputStream(" + path.toString() + ")";
+      }
+
+      @Override
+      public int hashCode() {
+        return System.identityHashCode(this);
+      }
+
+      @Override
+      public boolean equals(Object obj) {
+        return this == obj;
+      }
+    };
+    onOpen(path, stream);
+    return stream;
+  }
+
+  @Override
+  public OutputStream newOutputStream(final Path path, OpenOption... options) throws IOException {
+    OutputStream stream = new FilterOutputStream2(super.newOutputStream(path, options)) {
+      @Override
+      public void close() throws IOException {
+        onClose(path, this);
+        super.close();
+      }
+      
+      @Override
+      public String toString() {
+        return "OutputStream(" + path.toString() + ")";
+      }
+      
+      @Override
+      public int hashCode() {
+        return System.identityHashCode(this);
+      }
+
+      @Override
+      public boolean equals(Object obj) {
+        return this == obj;
+      }
+    };
+    onOpen(path, stream);
+    return stream;
+  }
+  
+  @Override
+  public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+    FileChannel channel = new FilterFileChannel(super.newFileChannel(path, options, attrs)) {
+      @Override
+      protected void implCloseChannel() throws IOException {
+        onClose(path, this);
+        super.implCloseChannel();
+      }
+
+      @Override
+      public String toString() {
+        return "FileChannel(" + path.toString() + ")";
+      }
+      
+      @Override
+      public int hashCode() {
+        return System.identityHashCode(this);
+      }
+
+      @Override
+      public boolean equals(Object obj) {
+        return this == obj;
+      }
+    };
+    onOpen(path, channel);
+    return channel;
+  }
+
+  @Override
+  public AsynchronousFileChannel newAsynchronousFileChannel(Path path, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) throws IOException {
+    AsynchronousFileChannel channel = new FilterAsynchronousFileChannel(super.newAsynchronousFileChannel(path, options, executor, attrs)) {
+      @Override
+      public void close() throws IOException {
+        onClose(path, this);
+        super.close();
+      }
+
+      @Override
+      public String toString() {
+        return "AsynchronousFileChannel(" + path.toString() + ")";
+      }
+      
+      @Override
+      public int hashCode() {
+        return System.identityHashCode(this);
+      }
+
+      @Override
+      public boolean equals(Object obj) {
+        return this == obj;
+      }
+    };
+    onOpen(path, channel);
+    return channel;
+  }
+
+  @Override
+  public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+    SeekableByteChannel channel = new FilterSeekableByteChannel(super.newByteChannel(path, options, attrs)) {
+      @Override
+      public void close() throws IOException {
+        onClose(path, this);
+        super.close();
+      }
+
+      @Override
+      public String toString() {
+        return "SeekableByteChannel(" + path.toString() + ")";
+      }
+      
+      @Override
+      public int hashCode() {
+        return System.identityHashCode(this);
+      }
+
+      @Override
+      public boolean equals(Object obj) {
+        return this == obj;
+      }
+    };
+    onOpen(path, channel);
+    return channel;
+  }
+
+  @Override
+  public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
+    DirectoryStream<Path> stream = super.newDirectoryStream(dir, filter);
+    if (stream instanceof SecureDirectoryStream) {
+      stream = new TrackingSecureDirectoryStream((SecureDirectoryStream<Path>)stream, dir);
+    } else {
+      stream = new FilterDirectoryStream<Path>(stream) {
+        @Override
+        public void close() throws IOException {
+          onClose(dir, this);
+          super.close();
+        }
+        
+        @Override
+        public String toString() {
+          return "DirectoryStream(" + dir + ")";
+        }
+        
+        @Override
+        public int hashCode() {
+          return System.identityHashCode(this);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+          return this == obj;
+        }
+      };
+    }
+    onOpen(dir, stream);
+    return stream;
+  }
+  
+  /** You can also open various things from SecureDirectoryStream, so we ensure we track those */
+  class TrackingSecureDirectoryStream extends FilterSecureDirectoryStream<Path> {
+    final Path dir;
+    
+    TrackingSecureDirectoryStream(SecureDirectoryStream<Path> delegate, Path dir) {
+      super(delegate);
+      this.dir = dir;
+    }
+
+    @Override
+    public void close() throws IOException {
+      onClose(dir, this);
+      super.close();
+    }
+    
+    @Override
+    public String toString() {
+      return "SecureDirectoryStream(" + dir + ")";
+    }
+    
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public SecureDirectoryStream<Path> newDirectoryStream(Path path, LinkOption... options) throws IOException {
+      SecureDirectoryStream<Path> stream = new TrackingSecureDirectoryStream(super.newDirectoryStream(path, options), path);
+      onOpen(path, stream);
+      return stream;
+    }
+
+    @Override
+    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
+      SeekableByteChannel channel = new FilterSeekableByteChannel(super.newByteChannel(path, options, attrs)) {
+        @Override
+        public void close() throws IOException {
+          onClose(path, this);
+          super.close();
+        }
+
+        @Override
+        public String toString() {
+          return "SeekableByteChannel(" + path.toString() + ")";
+        }
+        
+        @Override
+        public int hashCode() {
+          return System.identityHashCode(this);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+          return this == obj;
+        }
+      };
+      onOpen(path, channel);
+      return channel;
+    }
+  }
+}

Added: lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/LeakFS.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/LeakFS.java?rev=1641632&view=auto
==============================================================================
--- lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/LeakFS.java (added)
+++ lucene/dev/trunk/lucene/test-framework/src/java/org/apache/lucene/mockfile/LeakFS.java Tue Nov 25 15:21:18 2014
@@ -0,0 +1,67 @@
+package org.apache.lucene.mockfile;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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.
+ */
+
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** 
+ * FileSystem that tracks open handles.
+ * <p>
+ * When {@link FileSystem#close()} is called, this class will throw
+ * an exception if any file handles are still open.
+ */
+public class LeakFS extends HandleTrackingFS {
+  // we explicitly use reference hashcode/equality in our keys
+  private final Map<Object,Exception> openHandles = new ConcurrentHashMap<>();
+  
+  /**
+   * Create a new instance, tracking file handle leaks for the 
+   * specified delegate filesystem.
+   * @param delegate delegate filesystem to wrap.
+   */
+  public LeakFS(FileSystem delegate) {
+    super("leakfs://", delegate);
+  }
+
+  @Override
+  protected void onOpen(Path path, Object stream) {
+    openHandles.put(stream, new Exception());
+  }
+
+  @Override
+  protected void onClose(Path path, Object stream) {
+    openHandles.remove(stream);
+  }
+
+  @Override
+  public synchronized void onClose() {
+    if (!openHandles.isEmpty()) {
+      // print the first one as its very verbose otherwise
+      Exception cause = null;
+      Iterator<Exception> stacktraces = openHandles.values().iterator();
+      if (stacktraces.hasNext()) {
+        cause = stacktraces.next();
+      }
+      throw new RuntimeException("file handle leaks: " + openHandles.keySet(), cause);
+    }
+  }
+}