JimfsFileSystemCloseTest.java

/*
 * Copyright 2014 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.jimfs;

import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.WRITE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import com.google.common.collect.ImmutableList;
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.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.ClosedDirectoryStreamException;
import java.nio.file.ClosedFileSystemException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for what happens when a file system is closed.
 *
 * @author Colin Decker
 */
@RunWith(JUnit4.class)
public class JimfsFileSystemCloseTest {

  private JimfsFileSystem fs = (JimfsFileSystem) Jimfs.newFileSystem(Configuration.unix());

  @Test
  public void testIsNotOpen() throws IOException {
    assertTrue(fs.isOpen());
    fs.close();
    assertFalse(fs.isOpen());
  }

  @Test
  public void testIsNotAvailableFromProvider() throws IOException {
    URI uri = fs.getUri();
    assertEquals(fs, FileSystems.getFileSystem(uri));

    fs.close();

    assertThrows(FileSystemNotFoundException.class, () -> FileSystems.getFileSystem(uri));
  }

  @Test
  public void testOpenStreamsClosed() throws IOException {
    Path p = fs.getPath("/foo");
    OutputStream out = Files.newOutputStream(p);
    InputStream in = Files.newInputStream(p);

    out.write(1);
    assertEquals(1, in.read());

    fs.close();

    IOException expected = assertThrows(IOException.class, () -> out.write(1));
    assertThat(expected).hasMessageThat().isEqualTo("stream is closed");

    expected = assertThrows(IOException.class, () -> in.read());
    assertThat(expected).hasMessageThat().isEqualTo("stream is closed");
  }

  @Test
  public void testOpenChannelsClosed() throws IOException {
    Path p = fs.getPath("/foo");
    FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE);
    SeekableByteChannel sbc = Files.newByteChannel(p, READ);
    AsynchronousFileChannel afc = AsynchronousFileChannel.open(p, READ, WRITE);

    assertTrue(fc.isOpen());
    assertTrue(sbc.isOpen());
    assertTrue(afc.isOpen());

    fs.close();

    assertFalse(fc.isOpen());
    assertFalse(sbc.isOpen());
    assertFalse(afc.isOpen());

    assertThrows(ClosedChannelException.class, () -> fc.size());

    assertThrows(ClosedChannelException.class, () -> sbc.size());

