blob: dca4d9816c64a2064f29d4035ee03a19b149d669 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.javascript.karma.run;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.gsf.testrunner.ui.api.Manager;
import org.netbeans.modules.gsf.testrunner.api.Status;
import org.netbeans.modules.gsf.testrunner.api.TestSession;
import org.netbeans.modules.gsf.testrunner.api.TestSuite;
import org.netbeans.modules.gsf.testrunner.api.Testcase;
import org.netbeans.modules.gsf.testrunner.api.Trouble;
import org.netbeans.modules.javascript.karma.util.StringUtils;
import org.openide.filesystems.FileUtil;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
// XXX create Handler that will process test results so this class can be tested
@NbBundle.Messages("TestRunner.noName=<no name>")
public final class TestRunner {
static final Logger LOGGER = Logger.getLogger(TestRunner.class.getName());
public static final String NB_LINE = "$NB$netbeans "; // NOI18N
private static final String NEW_LINE_REGEX = "\\s*\\\\n\\s*"; // NOI18N
private static final String UNCAUGHT_ERROR_PREFIX = "Uncaught Error: "; // NOI18N
private static final String BROWSER_START = "$NB$netbeans browserStart"; // NOI18N
private static final String BROWSER_END = "$NB$netbeans browserEnd"; // NOI18N
private static final String BROWSER_ERROR = "$NB$netbeans browserError"; // NOI18N
private static final String SUITE_START = "$NB$netbeans suiteStart"; // NOI18N
private static final String SUITE_END = "$NB$netbeans suiteEnd"; // NOI18N
private static final String TEST = "$NB$netbeans test"; // NOI18N
private static final String TEST_PASS = "$NB$netbeans testPass"; // NOI18N
private static final String TEST_IGNORE = "$NB$netbeans testIgnore"; // NOI18N
private static final String TEST_FAILURE = "$NB$netbeans testFailure"; // NOI18N
private static final String NB_VALUE_REGEX = "\\$NB\\$(.*)\\$NB\\$"; // NOI18N
private static final String NAME_REGEX = "name=" + NB_VALUE_REGEX; // NOI18N
private static final String BROWSER_REGEX = "browser=" + NB_VALUE_REGEX; // NOI18N
private static final String DURATION_REGEX = "duration=" + NB_VALUE_REGEX; // NOI18N
private static final String DETAILS_REGEX = "details=" + NB_VALUE_REGEX; // NOI18N
private static final String ERROR_REGEX = "error=" + NB_VALUE_REGEX; // NOI18N
private static final Pattern NAME_PATTERN = Pattern.compile(NAME_REGEX);
private static final Pattern BROWSER_NAME_PATTERN = Pattern.compile(BROWSER_REGEX + " " + NAME_REGEX); // NOI18N
private static final Pattern BROWSER_ERROR_PATTERN = Pattern.compile(BROWSER_REGEX + " " + ERROR_REGEX); // NOI18N
private static final Pattern NAME_DURATION_PATTERN = Pattern.compile(NAME_REGEX + " " + DURATION_REGEX); // NOI18N
private static final Pattern NAME_DETAILS_DURATION_PATTERN = Pattern.compile(NAME_REGEX + " " + DETAILS_REGEX + " " + DURATION_REGEX); // NOI18N
private static final Pattern STACK_TRACE_FILE_LINE_PATTERN = Pattern.compile("http://[^/]+/(?:base|absolute)(?<FILE>[^?\\s]*)(?:\\?[^:]*)?"); // NOI18N
private final KarmaRunInfo karmaRunInfo;
private final AtomicLong browserCount = new AtomicLong();
private final Map<String, List<String>> browserErrors = new HashMap<>();
private TestSession testSession;
private TestSuite testSuite;
private long testSuiteRuntime = 0;
private boolean hasTests = false;
private final ArrayList<String> logMessages = new ArrayList<>();
public TestRunner(KarmaRunInfo karmaRunInfo) {
assert karmaRunInfo != null;
this.karmaRunInfo = karmaRunInfo;
}
public void process(String line) {
LOGGER.finest(line);
if (line.startsWith(TEST)) {
testFinished(line);
} else if (line.startsWith(SUITE_START)) {
suiteStarted(line);
} else if (line.startsWith(SUITE_END)) {
suiteFinished(line);
} else if (line.startsWith(BROWSER_START)) {
if (browserCount.incrementAndGet() == 1) {
sessionStarted(line);
}
} else if (line.startsWith(BROWSER_END)) {
if (browserCount.decrementAndGet() == 0) {
sessionFinished(line);
}
} else if (line.startsWith(BROWSER_ERROR)) {
browserError(line);
} else {
LOGGER.log(Level.FINE, "Unexpected line: {0}", line);
assert false : line;
}
}
public void logMessageToTestResultsWindowOutputView(String line) {
if(testSession == null) {
// cache log message
logMessages.add(line);
return;
}
if(!logMessages.isEmpty()) {
// flush all previously cached log messages
for (String log : logMessages) {
getManager().displayOutput(testSession, log, false);
}
logMessages.clear();
}
getManager().displayOutput(testSession, line, false);
}
private Manager getManager() {
return Manager.getInstance();
}
@NbBundle.Messages({
"# {0} - project name",
"TestRunner.runner.title={0} (Karma)",
})
private String getOutputTitle() {
StringBuilder sb = new StringBuilder(30);
sb.append(ProjectUtils.getInformation(karmaRunInfo.getProject()).getDisplayName());
String testFile = karmaRunInfo.getTestFile();
if (testFile != null) {
sb.append(":"); // NOI18N
sb.append(new File(testFile).getName());
}
return Bundle.TestRunner_runner_title(sb.toString());
}
private void initTestSession() {
if (testSession != null) {
return;
}
Manager.getInstance().setNodeFactory(new KarmaTestRunnerNodeFactory(new CallStackCallback(karmaRunInfo.getProject())));
testSession = new TestSession(getOutputTitle(), karmaRunInfo.getProject(), TestSession.SessionType.TEST);
testSession.setRerunHandler(karmaRunInfo.getRerunHandler());
getManager().testStarted(testSession);
}
private void sessionStarted(String line) {
initTestSession();
}
@NbBundle.Messages({
"TestRunner.tests.error=Uncaught errors occured.",
"TestRunner.tests.none=No tests executed - perhaps an error occured?",
"TestRunner.output.verify=Full output can be verified in Output window.",
"TestRunner.output.view=Full output can be found in Output window.",
})
private void sessionFinished(String line) {
assert testSession != null;
if (testSuite != null) {
// can happen for qunit
suiteFinished(null);
}
if (!browserErrors.isEmpty()) {
processErrors();
getManager().displayOutput(testSession, Bundle.TestRunner_tests_error(), true);
getManager().displayOutput(testSession, Bundle.TestRunner_output_verify(), true);
} else if (!hasTests) {
getManager().displayOutput(testSession, Bundle.TestRunner_tests_none(), true);
getManager().displayOutput(testSession, Bundle.TestRunner_output_verify(), true);
} else {
getManager().displayOutput(testSession, Bundle.TestRunner_output_view(), false);
}
getManager().sessionFinished(testSession);
testSession = null;
hasTests = false;
browserErrors.clear();
}
@NbBundle.Messages({
"# {0} - browser name",
"TestRunner.browser.error=[{0}] ERROR:",
"TestRunner.browser.unknown=Unknown",
})
private void browserError(String line) {
initTestSession();
Matcher matcher = BROWSER_ERROR_PATTERN.matcher(line);
String browser;
String error;
if (matcher.find()) {
browser = matcher.group(1);
getManager().displayOutput(testSession, Bundle.TestRunner_browser_error(browser), true);
error = matcher.group(2);
if (error.startsWith("\"")) { // NOI18N
error = error.substring(1);
if (error.endsWith("\"")) { // NOI18N
error = error.substring(0, error.length() - 1);
}
}
String[] errorLines = error.split(NEW_LINE_REGEX);
for (String errorLine : errorLines) {
getManager().displayOutput(testSession, errorLine, true);
}
} else {
LOGGER.log(Level.FINE, "Unexpected browser error line: {0}", line);
getManager().displayOutput(testSession, line, true);
browser = Bundle.TestRunner_browser_unknown();
error = line;
// to work around FindBugs
assert assertLine(line);
}
getManager().displayOutput(testSession, "", false); // NOI18N
List<String> errors = browserErrors.get(browser);
if (errors == null) {
errors = new ArrayList<>();
browserErrors.put(browser, errors);
}
errors.add(error);
}
private boolean assertLine(String line) {
assert false : line;
return true;
}
@NbBundle.Messages({
"# {0} - browser name",
"TestRunner.error.suite=[{0}] Uncaught Errors",
})
private void processErrors() {
if (!karmaRunInfo.isFailOnBrowserError()) {
return;
}
for (Map.Entry<String, List<String>> entry : browserErrors.entrySet()) {
// suite
TestSuite errorTestSuite = new TestSuite(Bundle.TestRunner_error_suite(entry.getKey()));
testSession.addSuite(errorTestSuite);
getManager().displaySuiteRunning(testSession, errorTestSuite.getName());
// tests
for (String info : entry.getValue()) {
String[] details = processDetails(info);
String name = details[0];
if (name.startsWith(UNCAUGHT_ERROR_PREFIX)) {
name = name.substring(UNCAUGHT_ERROR_PREFIX.length()).trim();
if (!StringUtils.hasText(name)) {
name = details[0];
}
}
Trouble trouble = new Trouble(true);
if (details.length > 1) {
String[] stackTrace = new String[details.length - 1];
System.arraycopy(details, 1, stackTrace, 0, stackTrace.length);
trouble.setStackTrace(stackTrace);
}
addTestCase(name, Status.ERROR, 0, trouble);
}
getManager().displayReport(testSession, testSession.getReport(0), true);
}
}
@NbBundle.Messages({
"# {0} - browser name",
"# {1} - suite name",
"TestRunner.suite.name=[{0}] {1}",
})
private void suiteStarted(String line) {
assert testSession != null;
if (testSuite != null) {
// can happen for more browsers and last browser suite
suiteFinished(null);
}
assert testSuite == null;
assert testSuiteRuntime == 0;
String name = line;
Matcher matcher = BROWSER_NAME_PATTERN.matcher(line);
if (matcher.find()) {
name = Bundle.TestRunner_suite_name(matcher.group(1), matcher.group(2));
} else {
LOGGER.log(Level.FINE, "Unexpected suite line: {0}", line);
assert false : line;
}
if (!StringUtils.hasText(name)) {
name = Bundle.TestRunner_noName();
}
testSuite = new TestSuite(name);
testSession.addSuite(testSuite);
getManager().displaySuiteRunning(testSession, testSuite.getName());
}
private void suiteFinished(@NullAllowed String line) {
assert testSession != null;
if (testSuite == null) {
// current suite already finished
return;
}
getManager().displayReport(testSession, testSession.getReport(testSuiteRuntime), true);
testSuite = null;
testSuiteRuntime = 0;
}
private void testFinished(String line) {
assert testSession != null;
hasTests = true;
if (line.startsWith(TEST_PASS)) {
testPass(line);
} else if (line.startsWith(TEST_FAILURE)) {
testFailure(line);
} else if (line.startsWith(TEST_IGNORE)) {
// #259119
//testIgnore(line);
} else {
LOGGER.log(Level.FINE, "Unexpected test line: {0}", line);
assert false : line;
}
}
private void testPass(String line) {
Matcher matcher = NAME_DURATION_PATTERN.matcher(line);
if (matcher.find()) {
String name = matcher.group(1);
if (!StringUtils.hasText(name)) {
name = Bundle.TestRunner_noName();
}
long runtime = Long.parseLong(matcher.group(2));
addTestCase(name, Status.PASSED, runtime);
} else {
LOGGER.log(Level.FINE, "Unexpected test PASS line: {0}", line);
assert false : line;
}
}
private void testFailure(String line) {
Matcher matcher = NAME_DETAILS_DURATION_PATTERN.matcher(line);
if (matcher.find()) {
String name = matcher.group(1);
if (!StringUtils.hasText(name)) {
name = Bundle.TestRunner_noName();
}
Trouble trouble = new Trouble(false);
trouble.setStackTrace(processDetails(matcher.group(2)));
long runtime = Long.parseLong(matcher.group(3));
addTestCase(name, Status.FAILED, runtime, trouble);
} else {
LOGGER.log(Level.FINE, "Unexpected test FAILURE line: {0}", line);
assert false : line;
}
}
static String[] processDetails(String details) {
if (details.startsWith("[\"")) { // NOI18N
details = details.substring(2);
}
if (details.endsWith("\"]")) { // NOI18N
details = details.substring(0, details.length() - 2);
}
return STACK_TRACE_FILE_LINE_PATTERN.matcher(details)
.replaceAll("${FILE}") // NOI18N
.split(NEW_LINE_REGEX);
}
private void testIgnore(String line) {
Matcher matcher = NAME_PATTERN.matcher(line);
if (matcher.find()) {
String name = matcher.group(1);
if (!StringUtils.hasText(name)) {
name = Bundle.TestRunner_noName();
}
addTestCase(name, Status.IGNORED);
} else {
LOGGER.log(Level.FINE, "Unexpected test IGNORE line: {0}", line);
assert false : line;
}
}
private void addTestCase(String name, Status status) {
addTestCase(name, status, 0L);
}
private void addTestCase(String name, Status status, long runtime) {
addTestCase(name, status, runtime, null);
}
private void addTestCase(String name, Status status, long runtime, Trouble trouble) {
Testcase testCase = new Testcase(name, "Karma", testSession); // NOI18N
testCase.setStatus(status);
testSuiteRuntime += runtime;
testCase.setTimeMillis(runtime);
if (trouble != null) {
testCase.setTrouble(trouble);
}
testSession.addTestCase(testCase);
}
//~ Inner classes
private static final class CallStackCallback implements JumpToCallStackAction.Callback {
private static final Pattern FILE_LINE_PATTERN = Pattern.compile("/(?<FILE>[^:]+):(?<LINE>\\d+)"); // NOI18N
private final File projectDir;
public CallStackCallback(Project project) {
assert project != null;
projectDir = FileUtil.toFile(project.getProjectDirectory());
}
@Override
public Pair<File, Integer> parseLocation(String callStack) {
Matcher matcher = FILE_LINE_PATTERN.matcher(callStack);
if (matcher.find()) {
File path = new File(matcher.group("FILE").replace('/', File.separatorChar)); // NOI18N
File file;
if (path.isAbsolute()) {
file = path;
} else {
file = new File(projectDir, path.getPath());
if (!file.isFile()) {
return null;
}
}
return Pair.of(file, Integer.parseInt(matcher.group("LINE"))); // NOI18N
}
return null;
}
}
}