diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/BaseEmulatorHelper.java b/google-cloud-core/src/main/java/com/google/cloud/testing/BaseEmulatorHelper.java new file mode 100644 index 000000000000..65277a77463b --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/BaseEmulatorHelper.java @@ -0,0 +1,428 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import com.google.cloud.ServiceOptions; +import com.google.common.io.CharStreams; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Utility class to start and stop a local service which is used by unit testing. + */ +public abstract class BaseEmulatorHelper { + + private final String emulator; + private final int port; + private final String projectId; + private EmulatorRunner activeRunner; + private BlockingProcessStreamReader blockingProcessReader; + + protected static final String PROJECT_ID_PREFIX = "test-project-"; + protected static final String DEFAULT_HOST = "localhost"; + protected static final int DEFAULT_PORT = 8080; + + protected BaseEmulatorHelper(String emulator, int port, String projectId) { + this.emulator = emulator; + this.port = port > 0 ? port : DEFAULT_PORT; + this.projectId = projectId; + } + + /** + * Returns the emulator runners supported by this emulator. Runners are evaluated in order, the + * first available runner is selected and executed + */ + protected abstract List getEmulatorRunners(); + + /** + * Returns a logger. + */ + protected abstract Logger getLogger(); + + /** + * Starts the local service as a subprocess. Blocks the execution until {@code blockUntilOutput} + * is found on stdout. + */ + protected final void startProcess(String blockUntilOutput) + throws IOException, InterruptedException { + for (EmulatorRunner runner : getEmulatorRunners()) { + // Iterate through all emulator runners until find first available runner. + if (runner.isAvailable()) { + activeRunner = runner; + runner.start(); + break; + } + } + if (activeRunner != null) { + blockingProcessReader = BlockingProcessStreamReader.start(emulator, + activeRunner.getProcess().getInputStream(), blockUntilOutput, getLogger()); + } else { + // No available runner found. + throw new IOException("No available emulator runner is found."); + } + } + + /** + * Stops the local service's subprocess and any possible thread listening for its output. + */ + protected final void stopProcess() throws IOException, InterruptedException { + if (blockingProcessReader != null) { + blockingProcessReader.terminate(); + blockingProcessReader = null; + } + if (activeRunner != null) { + activeRunner.stop(); + activeRunner = null; + } + } + + /** + * Returns the port to which the local emulator is listening. + */ + public int getPort() { + return port; + } + + /** + * Returns the project ID associated with the local emulator. + */ + public String getProjectId() { + return projectId; + } + + /** + * Returns service options to access the local emulator. + */ + public abstract T getOptions(); + + /** + * Starts the local emulator. + */ + public abstract void start() throws IOException, InterruptedException; + + /** + * Stops the local emulator. + */ + public abstract void stop() throws IOException, InterruptedException; + + /** + * Resets the internal state of the emulator. + */ + public abstract void reset() throws IOException; + + protected final String sendPostRequest(String request) throws IOException { + URL url = new URL("http", DEFAULT_HOST, this.port, request); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setDoOutput(true); + OutputStream out = con.getOutputStream(); + out.write("".getBytes()); + out.flush(); + + InputStream in = con.getInputStream(); + String response = CharStreams.toString(new InputStreamReader(con.getInputStream())); + in.close(); + return response; + } + + protected static int findAvailablePort(int defaultPort) { + try (ServerSocket tempSocket = new ServerSocket(0)) { + return tempSocket.getLocalPort(); + } catch (IOException e) { + return defaultPort; + } + } + + protected static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); + } + + /** + * Utility interface to start and run an emulator. + */ + protected interface EmulatorRunner { + + /** + * Returns {@code true} if the emulator associated to this runner is available and can be + * started. + */ + boolean isAvailable(); + + /** + * Starts the emulator associated to this runner. + */ + void start() throws IOException; + + /** + * Stops the emulator associated to this runner. + */ + void stop() throws InterruptedException; + + /** + * Returns the process associated to the emulator, if any. + */ + Process getProcess(); + } + + /** + * Utility class to start and run an emulator from the Google Cloud SDK. + */ + protected static class GcloudEmulatorRunner implements EmulatorRunner { + + private final List commandText; + private final String versionPrefix; + private final Version minVersion; + private Process process; + private static final Logger log = Logger.getLogger(GcloudEmulatorRunner.class.getName()); + + public GcloudEmulatorRunner(List commandText, String versionPrefix, String minVersion) { + this.commandText = commandText; + this.versionPrefix = versionPrefix; + this.minVersion = Version.fromString(minVersion); + } + + @Override + public boolean isAvailable() { + try { + return isGcloudInstalled() && isEmulatorUpToDate() && !commandText.isEmpty(); + } catch (IOException | InterruptedException e) { + e.printStackTrace(System.err); + } + return false; + } + + @Override + public void start() throws IOException { + log.fine("Starting emulator via Google Cloud SDK"); + process = CommandWrapper.create().setCommand(commandText).setRedirectErrorStream().start(); + } + + @Override + public void stop() throws InterruptedException { + if (process != null) { + process.destroy(); + process.waitFor(); + } + } + + @Override + public Process getProcess() { + return process; + } + + private boolean isGcloudInstalled() { + Map env = System.getenv(); + for (String envName : env.keySet()) { + if ("PATH".equals(envName)) { + return env.get(envName).contains("google-cloud-sdk"); + } + } + return false; + } + + private boolean isEmulatorUpToDate() throws IOException, InterruptedException { + Version currentVersion = getInstalledEmulatorVersion(versionPrefix); + return currentVersion != null && currentVersion.compareTo(minVersion) >= 0; + } + + private Version getInstalledEmulatorVersion(String versionPrefix) + throws IOException, InterruptedException { + Process process = + CommandWrapper.create() + .setCommand(Arrays.asList("gcloud", "version")) + // gcloud redirects all output to stderr while emulators' executables use either + // stdout or stderr with no apparent convention. To be able to properly intercept and + // block waiting for emulators to be ready we redirect everything to stdout + .setRedirectErrorStream() + .start(); + process.waitFor(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream()))) { + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + if (line.startsWith(versionPrefix)) { + String[] lineComponents = line.split(" "); + if (lineComponents.length > 1) { + return Version.fromString(lineComponents[1]); + } + } + } + return null; + } + } + } + + /** + * Utility class to start and run an emulator from a download URL. + */ + protected static class DownloadableEmulatorRunner implements EmulatorRunner { + + private final List commandText; + private final String md5CheckSum; + private final URL downloadUrl; + private final String fileName; + private Process process; + private static final Logger log = Logger.getLogger(DownloadableEmulatorRunner.class.getName()); + + public DownloadableEmulatorRunner(List commandText, URL downloadUrl, + String md5CheckSum) { + this.commandText = commandText; + this.md5CheckSum = md5CheckSum; + this.downloadUrl = downloadUrl; + String[] splitUrl = downloadUrl.toString().split("/"); + this.fileName = splitUrl[splitUrl.length - 1]; + } + + @Override + public boolean isAvailable() { + try { + downloadZipFile(); + return true; + } catch (IOException e) { + return false; + } + } + + @Override + public void start() throws IOException { + Path emulatorPath = downloadEmulator(); + process = CommandWrapper.create() + .setCommand(commandText) + .setDirectory(emulatorPath) + // gcloud redirects all output to stderr while emulators' executables use either stdout + // or stderr with no apparent convention. To be able to properly intercept and block + // waiting for emulators to be ready we redirect everything to stdout + .setRedirectErrorStream() + .start(); + } + + @Override + public void stop() throws InterruptedException { + if (process != null) { + process.destroy(); + process.waitFor(); + } + } + + @Override + public Process getProcess() { + return process; + } + + private Path downloadEmulator() throws IOException { + // Retrieve the file name from the download link + String[] splittedUrl = downloadUrl.toString().split("/"); + String fileName = splittedUrl[splittedUrl.length - 1]; + + // Each run is associated with its own folder that is deleted once test completes. + Path emulatorPath = Files.createTempDirectory(fileName.split("\\.")[0]); + File emulatorFolder = emulatorPath.toFile(); + emulatorFolder.deleteOnExit(); + + File zipFile = downloadZipFile(); + // Unzip the emulator + try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFile))) { + if (log.isLoggable(Level.FINE)) { + log.fine("Unzipping emulator"); + } + ZipEntry entry = zipIn.getNextEntry(); + while (entry != null) { + File filePath = new File(emulatorPath.toFile(), entry.getName()); + if (!entry.isDirectory()) { + extractFile(zipIn, filePath); + } else { + filePath.mkdir(); + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + } + return emulatorPath; + } + + private File downloadZipFile() throws IOException { + // Check if we already have a local copy of the emulator and download it if not. + File zipFile = new File(System.getProperty("java.io.tmpdir"), fileName); + if (!zipFile.exists() || (md5CheckSum != null && !md5CheckSum.equals(md5(zipFile)))) { + if (log.isLoggable(Level.FINE)) { + log.fine("Fetching emulator"); + } + ReadableByteChannel rbc = Channels.newChannel(downloadUrl.openStream()); + try (FileOutputStream fos = new FileOutputStream(zipFile)) { + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + } + } else { + if (log.isLoggable(Level.FINE)) { + log.fine("Using cached emulator"); + } + } + return zipFile; + } + + private void extractFile(ZipInputStream zipIn, File filePath) throws IOException { + try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) { + byte[] bytesIn = new byte[1024]; + int read; + while ((read = zipIn.read(bytesIn)) != -1) { + bos.write(bytesIn, 0, read); + } + } + } + + private static String md5(File zipFile) throws IOException { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + try (InputStream is = new BufferedInputStream(new FileInputStream(zipFile))) { + byte[] bytes = new byte[4 * 1024 * 1024]; + int len; + while ((len = is.read(bytes)) >= 0) { + md5.update(bytes, 0, len); + } + } + return String.format("%032x", new BigInteger(1, md5.digest())); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } + } + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/BlockingProcessStreamReader.java b/google-cloud-core/src/main/java/com/google/cloud/testing/BlockingProcessStreamReader.java new file mode 100644 index 000000000000..988b6593a1d4 --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/BlockingProcessStreamReader.java @@ -0,0 +1,158 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import com.google.common.base.Strings; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class allows to read a process output stream, block until a provided string appears on the + * stream and redirect pertinent error logs to a provided logger. + */ +class BlockingProcessStreamReader extends Thread { + + private static final int STREAM_READER_SLEEP_INTERVAL_IN_MS = 200; + private static final int LOG_LENGTH_LIMIT = 50000; + + private final BufferedReader errorReader; + private final Logger logger; + private StringBuilder currentLog; + private Level currentLogLevel; + private boolean collectionMode; + private volatile boolean terminated; + private final String emulatorTag; + private final Pattern logLinePattern; + + private BlockingProcessStreamReader(String emulator, InputStream stream, String blockUntil, + Logger logger) throws IOException { + super("blocking-process-stream-reader"); + setDaemon(true); + errorReader = new BufferedReader(new InputStreamReader(stream)); + this.logger = logger; + this.emulatorTag = "[" + emulator + "]"; + this.logLinePattern = Pattern.compile("(\\[" + emulator + "\\]\\s)?(\\w+):.*"); + if (!Strings.isNullOrEmpty(blockUntil)) { + String line; + do { + line = errorReader.readLine(); + } while (line != null && !line.contains(blockUntil)); + } + } + + void terminate() throws IOException { + terminated = true; + errorReader.close(); + interrupt(); + } + + @Override + public void run() { + String previousLine = ""; + String nextLine = ""; + while (!terminated) { + try { + if (errorReader.ready()) { + previousLine = nextLine; + nextLine = errorReader.readLine(); + if (nextLine == null) { + terminated = true; + } else { + processLogLine(previousLine, nextLine); + } + } else { + sleep(STREAM_READER_SLEEP_INTERVAL_IN_MS); + } + } catch (IOException e) { + e.printStackTrace(System.err); + } catch (InterruptedException e) { + previousLine = nextLine; + nextLine = null; + break; + } + } + processLogLine(previousLine, firstNonNull(nextLine, "")); + writeLog(); + } + + private void processLogLine(String previousLine, String nextLine) { + // Each log is two lines with the following format: + // [Emulator]? [Date] [Time] [LoggingClass] [method] + // [Emulator]? [LEVEL]: error message + // [Emulator]? more data + // Exceptions and stack traces are included in error stream, separated by a newline + Level nextLogLevel = getLevel(nextLine); + if (nextLogLevel != null) { + writeLog(); + currentLog = new StringBuilder(); + currentLogLevel = nextLogLevel; + collectionMode = true; + } else if (collectionMode) { + if (currentLog.length() > LOG_LENGTH_LIMIT) { + collectionMode = false; + } else if (currentLog.length() == 0) { + // strip level out of the line + currentLog.append(emulatorTag); + currentLog.append(previousLine.split(":", 2)[1]); + currentLog.append(System.getProperty("line.separator")); + } else { + if (!previousLine.startsWith(emulatorTag)) { + currentLog.append(emulatorTag); + currentLog.append(' '); + } + currentLog.append(previousLine); + currentLog.append(System.getProperty("line.separator")); + } + } + } + + private void writeLog() { + if (currentLogLevel != null && currentLog != null && currentLog.length() != 0) { + logger.log(currentLogLevel, currentLog.toString().trim()); + } + } + + private Level getLevel(String line) { + try { + Matcher matcher = logLinePattern.matcher(line); + if (matcher.matches()) { + return Level.parse(matcher.group(2)); + } else { + return null; + } + } catch (IllegalArgumentException e) { + return null; // level wasn't supplied in this log line + } + } + + static BlockingProcessStreamReader start(String emulator, InputStream stream, String blockUntil, + Logger logger) throws IOException { + BlockingProcessStreamReader thread = + new BlockingProcessStreamReader(emulator, stream, blockUntil, logger); + thread.start(); + return thread; + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/CommandWrapper.java b/google-cloud-core/src/main/java/com/google/cloud/testing/CommandWrapper.java new file mode 100644 index 000000000000..eaafca79394c --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/CommandWrapper.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class that executes system commands on both Windows and Unix. + */ +class CommandWrapper { + + private final List prefix; + private List command; + private String nullFilename; + private boolean redirectOutputToNull; + private boolean redirectErrorStream; + private boolean redirectErrorInherit; + private Path directory; + + private CommandWrapper() { + this.prefix = new ArrayList<>(); + if (BaseEmulatorHelper.isWindows()) { + this.prefix.add("cmd"); + this.prefix.add("/C"); + this.nullFilename = "NUL:"; + } else { + this.prefix.add("bash"); + this.nullFilename = "/dev/null"; + } + } + + CommandWrapper setCommand(List command) { + this.command = new ArrayList<>(command.size() + this.prefix.size()); + this.command.addAll(prefix); + this.command.addAll(command); + return this; + } + + CommandWrapper setRedirectOutputToNull() { + this.redirectOutputToNull = true; + return this; + } + + CommandWrapper setRedirectErrorStream() { + this.redirectErrorStream = true; + return this; + } + + CommandWrapper setRedirectErrorInherit() { + this.redirectErrorInherit = true; + return this; + } + + CommandWrapper setDirectory(Path directory) { + this.directory = directory; + return this; + } + + ProcessBuilder getBuilder() { + ProcessBuilder builder = new ProcessBuilder(command); + if (redirectOutputToNull) { + builder.redirectOutput(new File(nullFilename)); + } + if (directory != null) { + builder.directory(directory.toFile()); + } + if (redirectErrorStream) { + builder.redirectErrorStream(true); + } + if (redirectErrorInherit) { + builder.redirectError(ProcessBuilder.Redirect.INHERIT); + } + return builder; + } + + public Process start() throws IOException { + return getBuilder().start(); + } + + static CommandWrapper create() { + return new CommandWrapper(); + } +} diff --git a/google-cloud-core/src/main/java/com/google/cloud/testing/Version.java b/google-cloud-core/src/main/java/com/google/cloud/testing/Version.java new file mode 100644 index 000000000000..bab865eee2ef --- /dev/null +++ b/google-cloud-core/src/main/java/com/google/cloud/testing/Version.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Simplified wrapper for emulator's versions. + */ +class Version implements Comparable { + + private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$"); + + private final int major; + private final int minor; + private final int patch; + + private Version(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + @Override + public int compareTo(Version version) { + int result = major - version.major; + if (result == 0) { + result = minor - version.minor; + if (result == 0) { + result = patch - version.patch; + } + } + return result; + } + + @Override + public String toString() { + return String.format("%d.%d.%d", major, minor, patch); + } + + @Override + public boolean equals(Object other) { + return this == other || other instanceof Version && compareTo((Version) other) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch); + } + + int getMajor() { + return major; + } + + int getMinor() { + return minor; + } + + int getPatch() { + return patch; + } + + static Version fromString(String version) { + Matcher matcher = VERSION_PATTERN.matcher(checkNotNull(version)); + if (matcher.matches()) { + return new Version( + Integer.valueOf(matcher.group(1)), + Integer.valueOf(matcher.group(2)), + Integer.valueOf(matcher.group(3))); + } + throw new IllegalArgumentException("Invalid version format"); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/BaseEmulatorHelperTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/BaseEmulatorHelperTest.java new file mode 100644 index 000000000000..7219dcf8025f --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/BaseEmulatorHelperTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import com.google.api.client.util.Charsets; +import com.google.cloud.ServiceOptions; +import com.google.cloud.testing.BaseEmulatorHelper.EmulatorRunner; +import com.google.common.collect.ImmutableList; + +import org.easymock.EasyMock; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.logging.Logger; + +public class BaseEmulatorHelperTest { + + private static final String BLOCK_UNTIL = "Block until"; + + private static class TestEmulatorHelper extends BaseEmulatorHelper { + + private final List runners; + private final String blockUntil; + + private TestEmulatorHelper(List runners, String blockUntil) { + super("emulator", 1, "project"); + this.runners = runners; + this.blockUntil = blockUntil; + } + + @Override + protected List getEmulatorRunners() { + return runners; + } + + @Override + protected Logger getLogger() { + return null; + } + + @Override + public ServiceOptions getOptions() { + return null; + } + + @Override + public void start() throws IOException, InterruptedException { + startProcess(blockUntil); + } + + @Override + public void stop() throws IOException, InterruptedException { + stopProcess(); + } + + @Override + public void reset() throws IOException { + // do nothing + } + } + + @Test + public void testEmulatorHelper() throws IOException, InterruptedException { + Process process = EasyMock.createStrictMock(Process.class); + InputStream stream = new ByteArrayInputStream(BLOCK_UNTIL.getBytes(Charsets.UTF_8)); + EmulatorRunner emulatorRunner = EasyMock.createStrictMock(EmulatorRunner.class); + EasyMock.expect(process.getInputStream()).andReturn(stream); + EasyMock.expect(emulatorRunner.isAvailable()).andReturn(true); + emulatorRunner.start(); + EasyMock.expectLastCall(); + EasyMock.expect(emulatorRunner.getProcess()).andReturn(process); + emulatorRunner.stop(); + EasyMock.expectLastCall(); + EasyMock.replay(process, emulatorRunner); + TestEmulatorHelper helper = + new TestEmulatorHelper(ImmutableList.of(emulatorRunner), BLOCK_UNTIL); + helper.start(); + helper.stop(); + EasyMock.verify(); + } + + @Test + public void testEmulatorHelperMultipleRunners() throws IOException, InterruptedException { + Process process = EasyMock.createStrictMock(Process.class); + InputStream stream = new ByteArrayInputStream(BLOCK_UNTIL.getBytes(Charsets.UTF_8)); + EmulatorRunner firstRunner = EasyMock.createStrictMock(EmulatorRunner.class); + EmulatorRunner secondRunner = EasyMock.createStrictMock(EmulatorRunner.class); + EasyMock.expect(process.getInputStream()).andReturn(stream); + EasyMock.expect(firstRunner.isAvailable()).andReturn(false); + EasyMock.expect(secondRunner.isAvailable()).andReturn(true); + secondRunner.start(); + EasyMock.expectLastCall(); + EasyMock.expect(secondRunner.getProcess()).andReturn(process); + secondRunner.stop(); + EasyMock.expectLastCall(); + EasyMock.replay(process, secondRunner); + TestEmulatorHelper helper = + new TestEmulatorHelper(ImmutableList.of(firstRunner, secondRunner), BLOCK_UNTIL); + helper.start(); + helper.stop(); + EasyMock.verify(); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/BlockingProcessStreamReaderTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/BlockingProcessStreamReaderTest.java new file mode 100644 index 000000000000..73456f5e3da9 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/BlockingProcessStreamReaderTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import static org.junit.Assert.assertEquals; + +import com.google.api.client.util.Charsets; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BlockingProcessStreamReaderTest { + + private static final String BLOCK_UNTIL = "Dev App Server is now running"; + private static final String OUTPUT = "First Line\n" + + "Second Line\n" + + BLOCK_UNTIL; + private static final String OUTPUT_WITH_LOGS = "First Line\n" + + BLOCK_UNTIL + "\n" + + "Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "INFO: log line 1\n" + + "log line 2\n" + + "Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "FINE: log line 3\n"; + private static final String TAGGED_OUTPUT_WITH_LOGS = "[emulator] First Line\n" + + "[emulator]" + BLOCK_UNTIL + "\n" + + "[emulator] Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "[emulator] INFO: log line 1\n" + + "[emulator] log line 2\n" + + "[emulator] Nov 08, 2016 2:05:44 PM io.netty.buffer.PooledByteBufAllocator \n" + + "[emulator] FINE: log line 3\n"; + + + @Rule + public Timeout globalTimeout = Timeout.seconds(10); + + private static final class TestLogger extends Logger { + + private final Multimap logs = LinkedHashMultimap.create(); + + private TestLogger() { + super("text-logger", null); + } + + public void log(Level level, String msg) { + logs.put(level, msg); + } + + Multimap getLogs() { + return logs; + } + } + + @Test + public void testBlockUntil() throws IOException { + InputStream stream = new ByteArrayInputStream(OUTPUT.getBytes(Charsets.UTF_8)); + BlockingProcessStreamReader thread = + BlockingProcessStreamReader.start("emulator", stream, BLOCK_UNTIL, null); + thread.terminate(); + stream.close(); + } + + @Test + public void testForwardLogEntry() throws IOException, InterruptedException { + TestLogger logger = new TestLogger(); + InputStream stream = new ByteArrayInputStream(OUTPUT_WITH_LOGS.getBytes(Charsets.UTF_8)); + BlockingProcessStreamReader thread = + BlockingProcessStreamReader.start("emulator", stream, BLOCK_UNTIL, logger); + while (logger.getLogs().get(Level.INFO).isEmpty()) { + Thread.sleep(200); + } + assertEquals("[emulator] log line 1" + System.lineSeparator() + "[emulator] log line 2", + logger.getLogs().get(Level.INFO).iterator().next()); + thread.terminate(); + while (logger.getLogs().get(Level.FINE).isEmpty()) { + Thread.sleep(200); + } + assertEquals("[emulator] log line 3", logger.getLogs().get(Level.FINE).iterator().next()); + stream.close(); + } + + @Test + public void testForwardAlreadyTaggedLogs() throws IOException, InterruptedException { + TestLogger logger = new TestLogger(); + InputStream stream = new ByteArrayInputStream(TAGGED_OUTPUT_WITH_LOGS.getBytes(Charsets.UTF_8)); + BlockingProcessStreamReader thread = + BlockingProcessStreamReader.start("emulator", stream, BLOCK_UNTIL, logger); + while (logger.getLogs().get(Level.INFO).isEmpty()) { + Thread.sleep(200); + } + assertEquals("[emulator] log line 1" + System.lineSeparator() + "[emulator] log line 2", + logger.getLogs().get(Level.INFO).iterator().next()); + thread.terminate(); + while (logger.getLogs().get(Level.FINE).isEmpty()) { + Thread.sleep(200); + } + assertEquals("[emulator] log line 3", logger.getLogs().get(Level.FINE).iterator().next()); + stream.close(); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/CommandWrapperTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/CommandWrapperTest.java new file mode 100644 index 000000000000..cfea5a03dd81 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/CommandWrapperTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public class CommandWrapperTest { + + private static final List COMMAND = ImmutableList.of("my", "command"); + private static final List WIN_COMMAND = ImmutableList.of("cmd", "/C", "my", "command"); + private static final List UNIX_COMMAND = ImmutableList.of("bash", "my", "command"); + private static final Path DIRECTORY = Paths.get("my-path"); + private static final File WIN_NULL_FILE = new File("NUL:"); + private static final File UNIX_NULL_FILE = new File("/dev/null"); + + @Test + public void testCommandWrapperCommand() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertNull(processBuilder.directory()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperRedirectErrorStream() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setRedirectErrorStream(); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertNull(processBuilder.directory()); + assertTrue(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperRedirectErrorInherit() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setRedirectErrorInherit(); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertNull(processBuilder.directory()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.INHERIT, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperDirectory() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setDirectory(DIRECTORY); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + } + assertEquals(DIRECTORY, processBuilder.directory().toPath()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } + + @Test + public void testCommandWrapperRedirectOutputToNull() { + CommandWrapper commandWrapper = CommandWrapper.create(); + commandWrapper.setCommand(COMMAND); + commandWrapper.setRedirectOutputToNull(); + ProcessBuilder processBuilder = commandWrapper.getBuilder(); + if (BaseEmulatorHelper.isWindows()) { + assertEquals(WIN_COMMAND, processBuilder.command()); + assertEquals(ProcessBuilder.Redirect.to(WIN_NULL_FILE), processBuilder.redirectOutput()); + } else { + assertEquals(UNIX_COMMAND, processBuilder.command()); + assertEquals(ProcessBuilder.Redirect.to(UNIX_NULL_FILE), processBuilder.redirectOutput()); + } + assertNull(processBuilder.directory()); + assertFalse(processBuilder.redirectErrorStream()); + assertEquals(ProcessBuilder.Redirect.PIPE, processBuilder.redirectError()); + } +} diff --git a/google-cloud-core/src/test/java/com/google/cloud/testing/VersionTest.java b/google-cloud-core/src/test/java/com/google/cloud/testing/VersionTest.java new file mode 100644 index 000000000000..163cf3c3fd39 --- /dev/null +++ b/google-cloud-core/src/test/java/com/google/cloud/testing/VersionTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.testing; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class VersionTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testFromString() { + Version version = Version.fromString("2016.01.13"); + assertEquals(2016, version.getMajor()); + assertEquals(1, version.getMinor()); + assertEquals(13, version.getPatch()); + version = Version.fromString("1.2.0"); + assertEquals(1, version.getMajor()); + assertEquals(2, version.getMinor()); + assertEquals(0, version.getPatch()); + } + + @Test + public void testFromStringWithAlphas() { + thrown.expect(IllegalArgumentException.class); + Version.fromString("2016.01.hello"); + } + + @Test + public void testFromStringMissingPatch() { + thrown.expect(IllegalArgumentException.class); + Version.fromString("2016.01"); + } + + @Test + public void testFromStringMissingMinor() { + thrown.expect(IllegalArgumentException.class); + Version.fromString("2016"); + } + + @Test + public void testFromStringEmpty() { + thrown.expect(IllegalArgumentException.class); + Version.fromString(""); + } + + @Test + public void testFromStringNull() { + thrown.expect(NullPointerException.class); + Version.fromString(null); + } + + @Test + public void testCompare() { + Version version = Version.fromString("2016.01.13"); + Version sameVersion = Version.fromString("2016.01.13"); + Version olderVersion = Version.fromString("2015.12.01"); + Version newerVersion = Version.fromString("2016.08.12"); + assertEquals(0, version.compareTo(sameVersion)); + assertTrue(version.compareTo(olderVersion) > 0); + assertTrue(version.compareTo(newerVersion) < 0); + Version otherVersion = Version.fromString("1.2.0"); + assertTrue(version.compareTo(otherVersion) > 0); + } +} diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java index 99a5067a7827..7fb51f14f122 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/testing/LocalDatastoreHelper.java @@ -16,630 +16,110 @@ package com.google.cloud.datastore.testing; -import static com.google.common.base.MoreObjects.firstNonNull; -import static com.google.common.base.Preconditions.checkArgument; - import com.google.cloud.NoCredentials; import com.google.cloud.RetryParams; import com.google.cloud.datastore.DatastoreOptions; -import com.google.common.base.Strings; -import com.google.common.io.CharStreams; +import com.google.cloud.testing.BaseEmulatorHelper; +import com.google.common.collect.ImmutableList; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.math.BigInteger; -import java.net.HttpURLConnection; import java.net.MalformedURLException; -import java.net.ServerSocket; import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; import java.nio.file.FileVisitResult; import java.nio.file.Files; -import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; -import java.util.Objects; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; /** * Utility to start and stop local Google Cloud Datastore process. */ -public class LocalDatastoreHelper { - private static final Logger log = Logger.getLogger(LocalDatastoreHelper.class.getName()); - private static final Version GCD_VERSION = Version.fromString("1.2.0"); - private static final double DEFAULT_CONSISTENCY = 0.9; - private static final String GCD_BASENAME = "cloud-datastore-emulator-" + GCD_VERSION; - private static final String GCD_FILENAME = GCD_BASENAME + ".zip"; - private static final String MD5_CHECKSUM = "ec2237a0f0ac54964c6bd95e12c73720"; - private static final URL GCD_URL; - private static final String GCLOUD = "gcloud"; - private static final Path INSTALLED_GCD_PATH; - private static final String GCD_VERSION_PREFIX = "cloud-datastore-emulator "; - private static final String PROJECT_ID_PREFIX = "test-project-"; +public class LocalDatastoreHelper extends BaseEmulatorHelper { - private final String projectId; - private Path gcdPath; - private Process startProcess; - private ProcessStreamReader processReader; - private ProcessErrorStreamReader processErrorReader; - private final int port; + private final List emulatorRunners; private final double consistency; + private final Path gcdPath; - static { - INSTALLED_GCD_PATH = installedGcdPath(); - if (INSTALLED_GCD_PATH != null) { - GCD_URL = null; - } else { - try { - GCD_URL = new URL("https://storage.googleapis.com/gcd/tools/" + GCD_FILENAME); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - } - - private static Path installedGcdPath() { - String gcloudExecutableName; - if (isWindows()) { - gcloudExecutableName = GCLOUD + ".cmd"; - } else { - gcloudExecutableName = GCLOUD; - } - Path gcloudPath = executablePath(gcloudExecutableName); - gcloudPath = (gcloudPath == null) ? null : gcloudPath.getParent(); - if (gcloudPath == null) { - if (log.isLoggable(Level.FINE)) { - log.fine("SDK not found"); - } - return null; - } - if (log.isLoggable(Level.FINE)) { - log.fine("SDK found, looking for datastore emulator"); - } - Path installedGcdPath = gcloudPath.resolve("platform").resolve("cloud-datastore-emulator"); - if (Files.exists(installedGcdPath)) { - try { - Version installedVersion = installedGcdVersion(); - if (installedVersion != null && installedVersion.compareTo(GCD_VERSION) >= 0) { - if (log.isLoggable(Level.FINE)) { - log.fine("SDK datastore emulator found"); - } - return installedGcdPath; - } else { - if (log.isLoggable(Level.FINE)) { - log.fine("SDK datastore emulator found but version mismatch"); - } - } - } catch (IOException | InterruptedException | IllegalArgumentException ignore) { - // ignore - } - } - return null; - } - - private static Version installedGcdVersion() throws IOException, InterruptedException { - Process process = - CommandWrapper.create().command("gcloud", "version").redirectErrorStream().start(); - process.waitFor(); - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream()))) { - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - if (line.startsWith(GCD_VERSION_PREFIX)) { - String[] lineComponents = line.split(" "); - if (lineComponents.length > 1) { - return Version.fromString(lineComponents[1]); - } - } - } - return null; - } - } - - private static Path executablePath(String cmd) { - String[] paths = System.getenv("PATH").split(Pattern.quote(File.pathSeparator)); - for (String pathString : paths) { - try { - Path path = Paths.get(pathString); - if (Files.exists(path.resolve(cmd))) { - return path; - } - } catch (InvalidPathException ignore) { - // ignore - } - } - return null; - } - - private static class Version implements Comparable { - - private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)$"); - - final int major; - final int minor; - final int patch; - - Version(int major, int minor, int patch) { - this.major = major; - this.minor = minor; - this.patch = patch; - } - - @Override - public int compareTo(Version version) { - int result = major - version.major; - if (result == 0) { - result = minor - version.minor; - if (result == 0) { - result = patch - version.patch; - } - } - return result; - } - - @Override - public String toString() { - return String.format("%d.%d.%d", major, minor, patch); - } - - @Override - public boolean equals(Object other) { - return this == other || other instanceof Version && compareTo((Version) other) == 0; - } - - @Override - public int hashCode() { - return Objects.hash(major, minor, patch); - } - - static Version fromString(String version) { - Matcher matcher = VERSION_PATTERN.matcher(version); - if (matcher.matches()) { - return new Version( - Integer.valueOf(matcher.group(1)), - Integer.valueOf(matcher.group(2)), - Integer.valueOf(matcher.group(3))); - } - throw new IllegalArgumentException("Invalid version format"); - } - } - - private static class ProcessStreamReader extends Thread { - private final BufferedReader reader; - private volatile boolean terminated; - - ProcessStreamReader(InputStream inputStream, String blockUntil) throws IOException { - super("Local GCD InputStream reader"); - setDaemon(true); - reader = new BufferedReader(new InputStreamReader(inputStream)); - if (!Strings.isNullOrEmpty(blockUntil)) { - String line; - do { - line = reader.readLine(); - } while (line != null && !line.contains(blockUntil)); - } - } - - void terminate() throws IOException { - terminated = true; - reader.close(); - } - - @Override - public void run() { - while (!terminated) { - try { - String line = reader.readLine(); - if (line == null) { - terminated = true; - } - } catch (IOException e) { - // ignore - } - } - } - - public static ProcessStreamReader start(InputStream inputStream, String blockUntil) - throws IOException { - ProcessStreamReader thread = new ProcessStreamReader(inputStream, blockUntil); - thread.start(); - return thread; - } - } - - private static class ProcessErrorStreamReader extends Thread { - private static final int LOG_LENGTH_LIMIT = 50000; - private static final String GCD_LOGGING_CLASS = - "com.google.apphosting.client.serviceapp.BaseApiServlet"; - - private final BufferedReader errorReader; - private StringBuilder currentLog; - private Level currentLogLevel; - private boolean collectionMode; - private volatile boolean terminated; - - ProcessErrorStreamReader(InputStream errorStream) { - super("Local GCD ErrorStream reader"); - setDaemon(true); - errorReader = new BufferedReader(new InputStreamReader(errorStream)); - } - - void terminate() throws IOException { - terminated = true; - errorReader.close(); - } - - @Override - public void run() { - String previousLine = ""; - String nextLine = ""; - while (!terminated) { - try { - previousLine = nextLine; - nextLine = errorReader.readLine(); - if (nextLine == null) { - terminated = true; - } else { - processLogLine(previousLine, nextLine); - } - } catch (IOException e) { - // ignore - } - } - processLogLine(previousLine, firstNonNull(nextLine, "")); - writeLog(currentLogLevel, currentLog); - } - - private void processLogLine(String previousLine, String nextLine) { - // Each gcd log is two lines with the following format: - // [Date] [Time] [GCD_LOGGING_CLASS] [method] - // [LEVEL]: error message - // Exceptions and stack traces are included in gcd error stream, separated by a newline - Level nextLogLevel = getLevel(nextLine); - if (nextLogLevel != null) { - writeLog(currentLogLevel, currentLog); - currentLog = new StringBuilder(); - currentLogLevel = nextLogLevel; - collectionMode = previousLine.contains(GCD_LOGGING_CLASS); - } else if (collectionMode) { - if (currentLog.length() > LOG_LENGTH_LIMIT) { - collectionMode = false; - } else if (currentLog.length() == 0) { - // strip level out of the line - currentLog.append("GCD"); - currentLog.append(previousLine.split(":", 2)[1]); - currentLog.append(System.getProperty("line.separator")); - } else { - currentLog.append(previousLine); - currentLog.append(System.getProperty("line.separator")); - } - } - } - - private static void writeLog(Level level, StringBuilder msg) { - if (level != null && msg != null && msg.length() != 0) { - log.log(level, msg.toString().trim()); - } - } - - private static Level getLevel(String line) { - try { - return Level.parse(line.split(":")[0]); - } catch (IllegalArgumentException e) { - return null; // level wasn't supplied in this log line - } - } - - public static ProcessErrorStreamReader start(InputStream errorStream) { - ProcessErrorStreamReader thread = new ProcessErrorStreamReader(errorStream); - thread.start(); - return thread; - } - } - - private static class CommandWrapper { - private final List prefix; - private List command; - private String nullFilename; - private boolean redirectOutputToNull; - private boolean redirectErrorStream; - private boolean redirectErrorInherit; - private Path directory; - - private CommandWrapper() { - this.prefix = new ArrayList<>(); - if (isWindows()) { - this.prefix.add("cmd"); - this.prefix.add("/C"); - this.nullFilename = "NUL:"; - } else { - this.prefix.add("bash"); - this.nullFilename = "/dev/null"; - } - } - - public CommandWrapper command(String... command) { - this.command = new ArrayList<>(command.length + this.prefix.size()); - this.command.addAll(prefix); - this.command.addAll(Arrays.asList(command)); - return this; - } - - public CommandWrapper redirectOutputToNull() { - this.redirectOutputToNull = true; - return this; - } - - public CommandWrapper redirectErrorStream() { - this.redirectErrorStream = true; - return this; - } - - public CommandWrapper redirectErrorInherit() { - this.redirectErrorInherit = true; - return this; - } - - public CommandWrapper directory(Path directory) { - this.directory = directory; - return this; - } - - public ProcessBuilder builder() { - ProcessBuilder builder = new ProcessBuilder(command); - if (redirectOutputToNull) { - builder.redirectOutput(new File(nullFilename)); - } - if (directory != null) { - builder.directory(directory.toFile()); - } - if (redirectErrorStream) { - builder.redirectErrorStream(true); - } - if (redirectErrorInherit) { - builder.redirectError(ProcessBuilder.Redirect.INHERIT); - } - return builder; - } - - public Process start() throws IOException { - return builder().start(); - } - - public static CommandWrapper create() { - return new CommandWrapper(); - } - } - - private void downloadGcd() throws IOException { - // check if we already have a local copy of the gcd utility and download it if not. - File gcdZipFile = new File(System.getProperty("java.io.tmpdir"), GCD_FILENAME); - if (!gcdZipFile.exists() || !MD5_CHECKSUM.equals(md5(gcdZipFile))) { - if (log.isLoggable(Level.FINE)) { - log.fine("Fetching datastore emulator"); - } - ReadableByteChannel rbc = Channels.newChannel(GCD_URL.openStream()); - try (FileOutputStream fos = new FileOutputStream(gcdZipFile)) { - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - } - } else { - if (log.isLoggable(Level.FINE)) { - log.fine("Using cached datastore emulator"); - } - } - // unzip the gcd - try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(gcdZipFile))) { - if (log.isLoggable(Level.FINE)) { - log.fine("Unzipping datastore emulator"); - } - ZipEntry entry = zipIn.getNextEntry(); - while (entry != null) { - File filePath = new File(gcdPath.toFile(), entry.getName()); - if (!entry.isDirectory()) { - extractFile(zipIn, filePath); - } else { - filePath.mkdir(); - } - zipIn.closeEntry(); - entry = zipIn.getNextEntry(); - } - } - } + // Gcloud emulator settings + private static final String GCLOUD_CMD_TEXT = "gcloud beta emulators datastore start"; + private static final String GCLOUD_CMD_PORT_FLAG = "--host-port="; + private static final String VERSION_PREFIX = "cloud-datastore-emulator "; + private static final String MIN_VERSION = "1.2.0"; - private void startGcd(Path executablePath, double consistency) - throws IOException, InterruptedException { - // cleanup any possible data for the same project - File datasetFolder = new File(gcdPath.toFile(), projectId); - deleteRecurse(datasetFolder.toPath()); - - // Get path to cmd executable - Path gcdAbsolutePath; - if (isWindows()) { - gcdAbsolutePath = executablePath.toAbsolutePath().resolve("cloud_datastore_emulator.cmd"); - } else { - gcdAbsolutePath = executablePath.toAbsolutePath().resolve("cloud_datastore_emulator"); - } + // Downloadable emulator settings + private static final String BIN_NAME = "cloud-datastore-emulator/cloud_datastore_emulator"; + private static final String FILENAME = "cloud-datastore-emulator-" + MIN_VERSION + ".zip"; + private static final String MD5_CHECKSUM = "ec2237a0f0ac54964c6bd95e12c73720"; + private static final String BIN_CMD_PORT_FLAG = "--port="; + private static final URL EMULATOR_URL; - // create the datastore for the project - if (log.isLoggable(Level.FINE)) { - log.log(Level.FINE, "Creating datastore for the project: {0}", projectId); - } - Process createProcess = - CommandWrapper.create() - .command(gcdAbsolutePath.toString(), "create", projectId) - .redirectErrorInherit() - .directory(gcdPath) - .redirectOutputToNull() - .start(); - createProcess.waitFor(); + // Common settings + private static final String CONSISTENCY_FLAG = "--consistency="; + private static final double DEFAULT_CONSISTENCY = 0.9; - // start the datastore for the project - if (log.isLoggable(Level.FINE)) { - log.log(Level.FINE, "Starting datastore emulator for the project: {0}", projectId); - } - startProcess = - CommandWrapper.create() - .command(gcdAbsolutePath.toString(), "start", "--testing", - "--port=" + Integer.toString(port), "--consistency=" + Double.toString(consistency), - projectId) - .directory(gcdPath) - .start(); - processReader = ProcessStreamReader.start(startProcess.getInputStream(), - "Dev App Server is now running"); - processErrorReader = ProcessErrorStreamReader.start(startProcess.getErrorStream()); - } + private static final Logger LOGGER = Logger.getLogger(LocalDatastoreHelper.class.getName()); - private static String md5(File gcdZipFile) throws IOException { + static { try { - MessageDigest md5 = MessageDigest.getInstance("MD5"); - try (InputStream is = new BufferedInputStream(new FileInputStream(gcdZipFile))) { - byte[] bytes = new byte[4 * 1024 * 1024]; - int len; - while ((len = is.read(bytes)) >= 0) { - md5.update(bytes, 0, len); - } - } - return String.format("%032x", new BigInteger(1, md5.digest())); - } catch (NoSuchAlgorithmException e) { - throw new IOException(e); - } - } - - private static boolean isWindows() { - return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); - } - - private static void extractFile(ZipInputStream zipIn, File filePath) throws IOException { - try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) { - byte[] bytesIn = new byte[1024]; - int read; - while ((read = zipIn.read(bytesIn)) != -1) { - bos.write(bytesIn, 0, read); - } + EMULATOR_URL = new URL("http://storage.googleapis.com/gcd/tools/" + FILENAME); + } catch (MalformedURLException ex) { + throw new IllegalStateException(ex); } } - public static boolean sendQuitRequest(int port) { - StringBuilder result = new StringBuilder(); - String shutdownMsg = "Shutting down local server"; + private LocalDatastoreHelper(double consistency) { + super("datastore", BaseEmulatorHelper.findAvailablePort(DEFAULT_PORT), + PROJECT_ID_PREFIX + UUID.randomUUID().toString()); + Path tmpDirectory = null; try { - URL url = new URL("http", "localhost", port, "/_ah/admin/quit"); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("POST"); - con.setDoOutput(true); - con.setDoInput(true); - OutputStream out = con.getOutputStream(); - out.write("".getBytes()); - out.flush(); - InputStream in = con.getInputStream(); - int currByte = 0; - while ((currByte = in.read()) != -1 && result.length() < shutdownMsg.length()) { - result.append(((char) currByte)); - } - } catch (IOException ignore) { - // ignore - } - return result.toString().startsWith(shutdownMsg); - } - - public String sendPostRequest(String request) throws IOException { - URL url = new URL("http", "localhost", this.port, request); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("POST"); - con.setDoOutput(true); - OutputStream out = con.getOutputStream(); - out.write("".getBytes()); - out.flush(); - - InputStream in = con.getInputStream(); - String response = CharStreams.toString(new InputStreamReader(con.getInputStream())); - in.close(); - return response; - } - - /** - * Quit the local emulator and related local service. - */ - public void stop() throws IOException, InterruptedException { - sendQuitRequest(port); - if (processReader != null) { - processReader.terminate(); - processErrorReader.terminate(); - startProcess.destroy(); - startProcess.waitFor(); + tmpDirectory = Files.createTempDirectory("gcd"); + } catch (IOException ex) { + getLogger().log(Level.WARNING, "Failed to create temporary directory"); } + this.gcdPath = tmpDirectory; + this.consistency = consistency; + String binName = BIN_NAME; + if (isWindows()) { + binName = BIN_NAME.replace("/", "\\"); + } + List gcloudCommand = new ArrayList<>(Arrays.asList(GCLOUD_CMD_TEXT.split(" "))); + gcloudCommand.add(GCLOUD_CMD_PORT_FLAG + "localhost:" + getPort()); + gcloudCommand.add(CONSISTENCY_FLAG + consistency); + gcloudCommand.add("--no-store-on-disk"); + GcloudEmulatorRunner gcloudRunner = + new GcloudEmulatorRunner(gcloudCommand, VERSION_PREFIX, MIN_VERSION); + List binCommand = new ArrayList<>(Arrays.asList(binName, "start")); + binCommand.add("--testing"); + binCommand.add(BIN_CMD_PORT_FLAG + getPort()); + binCommand.add(CONSISTENCY_FLAG + consistency); if (gcdPath != null) { - deleteRecurse(gcdPath); + gcloudCommand.add("--data-dir=" + gcdPath.toString()); } + DownloadableEmulatorRunner downloadRunner = + new DownloadableEmulatorRunner(binCommand, EMULATOR_URL, MD5_CHECKSUM); + emulatorRunners = ImmutableList.of(gcloudRunner, downloadRunner); } - /** - * Reset the internal state of the emulator. - */ - public void reset() throws IOException { - sendPostRequest("/reset"); + @Override + protected List getEmulatorRunners() { + return emulatorRunners; } - private static void deleteRecurse(Path path) throws IOException { - if (path == null || !Files.exists(path)) { - return; - } - Files.walkFileTree(path, new SimpleFileVisitor() { - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - }); - } - - private LocalDatastoreHelper(double consistency) { - checkArgument(consistency >= 0.0 && consistency <= 1.0, "Consistency must be between 0 and 1"); - projectId = PROJECT_ID_PREFIX + UUID.randomUUID().toString(); - this.consistency = consistency; - this.port = findAvailablePort(); - } - - private static int findAvailablePort() { - try (ServerSocket tempSocket = new ServerSocket(0)) { - return tempSocket.getLocalPort(); - } catch (IOException e) { - return -1; - } + @Override + protected Logger getLogger() { + return LOGGER; } private DatastoreOptions.Builder optionsBuilder() { return DatastoreOptions.newBuilder() - .setProjectId(projectId) - .setHost("localhost:" + Integer.toString(port)) + .setProjectId(getProjectId()) + .setHost(DEFAULT_HOST + ":" + Integer.toString(getPort())) .setCredentials(NoCredentials.getInstance()) .setRetryParams(RetryParams.noRetries()); } @@ -657,6 +137,7 @@ public DatastoreOptions options() { * Returns a {@link DatastoreOptions} instance that sets the host to use the Datastore emulator on * localhost. */ + @Override public DatastoreOptions getOptions() { return optionsBuilder().build(); } @@ -683,14 +164,7 @@ public DatastoreOptions getOptions(String namespace) { */ @Deprecated public String projectId() { - return projectId; - } - - /** - * Returns the project ID associated with this local Datastore emulator. - */ - public String getProjectId() { - return projectId; + return getProjectId(); } /** @@ -718,8 +192,7 @@ public double getConsistency() { * consistent. */ public static LocalDatastoreHelper create(double consistency) { - LocalDatastoreHelper helper = new LocalDatastoreHelper(consistency); - return helper; + return new LocalDatastoreHelper(consistency); } /** @@ -733,29 +206,47 @@ public static LocalDatastoreHelper create() { } /** - * Starts the local Datastore emulator. Leftover data from previous uses of the emulator will be - * removed. - * - * @throws InterruptedException if emulator-related tasks are interrupted - * @throws IOException if there are socket exceptions or issues creating/deleting the temporary - * data folder + * Starts the local Datastore emulator through {@code gcloud}, downloads and caches the zip file + * if user does not have {@code gcloud} or a compatible emulator version installed. */ + @Override public void start() throws IOException, InterruptedException { - // send a quick request in case we have a hanging process from a previous run - sendQuitRequest(port); - // Each run is associated with its own folder that is deleted once test completes. - gcdPath = Files.createTempDirectory("gcd"); - File gcdFolder = gcdPath.toFile(); - gcdFolder.deleteOnExit(); + String blockUntilOutput = "Dev App Server is now running"; + startProcess(blockUntilOutput); + } - Path gcdExecutablePath; - // If cloud is available we use it, otherwise we download and start gcd - if (INSTALLED_GCD_PATH == null) { - downloadGcd(); - gcdExecutablePath = gcdPath.resolve("cloud-datastore-emulator"); - } else { - gcdExecutablePath = INSTALLED_GCD_PATH; + /** + * Reset the internal state of the Datastore emulator. + */ + public void reset() throws IOException { + sendPostRequest("/reset"); + } + + /** + * Stops the Datastore emulator. + */ + public void stop() throws IOException, InterruptedException { + sendPostRequest("/shutdown"); + stopProcess(); + deleteRecursively(gcdPath); + } + + private static void deleteRecursively(Path path) throws IOException { + if (path == null || !Files.exists(path)) { + return; } - startGcd(gcdExecutablePath, consistency); + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); } } diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/spi/v2/testing/LocalLoggingHelper.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/spi/v2/testing/LocalLoggingHelper.java index 015b31ac6e1c..cfd249c16644 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/spi/v2/testing/LocalLoggingHelper.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/spi/v2/testing/LocalLoggingHelper.java @@ -39,7 +39,7 @@ public class LocalLoggingHelper { private final LocalLoggingImpl loggingImpl; /** - * Constructs a new LocalPubsubHelper. The method start() must + * Constructs a new LocalLoggingHelper. The method start() must * be called before it is used. */ public LocalLoggingHelper(String addressString) { diff --git a/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/testing/LocalPubsubHelper.java b/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/testing/LocalPubSubHelper.java similarity index 59% rename from google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/testing/LocalPubsubHelper.java rename to google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/testing/LocalPubSubHelper.java index 93cc35e57b87..3ad7e9151059 100644 --- a/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/testing/LocalPubsubHelper.java +++ b/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/testing/LocalPubSubHelper.java @@ -16,12 +16,11 @@ package com.google.cloud.pubsub.testing; -import com.google.api.gax.testing.DownloadableEmulatorRunner; -import com.google.api.gax.testing.GCloudEmulatorRunner; -import com.google.api.gax.testing.LocalServiceHelper; import com.google.cloud.NoCredentials; import com.google.cloud.RetryParams; import com.google.cloud.pubsub.PubSubOptions; +import com.google.cloud.testing.BaseEmulatorHelper; +import com.google.common.collect.ImmutableList; import io.grpc.ManagedChannel; import io.grpc.netty.NegotiationType; @@ -33,25 +32,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; import java.util.UUID; +import java.util.logging.Logger; /** * A class that runs a Pubsub emulator instance for use in tests. */ -public class LocalPubsubHelper { +public class LocalPubSubHelper extends BaseEmulatorHelper { - private final int port; - private final LocalServiceHelper serviceHelper; - private final String projectId; + private final List emulatorRunners; - // Local server settings - private static final int DEFAULT_PORT = 8080; - private static final String DEFAULT_HOST = "localhost"; - private static final URL EMULATOR_URL; - private static final String PROJECT_ID_PREFIX = "test-project-"; - - // GCloud emulator settings + // Gcloud emulator settings private static final String GCLOUD_CMD_TEXT = "gcloud beta emulators pubsub start"; private static final String GCLOUD_CMD_PORT_FLAG = "--host-port="; private static final String VERSION_PREFIX = "pubsub-emulator"; @@ -62,6 +53,9 @@ public class LocalPubsubHelper { private static final String FILENAME = "pubsub-emulator-20160113-2.zip"; private static final String BIN_NAME = "pubsub-emulator/bin/cloud-pubsub-fake"; private static final String MD5_CHECKSUM = "20943e9defa300f2de101568459c133d"; + private static final URL EMULATOR_URL; + + private static final Logger LOGGER = Logger.getLogger(LocalPubSubHelper.class.getName()); static { try { @@ -71,95 +65,87 @@ public class LocalPubsubHelper { } } - private static boolean isWindows() { - return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); - } - - private LocalPubsubHelper() { - port = LocalServiceHelper.findAvailablePort(DEFAULT_PORT); + private LocalPubSubHelper() { + super("pubsub", BaseEmulatorHelper.findAvailablePort(DEFAULT_PORT), + PROJECT_ID_PREFIX + UUID.randomUUID().toString()); String binName = BIN_NAME; if (isWindows()) { binName = BIN_NAME.replace("/", "\\"); } List gcloudCommand = new ArrayList<>(Arrays.asList(GCLOUD_CMD_TEXT.split(" "))); - gcloudCommand.add(GCLOUD_CMD_PORT_FLAG + "localhost:" + port); - GCloudEmulatorRunner gcloudRunner = - new GCloudEmulatorRunner(gcloudCommand, VERSION_PREFIX, MIN_VERSION); + gcloudCommand.add(GCLOUD_CMD_PORT_FLAG + "localhost:" + getPort()); + GcloudEmulatorRunner gcloudRunner = + new GcloudEmulatorRunner(gcloudCommand, VERSION_PREFIX, MIN_VERSION); DownloadableEmulatorRunner downloadRunner = - new DownloadableEmulatorRunner(Arrays.asList(binName, BIN_CMD_PORT_FLAG + port), + new DownloadableEmulatorRunner(Arrays.asList(binName, BIN_CMD_PORT_FLAG + getPort()), EMULATOR_URL, MD5_CHECKSUM); - serviceHelper = new LocalServiceHelper(Arrays.asList(gcloudRunner, downloadRunner), port); - projectId = PROJECT_ID_PREFIX + UUID.randomUUID().toString(); + emulatorRunners = ImmutableList.of(gcloudRunner, downloadRunner); } - /** - * Constructs a new {@code LocalPubsubHelper}. The method {@code start()} must be called before it - * is used. - */ - public static LocalPubsubHelper create() { - return new LocalPubsubHelper(); + @Override + protected List getEmulatorRunners() { + return emulatorRunners; + } + + @Override + protected Logger getLogger() { + return LOGGER; } /** - * Start the local pubsub emulator through gcloud, download the zip file if user does not have - * gcloud installed. - * - * @throws InterruptedException - * @throws IOException + * Creates a channel for making requests to the in-memory service. */ - public void start() throws IOException, InterruptedException { - String blockUntilOutput = "Server started, listening on " + port; - serviceHelper.start(blockUntilOutput); + public ManagedChannel createChannel() { + return NettyChannelBuilder.forAddress(DEFAULT_HOST, getPort()) + .negotiationType(NegotiationType.PLAINTEXT) + .build(); } /** - * Reset the internal state of the emulator. - * - * @throws IOException + * Returns a {@link PubSubOptions} instance that sets the host to use the PubSub emulator on + * localhost. */ - public void reset() throws IOException { - this.serviceHelper.sendPostRequest("/reset"); + @Override + public PubSubOptions getOptions() { + return PubSubOptions.newBuilder() + .setProjectId(getProjectId()) + .setHost(DEFAULT_HOST + ":" + getPort()) + .setCredentials(NoCredentials.getInstance()) + .setRetryParams(RetryParams.noRetries()) + .build(); } /** - * Quit the local emulator and related local service. - * - * @throws InterruptedException - * @throws IOException + * Constructs a new {@code LocalPubSubHelper}. The method {@code start()} must be called before it + * is used. */ - public void stop() throws IOException, InterruptedException { - this.serviceHelper.sendPostRequest("/shutdown"); - this.serviceHelper.stop(); + public static LocalPubSubHelper create() { + return new LocalPubSubHelper(); } /** - * Creates a channel for making requests to the in-memory service. + * Starts the local PubSub emulator through {@code gcloud}, downloads and caches the zip file + * if user does not have {@code gcloud} or a compatible emulator version installed. */ - public ManagedChannel createChannel() { - return NettyChannelBuilder.forAddress(DEFAULT_HOST, port) - .negotiationType(NegotiationType.PLAINTEXT) - .build(); + public void start() throws IOException, InterruptedException { + String blockUntilOutput = "Server started, listening on " + getPort(); + startProcess(blockUntilOutput); } /** - * Returns a {@link PubSubOptions} instance that sets the host to use the PubSub emulator on - * localhost. + * Reset the internal state of the PubSub emulator. */ - @Deprecated - public PubSubOptions options() { - return getOptions(); + @Override + public void reset() throws IOException { + sendPostRequest("/reset"); } /** - * Returns a {@link PubSubOptions} instance that sets the host to use the PubSub emulator on - * localhost. + * Stops the PubSub emulator and related local service. */ - public PubSubOptions getOptions() { - return PubSubOptions.newBuilder() - .setProjectId(projectId) - .setHost("localhost:" + port) - .setCredentials(NoCredentials.getInstance()) - .setRetryParams(RetryParams.noRetries()) - .build(); + @Override + public void stop() throws IOException, InterruptedException { + sendPostRequest("/shutdown"); + stopProcess(); } } diff --git a/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/LocalSystemTest.java b/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/LocalSystemTest.java index 391ad1e937c4..adcf45eedf20 100644 --- a/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/LocalSystemTest.java +++ b/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/LocalSystemTest.java @@ -16,7 +16,7 @@ package com.google.cloud.pubsub; -import com.google.cloud.pubsub.testing.LocalPubsubHelper; +import com.google.cloud.pubsub.testing.LocalPubSubHelper; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -25,7 +25,7 @@ public class LocalSystemTest extends BaseSystemTest { - private static LocalPubsubHelper pubsubHelper; + private static LocalPubSubHelper pubsubHelper; private static PubSub pubsub; @Override @@ -40,7 +40,7 @@ protected String formatForTest(String resourceName) { @BeforeClass public static void startServer() throws IOException, InterruptedException { - pubsubHelper = LocalPubsubHelper.create(); + pubsubHelper = LocalPubSubHelper.create(); pubsubHelper.start(); pubsub = pubsubHelper.getOptions().getService(); } diff --git a/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/spi/v1/PublisherApiTest.java b/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/spi/v1/PublisherApiTest.java index e2a9fd3339df..b0e6e06f3f7e 100644 --- a/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/spi/v1/PublisherApiTest.java +++ b/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/spi/v1/PublisherApiTest.java @@ -15,7 +15,7 @@ package com.google.cloud.pubsub.spi.v1; import com.google.api.gax.grpc.BundlingSettings; -import com.google.cloud.pubsub.testing.LocalPubsubHelper; +import com.google.cloud.pubsub.testing.LocalPubSubHelper; import com.google.protobuf.ByteString; import com.google.pubsub.v1.PubsubMessage; import com.google.pubsub.v1.PullResponse; @@ -38,14 +38,14 @@ import java.util.List; public class PublisherApiTest { - private static LocalPubsubHelper pubsubHelper; + private static LocalPubSubHelper pubsubHelper; private PublisherApi publisherApi; private PublisherApi bundledPublisherApi; private SubscriberApi subscriberApi; @BeforeClass public static void startServer() throws IOException, InterruptedException { - pubsubHelper = LocalPubsubHelper.create(); + pubsubHelper = LocalPubSubHelper.create(); pubsubHelper.start(); } diff --git a/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/testing/LocalPubSubHelperTest.java b/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/testing/LocalPubSubHelperTest.java new file mode 100644 index 000000000000..a0c1c1965d40 --- /dev/null +++ b/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/testing/LocalPubSubHelperTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * 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.cloud.pubsub.testing; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.NoCredentials; +import com.google.cloud.pubsub.PubSub; +import com.google.cloud.pubsub.PubSubException; +import com.google.cloud.pubsub.PubSubOptions; +import com.google.cloud.pubsub.TopicInfo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.IOException; + +public class LocalPubSubHelperTest { + + private static final String PROJECT_ID_PREFIX = "test-project-"; + private static final String TOPIC = "topic"; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testCreate() { + LocalPubSubHelper helper = LocalPubSubHelper.create(); + assertTrue(helper.getProjectId().startsWith(PROJECT_ID_PREFIX)); + } + + @Test + public void testOptions() { + LocalPubSubHelper helper = LocalPubSubHelper.create(); + PubSubOptions options = helper.getOptions(); + assertTrue(options.getProjectId().startsWith(PROJECT_ID_PREFIX)); + assertTrue(options.getHost().startsWith("localhost:")); + assertSame(NoCredentials.getInstance(), options.getCredentials()); + } + + @Test + public void testStartStopReset() throws IOException, InterruptedException { + LocalPubSubHelper helper = LocalPubSubHelper.create(); + helper.start(); + PubSub pubsub = helper.getOptions().getService(); + pubsub.create(TopicInfo.of(TOPIC)); + assertNotNull(pubsub.getTopic(TOPIC)); + helper.reset(); + assertNull(pubsub.getTopic(TOPIC)); + helper.stop(); + thrown.expect(PubSubException.class); + pubsub.getTopic(TOPIC); + } +}