Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BaseEmulatorHelper class and tests, refactor helper classes #1377

Merged
merged 2 commits into from
Nov 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> prefix;
private List<String> 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<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Version> {

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");
}
}
Loading