Skip to content

Commit

Permalink
Add integration test and example jar.
Browse files Browse the repository at this point in the history
The integration tests show that we can read/write from GCS, and should
be able to run from Travis.

The example jar demonstrates the other use case for NIO: adding GCS
support to a legacy Java 7 program just by adding a jar file to its
classpath.
  • Loading branch information
jean-philippe-martin committed Mar 7, 2016
1 parent 3137b3d commit 73b9ef6
Show file tree
Hide file tree
Showing 9 changed files with 503 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableSet;
import com.google.gcloud.storage.StorageOptions;

import java.io.IOException;
import java.net.URI;
Expand Down Expand Up @@ -62,6 +63,20 @@ public static CloudStorageFileSystem forBucket(String bucket, CloudStorageConfig
new CloudStorageFileSystemProvider(), bucket, checkNotNull(config));
}

/**
* Creates a new filesystem for a particular bucket, with customizable settings and storage
* options.
*
* @see #forBucket(String)
*/
public static CloudStorageFileSystem forBucket(String bucket, CloudStorageConfiguration config,
StorageOptions storageOptions) {
checkArgument(!bucket.startsWith(URI_SCHEME + ":"),
"Bucket name must not have schema: %s", bucket);
return new CloudStorageFileSystem(new CloudStorageFileSystemProvider(
checkNotNull(storageOptions)), bucket, checkNotNull(config));
}

public static final String URI_SCHEME = "gs";
public static final String GCS_VIEW = "gcs";
public static final String BASIC_VIEW = "basic";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public CloudStorageFileSystemProvider() {
this(storageOptions);
}

