| /* |
| * 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; |
| } |
| |
| } |
| |
| } |