    assertThrows(ClosedChannelException.class, () -> afc.size());
  }

  @Test
  public void testOpenDirectoryStreamsClosed() throws IOException {
    Path p = fs.getPath("/foo");
    Files.createDirectory(p);

    try (DirectoryStream<Path> stream = Files.newDirectoryStream(p)) {

      fs.close();

      assertThrows(ClosedDirectoryStreamException.class, () -> stream.iterator());
    }
  }

  @Test
  public void testOpenWatchServicesClosed() throws IOException {
    WatchService ws1 = fs.newWatchService();
    WatchService ws2 = fs.newWatchService();

    assertNull(ws1.poll());
    assertNull(ws2.poll());

    fs.close();

    assertThrows(ClosedWatchServiceException.class, () -> ws1.poll());

    assertThrows(ClosedWatchServiceException.class, () -> ws2.poll());
  }

  @Test
  public void testPathMethodsThrow() throws IOException {
    Path p = fs.getPath("/foo");
    Files.createDirectory(p);

    WatchService ws = fs.newWatchService();

    fs.close();

    assertThrows(
        ClosedWatchServiceException.class,
        () -> p.register(ws, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY));

    assertThrows(ClosedFileSystemException.class, () -> p.toRealPath());

    // While technically (according to the FileSystem.close() spec) all methods on Path should
    // probably throw, we only throw for methods that access the file system itself in some way...
    // path manipulation methods seem totally harmless to keep working, and I don't see any need to
    // add the overhead of checking that the file system is open for each of those method calls.
  }

  @Test
  public void testOpenFileAttributeViewsThrow() throws IOException {
    Path p = fs.getPath("/foo");
    Files.createFile(p);

    BasicFileAttributeView view = Files.getFileAttributeView(p, BasicFileAttributeView.class);

    fs.close();

    assertThrows(ClosedFileSystemException.class, () -> view.readAttributes());

    assertThrows(ClosedFileSystemException.class, () -> view.setTimes(null, null, null));
  }

  @Test
  public void testFileSystemMethodsThrow() throws IOException {
    fs.close();

    assertThrows(ClosedFileSystemException.class, () -> fs.getPath("/foo"));

    assertThrows(ClosedFileSystemException.class, () -> fs.getRootDirectories());

    assertThrows(ClosedFileSystemException.class, () -> fs.getFileStores());

    assertThrows(ClosedFileSystemException.class, () -> fs.getPathMatcher("glob:*.java"));

    assertThrows(ClosedFileSystemException.class, () -> fs.getUserPrincipalLookupService());

    assertThrows(ClosedFileSystemException.class, () -> fs.newWatchService());

    assertThrows(ClosedFileSystemException.class, () -> fs.supportedFileAttributeViews());
  }

  @Test
  public void testFilesMethodsThrow() throws IOException {
    Path file = fs.getPath("/file");
    Path dir = fs.getPath("/dir");
    Path nothing = fs.getPath("/nothing");

    Files.createDirectory(dir);
    Files.createFile(file);

    fs.close();

    // not exhaustive, but should cover every major type of functionality accessible through Files
    // TODO(cgdecker): reflectively invoke all methods with default arguments?

    assertThrows(ClosedFileSystemException.class, () -> Files.delete(file));

    assertThrows(ClosedFileSystemException.class, () -> Files.createDirectory(nothing));

    assertThrows(ClosedFileSystemException.class, () -> Files.createFile(nothing));

    assertThrows(
        ClosedFileSystemException.class,
        () -> Files.write(nothing, ImmutableList.of("hello world"), UTF_8));

    assertThrows(ClosedFileSystemException.class, () -> Files.newInputStream(file));

    assertThrows(ClosedFileSystemException.class, () -> Files.newOutputStream(file));

    assertThrows(ClosedFileSystemException.class, () -> Files.newByteChannel(file));

    assertThrows(ClosedFileSystemException.class, () -> Files.newDirectoryStream(dir));

    assertThrows(ClosedFileSystemException.class, () -> Files.copy(file, nothing));

    assertThrows(ClosedFileSystemException.class, () -> Files.move(file, nothing));

    assertThrows(ClosedFileSystemException.class, () -> Files.copy(dir, nothing));

    assertThrows(ClosedFileSystemException.class, () -> Files.move(dir, nothing));

    assertThrows(ClosedFileSystemException.class, () -> Files.createSymbolicLink(nothing, file));

    assertThrows(ClosedFileSystemException.class, () -> Files.createLink(nothing, file));

    assertThrows(ClosedFileSystemException.class, () -> Files.exists(file));

    assertThrows(ClosedFileSystemException.class, () -> Files.getAttribute(file, "size"));

    assertThrows(
        ClosedFileSystemException.class,
        () -> Files.setAttribute(file, "lastModifiedTime", FileTime.fromMillis(0)));

    assertThrows(
        ClosedFileSystemException.class,
        () -> Files.getFileAttributeView(file, BasicFileAttributeView.class));

    assertThrows(
        ClosedFileSystemException.class,
        () -> Files.readAttributes(file, "basic:size,lastModifiedTime"));

    assertThrows(
        ClosedFileSystemException.class,
        () -> Files.readAttributes(file, BasicFileAttributes.class));

    assertThrows(ClosedFileSystemException.class, () -> Files.isDirectory(dir));

    assertThrows(ClosedFileSystemException.class, () -> Files.readAllBytes(file));

    assertThrows(ClosedFileSystemException.class, () -> Files.isReadable(file));
  }
}