private CloudStorageFileSystemProvider(@Nullable StorageOptions gcsStorageOptions) {
CloudStorageFileSystemProvider(@Nullable StorageOptions gcsStorageOptions) {
if (gcsStorageOptions == null) {
this.storage = StorageOptions.defaultInstance().service();
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.google.gcloud.storage.contrib.nio.CloudStorageFileSystemProvider
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.google.common.testing.EqualsTester;
import com.google.common.testing.NullPointerTester;
import com.google.gcloud.storage.StorageOptions;
import com.google.gcloud.storage.testing.LocalGcsHelper;

import org.junit.Before;
Expand Down Expand Up @@ -112,6 +113,7 @@ public void testNullness() throws IOException, NoSuchMethodException, SecurityEx
new NullPointerTester()
.ignore(CloudStorageFileSystem.class.getMethod("equals", Object.class))
.setDefault(CloudStorageConfiguration.class, CloudStorageConfiguration.DEFAULT);
.setDefault(StorageOptions.class, StorageOptions.defaultInstance());
tester.testAllPublicStaticMethods(CloudStorageFileSystem.class);
tester.testAllPublicInstanceMethods(fs);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
package com.google.gcloud.storage.contrib.nio;

import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableList;
import com.google.gcloud.AuthCredentials;
import com.google.gcloud.storage.StorageOptions;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.StringBufferInputStream;
import java.io.StringReader;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.Random;


/**
* Integration test for gcloud-nio. This test actually talks to GCS (you need an account).
* Tests both reading and writing.
*/
@RunWith(JUnit4.class)
public class ITGcsNio {

//
// The variables below are filled in via environment variables
// (of the same name).
// You *must* set these environment variables before running this test or it will fail.
// The test also relies on you using the gcloud command-line utility to log into
// an account that has read access to the bucket you'll point the tests to.
//
// The instructions for how to get the Service Account JSON Key are
// at https://cloud.google.com/storage/docs/authentication?hl=en#service_accounts
//
// The short version is this: go to cloud.google.com/console,
// select your project, search for "API manager", click "Credentials",
// click "create credentials/service account key", new service account,
// JSON. The contents of the file that's sent to your browsers is your
// "Service Account JSON Key".
//

// The bucket we'll read the files from (e.g. "abucket")
private final String TEST_NIO_BUCKET;
// A first file we can read (can be small) (e.g. "folder/file.txt")
private final String TEST_NIO_SML;
// The size of the small file, in bytes
private final int TEST_NIO_SML_SIZE;
// A second file we can read (can be large)
private final String TEST_NIO_LGE;
// A non-existant file we can write to. We're going to add a suffix to it
// just in case multiple tests are running concurrently.
private final String TEST_NIO_WRITE_PREFIX;
// The project that the bucket belongs to.
// This ought to match the project_id entry in the service account json key below.
private final String TEST_NIO_PROJECT;
// the full text of the service account key.
// e.g. {"type":"service_account","project_id":...
// Naturally you don't want to share this since it gives people access to this project.
private final String TEST_NIO_SERVICE_ACCOUNT_JSON_KEY;

private final Random rnd;

private static final List<String> FILE_CONTENTS = ImmutableList.of(
"Tous les êtres humains naissent libres et égaux en dignité et en droits.",
"Ils sont doués de raison et de conscience et doivent agir ",
"les uns envers les autres dans un esprit de fraternité.");



/**
* Sets up the test according to the provided env. vars.
*/
public ITGcsNio() {
TEST_NIO_BUCKET = getEnv("TEST_NIO_BUCKET");
TEST_NIO_SML = getEnv("TEST_NIO_SML");
TEST_NIO_SML_SIZE = Integer.parseInt(getEnv("TEST_NIO_SML_SIZE"));
TEST_NIO_LGE = getEnv("TEST_NIO_LGE");
TEST_NIO_WRITE_PREFIX = getEnv("TEST_NIO_WRITE_PREFIX");
TEST_NIO_PROJECT = getEnv("TEST_NIO_PROJECT");
TEST_NIO_SERVICE_ACCOUNT_JSON_KEY = getEnv("TEST_NIO_SERVICE_ACCOUNT_JSON_KEY");
rnd = new Random();
}

@Test
public void testFileExists() throws IOException {
CloudStorageFileSystem testBucket = getTestBucket();
Path path = testBucket.getPath(TEST_NIO_SML);
assertThat(Files.exists(path)).isTrue();
}

@Test
public void testFileSize() throws IOException {
CloudStorageFileSystem testBucket = getTestBucket();
Path path = testBucket.getPath(TEST_NIO_SML);
assertThat(Files.size(path)).isEqualTo(TEST_NIO_SML_SIZE);
}

@Test
public void testReadByteChannel() throws IOException {
CloudStorageFileSystem testBucket = getTestBucket();
Path path = testBucket.getPath(TEST_NIO_SML);
long size = Files.size(path);
SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.READ);
assertThat(chan.size()).isEqualTo(size);
ByteBuffer buf = ByteBuffer.allocate(1024 * 1024);
int timeout = 1024 * 1024;
int read = 0;
while (chan.isOpen()) {
if (timeout-- <= 0) {
break;
}
int rc = chan.read(buf);
if (rc < 0) {
// EOF
break;
}
buf.clear();
assertThat(rc).isGreaterThan(0);
read += rc;
assertThat(chan.position() == read);
}
assertThat(read).isEqualTo(size);
}

@Test
public void testSeek() throws IOException {
CloudStorageFileSystem testBucket = getTestBucket();
Path path = testBucket.getPath(TEST_NIO_LGE);
long size = Files.size(path);
final ByteBuffer buf1a = ByteBuffer.allocate(100);
final ByteBuffer buf1b = ByteBuffer.allocate(100);
final ByteBuffer buf2a = ByteBuffer.allocate(100);
final ByteBuffer buf2b = ByteBuffer.allocate(100);
SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.READ);
assertThat(chan.size()).isEqualTo(size);
reallyRead(chan, buf1a);
long dest = size / 2;
chan.position(dest);
reallyRead(chan, buf2a);
// now go back and read it again
// (we do 2 locations because 0 is sometimes a special case).
chan.position(0);
reallyRead(chan, buf1b);
chan.position(dest);
reallyRead(chan, buf2b);
// if the two spots in the file have the same contents, then this isn't a good file for this
// test.
assertThat(buf1a.array()).isNotEqualTo(buf2a.array());
assertThat(buf1a.array()).isEqualTo(buf1b.array());
assertThat(buf2a.array()).isEqualTo(buf2b.array());
}

@Test
public void testCreate() throws IOException {
CloudStorageFileSystem testBucket = getTestBucket();
Path path = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix());
try {
// file shouldn't exist initially. If it does it's either because it's a leftover
// from a previous run (so we should delete the file)
// or because we're misconfigured and pointing to an actually important file
// (so we should absolutely not delete it).
// So if the file's here, don't try to fix it automatically, let the user deal with it.
assertThat(Files.exists(path)).isFalse();

Files.createFile(path);
// now it does, and it has size 0.
assertThat(Files.exists(path)).isTrue();
long size = Files.size(path);
assertThat(size).isEqualTo(0);
} finally {
// let's not leave files around
Files.deleteIfExists(path);
}
}

@Test
public void testWrite() throws IOException {
CloudStorageFileSystem testBucket = getTestBucket();
Path path = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix());
try {
// file shouldn't exist initially. If it does it's either because it's a leftover
// from a previous run (so we should delete the file)
// or because we're misconfigured and pointing to an actually important file
// (so we should absolutely not delete it).
// So if the file's here, don't try to fix it automatically, let the user deal with it.
assertThat(Files.exists(path)).isFalse();

Files.write(path, FILE_CONTENTS, UTF_8);
// now it does.
assertThat(Files.exists(path)).isTrue();

// let's check that the contents look OK.
ByteBuffer buf = ByteBuffer.allocate(100);
SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.READ);
reallyRead(chan, buf);
byte[] gotBytes = buf.array();
byte[] wantBytes = FILE_CONTENTS.get(0).getBytes(UTF_8);
for (int i = 0; i < wantBytes.length; i++) {
assertThat(gotBytes[i] == wantBytes[i]).isTrue();
}

} finally {
// let's not leave files around
Files.deleteIfExists(path);
}
}

