/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* 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 org.jetbrains.osgi.bnd.run;
import aQute.bnd.build.ProjectLauncher;
import aQute.bnd.build.ProjectTester;
import aQute.bnd.service.EclipseJUnitTester;
import com.intellij.execution.CantRunException;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.JavaCommandLineState;
import com.intellij.execution.configurations.JavaParameters;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.testframework.JavaTestLocator;
import com.intellij.execution.testframework.TestConsoleProperties;
import com.intellij.execution.testframework.sm.SMCustomMessagesParsing;
import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil;
import com.intellij.execution.testframework.sm.runner.GeneralTestEventsProcessor;
import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter;
import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties;
import com.intellij.execution.testframework.sm.runner.SMTestLocator;
import com.intellij.execution.testframework.sm.runner.events.*;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.intellij.openapi.util.Pair.pair;
import static com.intellij.util.io.URLUtil.SCHEME_SEPARATOR;
import static org.osmorc.i18n.OsmorcBundle.message;
public class BndTestState extends JavaCommandLineState {
private static final String TEST_FRAMEWORK_NAME = "Bnd-OSGi-JUnit";
private static final Logger LOG = Logger.getInstance(BndTestState.class);
private final BndRunConfigurationBase.Test myConfiguration;
private final ProjectTester myTester;
private final ServerSocket mySocket;
public BndTestState(@NotNull ExecutionEnvironment environment, @NotNull BndRunConfigurationBase.Test configuration) throws ExecutionException {
super(environment);
myConfiguration = configuration;
File runFile = new File(myConfiguration.bndRunFile);
if (!runFile.isFile()) {
throw new CantRunException(message("bnd.run.configuration.invalid", runFile));
}
try {
String title = message("bnd.run.configuration.progress");
myTester = ProgressManager.getInstance().run(new Task.WithResult<ProjectTester, Exception>(myConfiguration.getProject(), title, false) {
@Override
protected ProjectTester compute(@NotNull ProgressIndicator indicator) throws Exception {
indicator.setIndeterminate(true);
return BndLaunchUtil.getRun(runFile).getProjectTester();
}
});
}
catch (Throwable t) {
LOG.info(t);
throw new CantRunException(message("bnd.run.configuration.cannot.run", runFile, BndLaunchUtil.message(t)));
}
//noinspection InstanceofIncompatibleInterface
if (!(myTester instanceof EclipseJUnitTester)) {
throw new CantRunException(message("bnd.test.runner.unsupported", myTester.getClass().getName()));
}
try {
mySocket = new ServerSocket(0);
//noinspection CastToIncompatibleInterface
((EclipseJUnitTester)myTester).setPort(mySocket.getLocalPort());
}
catch (Exception e) {
LOG.info(e);
throw new CantRunException(message("bnd.test.cannot.run", e.getMessage()));
}
try {
myTester.prepare();
}
catch (Exception e) {
LOG.info(e);
throw new CantRunException(message("bnd.run.configuration.cannot.run", runFile, e.getMessage()));
}
}
@Override
protected JavaParameters createJavaParameters() throws ExecutionException {
ProjectLauncher launcher = myTester.getProjectLauncher();
return BndLaunchUtil.createJavaParameters(myConfiguration, launcher);
}
@Nullable
@Override
protected ConsoleView createConsole(@NotNull Executor executor) throws ExecutionException {
TestConsoleProperties consoleProperties = new MyTestConsoleProperties(this, executor);
return SMTestRunnerConnectionUtil.createConsole(TEST_FRAMEWORK_NAME, consoleProperties);
}
@NotNull
@Override
protected OSProcessHandler startProcess() throws ExecutionException {
OSProcessHandler processHandler = super.startProcess();
processHandler.addProcessListener(new ProcessAdapter() {
@Override
public void processTerminated(ProcessEvent event) {
cleanup();
}
});
return processHandler;
}
private void cleanup() {
try {
mySocket.close();
FileUtil.delete(myTester.getReportDir());
}
catch (Exception e) {
LOG.error(e);
}
}
private static class MyTestConsoleProperties extends SMTRunnerConsoleProperties implements SMCustomMessagesParsing {
private final ServerSocket mySocket;
public MyTestConsoleProperties(@NotNull BndTestState runProfile, @NotNull Executor executor) {
super(runProfile.myConfiguration, TEST_FRAMEWORK_NAME, executor);
mySocket = runProfile.mySocket;
setPrintTestingStartedTime(false);
}
@NotNull
@Override
public OutputToGeneralTestEventsConverter createTestEventsConverter(@NotNull String testFrameworkName,
@NotNull TestConsoleProperties consoleProperties) {
return new MyProcessOutputConsumer(testFrameworkName, consoleProperties, mySocket);
}
@Override
public SMTestLocator getTestLocator() {
return JavaTestLocator.INSTANCE;
}
}
private static class MyProcessOutputConsumer extends OutputToGeneralTestEventsConverter {
private final ServerSocket mySocket;
private GeneralTestEventsProcessor myProcessor;
private final Object myTestLock = new Object();
private String myCurrentTest = null;
public MyProcessOutputConsumer(@NotNull String testFrameworkName,
@NotNull TestConsoleProperties consoleProperties,
@NotNull ServerSocket socket) {
super(testFrameworkName, consoleProperties);
mySocket = socket;
}
@Override
public void setProcessor(GeneralTestEventsProcessor processor) {
myProcessor = processor;
startProtocolListener();
}
private void startProtocolListener() {
ApplicationManager.getApplication().executeOnPooledThread(() -> {
try (Socket socket = mySocket.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
reader.lines().forEach(this::processEventLine);
}
catch (IOException e) {
LOG.debug(e);
}
});
}
@Override
public void process(String text, Key outputType) {
GeneralTestEventsProcessor processor = myProcessor;
synchronized (myTestLock) {
if (myCurrentTest != null) {
processor.onTestOutput(new TestOutputEvent(myCurrentTest, text, outputType == ProcessOutputTypes.STDOUT));
}
else {
processor.onUncapturedOutput(text, outputType);
}
}
}
@Override
public void flushBufferBeforeTerminating() { }
@Override
public void dispose() {
myProcessor = null;
}
@SuppressWarnings("SpellCheckingInspection")
private static final class Proto {
private static final String INIT = "%TESTC ";
private static final String TREE = "%TSTTREE";
private static final String TEST = "%TESTS ";
private static final String ERROR = "%ERROR ";
private static final String FAILED = "%FAILED ";
private static final String TRACE = "%TRACES ";
private static final String TRACE_END = "%TRACEE ";
private static final String TEST_END = "%TESTE ";
private static final String DONE = "%RUNTIME";
}
private static final int EVENT_TYPE_LEN = 8;
private int myTestCount = 0;
private String myCurrentSuite = null;
private long myTestStarted = 0;
private String myReason = null;
private String myFailingTest = null;
private List<String> myTrace = null;
private void processEventLine(@NotNull String line) {
if (LOG.isDebugEnabled()) LOG.debug(">> " + line);
if (myTrace != null) {
if (Proto.TRACE_END.equals(line)) {
processTrace();
}
else {
myTrace.add(line);
}
return;
}
if (line.length() >= EVENT_TYPE_LEN && line.charAt(0) == '%') {
if (line.startsWith(Proto.INIT)) {
processInit(line);
}
else if (line.startsWith(Proto.TREE)) {
processTreeLine(line);
}
else if (line.startsWith(Proto.TEST)) {
processTestStart(line);
}
else if (line.startsWith(Proto.FAILED)) {
myReason = Proto.FAILED;
myFailingTest = line;
}
else if (line.startsWith(Proto.ERROR)) {
myReason = Proto.ERROR;
myFailingTest = line;
}
else if (Proto.TRACE.equals(line)) {
myTrace = ContainerUtil.newArrayListWithCapacity(20);
}
else if (line.startsWith(Proto.TEST_END)) {
processTestEnd(line);
}
else if (line.startsWith(Proto.DONE)) {
processDone();
}
}
}
private static void processInit(@NotNull String line) {
int p = line.indexOf(' ', EVENT_TYPE_LEN);
if (p < 0 || !" v2".equals(line.substring(p))) {
LOG.warn("unsupported protocol: " + line);
}
}
private void processTreeLine(@NotNull String line) {
List<String> parts = StringUtil.split(line, ",");
if (parts.size() == 4 && "false".equals(parts.get(2))) {
Pair<String, String> names = parseTestName(parts.get(1), true);
if (names != null) {
myTestCount++;
}
}
}
private void processTestStart(@NotNull String line) {
myTestStarted = System.currentTimeMillis();
Pair<String, String> names = parseTestName(line, false);
String testName = fullTestName(names, line);
if (myTestCount > 0) {
myProcessor.onTestsCountInSuite(myTestCount);
myTestCount = -1;
}
if (names != null) {
String suite = names.first;
if (!suite.equals(myCurrentSuite)) {
if (myCurrentSuite != null) {
myProcessor.onSuiteFinished(new TestSuiteFinishedEvent(myCurrentSuite));
}
myProcessor.onSuiteStarted(new TestSuiteStartedEvent(suite, JavaTestLocator.SUITE_PROTOCOL + SCHEME_SEPARATOR + suite));
myCurrentSuite = suite;
}
}
GeneralTestEventsProcessor processor = myProcessor;
synchronized (myTestLock) {
myCurrentTest = testName;
processor.onTestStarted(new TestStartedEvent(testName, JavaTestLocator.TEST_PROTOCOL + SCHEME_SEPARATOR + testName));
}
}
private void processTestEnd(@NotNull String line) {
long t = System.currentTimeMillis() - myTestStarted;
String testName = fullTestName(parseTestName(line, false), line);
GeneralTestEventsProcessor processor = myProcessor;
synchronized (myTestLock) {
processor.onTestFinished(new TestFinishedEvent(testName, t));
myCurrentTest = null;
}
}
private void processTrace() {
if (myTrace != null) {
StringBuilder header = new StringBuilder();
StringBuilder stack = new StringBuilder();
boolean inStack = false;
for (String line : myTrace) {
if (!inStack && line.startsWith("\tat ")) {
inStack = true;
}
(inStack ? stack : header).append(line).append('\n');
}
Pair<String, String> pair = null;
String message = header.toString();
if (message.startsWith("org.junit.") || message.startsWith("junit.framework.")) {
pair = matchComparison(message);
if (pair != null) {
myReason = Proto.FAILED;
}
}
if (myFailingTest != null) {
String testName = fullTestName(parseTestName(myFailingTest, false), myFailingTest);
boolean testError = myReason != Proto.FAILED;
String expected = pair != null ? pair.first : null;
String actual = pair != null ? pair.second : null;
myProcessor.onTestFailure(new TestFailedEvent(testName, message, stack.toString(), testError, actual, expected));
}
else {
myProcessor.onError(message, stack.toString(), false);
}
}
myTrace = null;
myReason = null;
}
private void processDone() {
if (myCurrentSuite != null) {
myProcessor.onSuiteFinished(new TestSuiteFinishedEvent(myCurrentSuite));
myCurrentSuite = null;
}
}
@Nullable
private static Pair<String, String> parseTestName(@NotNull String line, boolean fromStart) {
int comma = fromStart ? 0 : line.indexOf(',', EVENT_TYPE_LEN);
if (comma >= 0) {
int paren = line.indexOf('(', comma + 1);
if (paren > 0 && paren < line.length() - 1) {
String test = line.substring(comma + 1, paren);
String suite = line.substring(paren + 1, line.length() - 1);
return pair(suite, test);
}
}
return null;
}
@NotNull
private static String fullTestName(@Nullable Pair<String, String> names, @NotNull String line) {
return names != null ? names.first + '.' + names.second : line.substring(EVENT_TYPE_LEN);
}
private static final class Comparisons {
private static final List<Pattern> PATTERNS = ContainerUtil.newArrayList(
compile("\nExpected: is \"(.*)\"\n\\s*got: \"(.*)\"\n"),
compile("\nExpected: is \"(.*)\"\n\\s*but: was \"(.*)\""),
compile("\nExpected: (.*)\n\\s*got: (.*)"),
compile(".*?\\s*expected same:<(.*)> was not:<(.*)>"),
compile(".*?\\s*expected:<(.*?)> but was:<(.*?)>"),
compile("\nExpected: \"(.*)\"\n\\s*but: was \"(.*)\""),
compile("\\s*Expected: (.*)\\s*but: was (.*)")
);
private static Pattern compile(String regex) {
return Pattern.compile(regex, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
}
}
@Nullable
private static Pair<String, String> matchComparison(@NotNull String message) {
for (Pattern pattern : Comparisons.PATTERNS) {
Matcher matcher = pattern.matcher(message);
if (matcher.find()) {
return pair(matcher.group(1).replaceAll("\\\\n", "\n"), matcher.group(2).replaceAll("\\\\n", "\n"));
}
}
return null;
}
}
}