@Test
public void testWriteOnClose() throws Exception {
CloudStorageFileSystem testBucket = getTestBucket();
Path path = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix());
// file shouldn't exist initially (see above)
assertThat(Files.exists(path)).isFalse();
try {
try (SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.WRITE)) {
// writing lots of contents to defeat channel-internal buffering.
for (int i=0; i<9999; i++) {
for (String s : FILE_CONTENTS) {
chan.write(ByteBuffer.wrap(s.getBytes(UTF_8)));
}
}
try {
Files.size(path);
// we shouldn't make it to this line. Not using thrown.expect because
// I still want to run a few lines after the exception.
assertThat(false).isTrue();
} catch (NoSuchFileException nsf) {
// that's what we wanted, we're good.
}
}
// channel now closed, the file should be there and with the new contents.
assertThat(Files.exists(path)).isTrue();
assertThat(Files.size(path)).isGreaterThan(0L);
} finally {
Files.deleteIfExists(path);
}
}

@Test
public void testCopy() throws IOException {
CloudStorageFileSystem testBucket = getTestBucket();
Path src = testBucket.getPath(TEST_NIO_SML);
Path dst = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix());
try {
// file shouldn't exist initially (see above).
assertThat(Files.exists(dst)).isFalse();

Files.copy(src, dst);

assertThat(Files.exists(dst)).isTrue();
assertThat(Files.size(dst)).isEqualTo(TEST_NIO_SML_SIZE);
} finally {
// let's not leave files around
Files.deleteIfExists(dst);
}
}

private String getEnv(String name) {
String ret = System.getenv(name);
// we don't want empty strings either because then tests could access places not expected by
// the caller.
if (null == ret || ret.length() == 0) {
throw new RuntimeException(name + " environment variable must be set. Please see the "
+ "instructions for setting up nio's integration tests.");
}
return ret;
}

private int reallyRead(ReadableByteChannel chan, ByteBuffer buf) throws IOException {
int sofar = 0;
int bytes = buf.remaining();
while (sofar < bytes) {
int read = chan.read(buf);
if (read < 0) {
throw new EOFException("channel EOF");
}
sofar += read;
}
return sofar;
}

private String randomSuffix() {
return "-" + rnd.nextInt(99999);
}


private CloudStorageFileSystem getTestBucket() throws IOException {
// in typical usage we use the single-argument version of forBucket
// and rely on the user being logged into their project with the
// gcloud tool, and then everything authenticates automagically
// (or we just use paths that start with "gs://" and rely on NIO's magic).
//
// However for the tests we want to be able to run in automated environments
// where we can set environment variables but not necessarily install gcloud
// or run it. That's why we're setting the credentials programmatically.
StorageOptions storageOptions = StorageOptions.builder()
.authCredentials(AuthCredentials.createForJson(new ByteArrayInputStream
(TEST_NIO_SERVICE_ACCOUNT_JSON_KEY.getBytes(UTF_8))))
.projectId(TEST_NIO_PROJECT)
.build();
return CloudStorageFileSystem.forBucket(
TEST_NIO_BUCKET, CloudStorageConfiguration.DEFAULT, storageOptions);
}

}
Loading

0 comments on commit 73b9ef6

Please sign in to comment.