blob: 60a06ec9f4c3e2ded4ba87a4b0854338d7235c69 [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.jstestdriver;
import org.netbeans.modules.javascript.jstestdriver.ui.nodes.JSTestDriverTestRunnerNodeFactory;
import org.netbeans.modules.javascript.jstestdriver.ui.nodes.JumpToCallStackAction;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeListener;
import org.netbeans.api.extexecution.print.ConvertedLine;
import org.netbeans.api.extexecution.print.LineConvertor;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.libs.jstestdriver.api.JsTestDriver;
import org.netbeans.libs.jstestdriver.api.ServerListener;
import org.netbeans.libs.jstestdriver.api.TestListener;
import org.netbeans.modules.gsf.testrunner.ui.api.Manager;
import org.netbeans.modules.gsf.testrunner.api.Report;
import org.netbeans.modules.gsf.testrunner.api.RerunHandler;
import org.netbeans.modules.gsf.testrunner.api.RerunType;
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.web.browser.api.WebBrowser;
import org.netbeans.modules.web.browser.api.WebBrowserFeatures;
import org.netbeans.modules.web.browser.api.WebBrowserPane;
import org.netbeans.modules.web.common.api.RemoteFileCache;
import org.netbeans.modules.web.common.api.ServerURLMapping;
import org.netbeans.modules.web.common.api.WebUtils;
import org.openide.LifecycleManager;
import org.openide.cookies.LineCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.MIMEResolver;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.text.Line;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.util.Pair;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.windows.OutputEvent;
import org.openide.windows.OutputListener;
@Messages("JsTestDriverResolver=js-test-driver Conf Files")
@MIMEResolver.Registration(
displayName="#JsTestDriverResolver",
position=132, // has to be after languages.apachecon resolver, see #267416
resource="resources/mime-resolver.xml"
)
public class JSTestDriverSupport {
private static JSTestDriverSupport def;
private static final Logger LOGGER = Logger.getLogger(JSTestDriverSupport.class.getName());
private static final Logger USG_LOGGER = Logger.getLogger("org.netbeans.ui.metrics.jstestdriver"); // NOI18N
private RequestProcessor RP = new RequestProcessor("js-test-driver server", 5); //NOI18N
private AbstractLookup projectContext;
private InstanceContent lookupContent;
private List<WebBrowserPane> integratedBrowserPanes;
public static synchronized JSTestDriverSupport getDefault() {
if (def == null) {
def = new JSTestDriverSupport();
}
return def;
}
private JsTestDriver testDriver;
private volatile boolean starting = false;
private JSTestDriverSupport() {
lookupContent = new InstanceContent();
projectContext = new AbstractLookup(lookupContent);
}
private synchronized JsTestDriver getJsTestDriver() {
if (testDriver == null) {
if (!isConfiguredProperly()) {
return null;
}
String jsTestDriverJar = JSTestDriverCustomizerPanel.getJSTestDriverJar();
File f = new File(jsTestDriverJar);
try {
testDriver = new JsTestDriver(f);
} catch (RuntimeException ex) {
LOGGER.log(Level.INFO, "cannot access js-test-driver wrapper", ex); //NOI18N
return null;
}
}
return testDriver;
}
public String getUserDescription() {
if (JSTestDriverCustomizerPanel.getPort() == -1) {
return NbBundle.getMessage(JSTestDriverSupport.class, "SERVER_EXTERNAL", JSTestDriverCustomizerPanel.getServerURL());
} else if (wasStartedExternally()) {
return NbBundle.getMessage(JSTestDriverSupport.class, "SERVER_EXTERNAL2", JSTestDriverCustomizerPanel.getPort());
} else if (isRunning()) {
return NbBundle.getMessage(JSTestDriverSupport.class, "SERVER_RUNNING", JSTestDriverCustomizerPanel.getServerURL());
} else {
return NbBundle.getMessage(JSTestDriverSupport.class, "SERVER_NOT_RUNNING");
}
}
boolean isRunning() {
return testDriver != null && testDriver.isRunning();
}
void forgetCurrentServer() {
testDriver = null;
}
public boolean wasStartedExternally() {
return testDriver != null && testDriver.wasStartedExternally();
}
public boolean isStarting() {
return starting;
}
public void stop() {
assert isRunning();
assert testDriver != null;
testDriver.stopServer();
TestDriverServiceNode.getInstance().refresh();
try { // try waiting a bit after stopping server
Thread.sleep(1000);
} catch (InterruptedException ex) {
LOGGER.log(Level.INFO, null, ex);
Thread.currentThread().interrupt();
}
if (integratedBrowserPanes != null) {
for (WebBrowserPane wbp : integratedBrowserPanes) {
wbp.close(true);
}
}
}
public void start(final ServerListener l) {
assert !isRunning();
JsTestDriver td = getJsTestDriver();
if (td == null) {
if (configure()) {
td = getJsTestDriver();
}
}
if (td == null) {
return;
}
final JsTestDriver td2 = td;
starting = true;
TestDriverServiceNode.getInstance().refresh();
RP.post(new Runnable() {
@Override
public void run() {
try {
td2.startServer(JSTestDriverCustomizerPanel.getPort(),
JSTestDriverCustomizerPanel.isStricModel(),
new ServerListener() {
@Override
public void serverStarted() {
RP.post(new Runnable() {
@Override
public void run() {
captureBrowsers();
if (l != null) {
l.serverStarted();
}
starting = false;
TestDriverServiceNode.getInstance().refresh();
logUsage(JSTestDriverSupport.class, "USG_JSTESTDRIVER_STARTED", null); // NOI18N
}
});
}
});
if (td2.wasStartedExternally()) {
starting = false;
TestDriverServiceNode.getInstance().refresh();
if (l != null) {
l.serverStarted();
}
}
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, "cannot start server", t); //NOI18N
}
}
});
}
public boolean configure() {
final boolean[] res = new boolean[1];
Runnable r = new Runnable() {
@Override
public void run() {
boolean b = JSTestDriverCustomizerPanel.showCustomizer();
TestDriverServiceNode.getInstance().refresh();
res[0] = b;
}
};
try {
if (SwingUtilities.isEventDispatchThread()) {
r.run();
} else {
// it should be safe to call invokeAndWait here
// as this action will be either called from AWT thread or from
// RequestProcessor as a result of "test project" or "start server" in
// case when configuration is missing the first time:
SwingUtilities.invokeAndWait(r);
}
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
} catch (InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
}
return res[0];
}
public boolean isConfiguredProperly() {
return JSTestDriverCustomizerPanel.isConfiguredProperly();
}
public void runAllTests(Project project, File baseFolder, File configFile,
String testsToRun) {
JsTestDriver td = getJsTestDriver();
if (td == null) {
if (configure()) {
td = getJsTestDriver();
}
}
if (td == null) {
return;
}
String serverURL = JSTestDriverCustomizerPanel.getServerURL();
int port = JSTestDriverCustomizerPanel.getPort();
boolean strictMode = JSTestDriverCustomizerPanel.isStricModel();
if (!isRunning() && port != -1) {
final Semaphore s = new Semaphore(0);
start(new ServerListener() {
@Override
public void serverStarted() {
try {
// give browsers some time to start and connect to server
Thread.sleep(3000);
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
s.release();
}
});
try {
s.acquire();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
if (!isRunning()) {
return;
}
}
updateJsDebuggerProjectContext(project);
TestListener listener = new Listener(project, new Rerun(project, baseFolder, configFile));
LineConvertor convertor = new LineConvertorImpl(project);
td.runTests(serverURL, strictMode, baseFolder, configFile, testsToRun, listener, convertor);
}
private void updateJsDebuggerProjectContext(Project p) {
// update lookup used by JS debugger with the right project
lookupContent.set(Collections.singletonList(p), null);
}
private void captureBrowsers() {
integratedBrowserPanes = new ArrayList<WebBrowserPane>();
for (WebBrowser bd : JSTestDriverCustomizerPanel.getBrowsers()) {
String s = JSTestDriverCustomizerPanel.getServerURL();
// #230400 - use IP address instead of localhost so that mobile browsers testing works:
if (bd.getBrowserFamily().isMobile() && s.startsWith("http://localhost:")) {
s = s.replaceAll("localhost", WebUtils.getLocalhostInetAddress().getHostAddress());
}
s = s+"/capture"; //NOI18N
if (bd.hasNetBeansIntegration()) {
// '/timeout/-1/' - will prevent js-test-driver from timeouting the test
// when test execution takes too much time, for example when test is being debugged
s += "/timeout/-1/"; //NOI18N
}
if (JSTestDriverCustomizerPanel.isStricModel()) {
s += "?strict"; //NOI18N
}
try {
URL u = new URL(s);
WebBrowserFeatures features = new WebBrowserFeatures(true, true, false, false, true, false);
WebBrowserPane pane = bd.createNewBrowserPane(features, true);
// the problem here is following: js-test-driver is a global server
// which does not have any project specific context. But in order to
// debug JavaScript the JS debugger needs a project context in order
// to correctly resolve breakpoints etc. So when server is started
// there will not be any project context; only when a test is
// executed from a project the project context will be set for JS debugger by
// updating the lookup; JS debugger listens on lookup changes
pane.setProjectContext(projectContext);
pane.showURL(u);
if (bd.hasNetBeansIntegration()) {
integratedBrowserPanes.add(pane);
}
} catch (MalformedURLException ex) {
Exceptions.printStackTrace(ex);
}
}
}
private static class LineConvertorImpl implements LineConvertor {
private Project p;
public LineConvertorImpl(Project p) {
this.p = p;
}
// XXX: exact this algorithm is also in
// org.netbeans.modules.web.webkit.tooling.console.BrowserConsoleLogger.java
// keep them in sync
@Override
public List<ConvertedLine> convert(String line) {
// pattern is "at ...... (file:line:column)"
// file can be also http:// url
if (!line.endsWith(")")) { //NOI18N
return convertLineURL(line);
}
int start = line.lastIndexOf('(');
if (start == -1) {
return null;
}
int lineNumberEnd = line.lastIndexOf(':');
if (lineNumberEnd == -1) {
return null;
}
int fileEnd = line.lastIndexOf(':', lineNumberEnd-1);
if (fileEnd == -1) {
return null;
}
if (start >= fileEnd) {
return null;
}
int lineNumber = -1;
int columnNumber = -1;
try {
lineNumber = Integer.parseInt(line.substring(fileEnd+1, lineNumberEnd));
columnNumber = Integer.parseInt(line.substring(lineNumberEnd+1, line.length()-1));
} catch (NumberFormatException e) {
//ignore
}
if (columnNumber != -1 && lineNumber == -1) {
// perhaps stack trace had only line number:
lineNumber = columnNumber;
}
if (lineNumber == -1) {
return null;
}
String file = line.substring(start+1, fileEnd);
if (file.length() == 0) {
return null;
}
FileObject fo = p.getProjectDirectory().getFileObject(file);
if (fo == null) {
return null;
}
List<ConvertedLine> res = new ArrayList<ConvertedLine>();
//res.add(ConvertedLine.forText(line.substring(0, start), null));
ListenerImpl l = new ListenerImpl(fo, lineNumber, columnNumber);
res.add(ConvertedLine.forText(/*line.substring(start, line.length()-1)*/line, l.isValidHyperlink() ? l : null));
//res.add(ConvertedLine.forText(line.substring(line.length()-1, line.length()), null));
return res;
}
private List<ConvertedLine> convertLineURL(String line) {
int u1 = line.indexOf("http://"); // NOI18N
if (u1 < 0) {
u1 = line.indexOf("https://"); // NOI18N
}
if (u1 < 0) {
return null;
}
int ue = line.indexOf(' ', u1);
int uee = line.indexOf('\n', u1);
if (uee != -1
&& uee < ue) {
// #236037
ue = uee;
}
if (ue < 0) {
ue = line.length();
}
int col2 = line.lastIndexOf(':', ue);
if (col2 < 0) {
return null;
}
int col1 = line.lastIndexOf(':', col2 - 1);
if (col1 < 0) {
return null;
}
int lineNumber = -1;
int columnNumber = -1;
try {
lineNumber = Integer.parseInt(line.substring(col1+1, col2));
columnNumber = Integer.parseInt(line.substring(col2+1, ue));
} catch (NumberFormatException e) {
//ignore
}
if (columnNumber != -1 && lineNumber == -1) {
// perhaps stack trace had only line number:
lineNumber = columnNumber;
}
if (lineNumber == -1) {
return null;
}
String file = line.substring(u1, col1);
if (file.length() == 0) {
return null;
}
FileObject fo = null;
try {
URL url = URI.create(file).toURL();
fo = ServerURLMapping.fromServer(p, url);
if (fo == null) {
fo = RemoteFileCache.getRemoteFile(url);
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if (fo == null) {
return null;
}
List<ConvertedLine> res = new ArrayList<ConvertedLine>();
//res.add(ConvertedLine.forText(line.substring(0, start), null));
ListenerImpl l = new ListenerImpl(fo, lineNumber, columnNumber);
res.add(ConvertedLine.forText(/*line.substring(start, line.length()-1)*/line, l.isValidHyperlink() ? l : null));
//res.add(ConvertedLine.forText(line.substring(line.length()-1, line.length()), null));
return res;
}
}
private static class ListenerImpl implements OutputListener {
private FileObject fo;
private int line;
private int column;
public ListenerImpl(FileObject fo, int line, int column) {
this.fo = fo;
this.line = line;
this.column = column;
}
@Override
public void outputLineSelected(OutputEvent ev) {
}
@Override
public void outputLineAction(OutputEvent ev) {
Line l = getLine();
if (l != null) {
l.show(Line.ShowOpenType.OPEN,
Line.ShowVisibilityType.FOCUS, column != -1 ? column -1 : -1);
}
}
private Line getLine() {
LineCookie result = null;
try {
DataObject dataObject = DataObject.find(fo);
if (dataObject != null) {
result = dataObject.getCookie(LineCookie.class);
}
} catch (DataObjectNotFoundException e) {
e.printStackTrace();
}
if (result != null) {
return result.getLineSet().getCurrent(line-1);
}
return null;
}
@Override
public void outputLineCleared(OutputEvent ev) {
}
public boolean isValidHyperlink() {
return getLine() != null;
}
}
private static class Listener implements TestListener {
private TestSession testSession;
private Manager manager;
private Report report;
@NbBundle.Messages({
"# {0} - project name",
"JsTestDriverSupport.runner.title={0} (JS Test Driver)",
})
public Listener(Project project, Rerun rerun) {
manager = Manager.getInstance();
manager.setNodeFactory(new JSTestDriverTestRunnerNodeFactory(new CallStackCallback(project)));
testSession = new TestSession(
Bundle.JsTestDriverSupport_runner_title(ProjectUtils.getInformation(project).getDisplayName()),
project, TestSession.SessionType.TEST);
testSession.setRerunHandler(rerun);
manager.testStarted(testSession);
}
@Override
public void onTestComplete(TestResult testResult) {
TestSuite currentSuite = testSession.getCurrentSuite();
if (currentSuite == null || !currentSuite.getName().equals(testResult.getBrowserInfo().getDisplayName())) {
if (report != null) {
manager.displayReport(testSession, report, true);
}
TestSuite ts = new TestSuite(testResult.getBrowserInfo().getDisplayName());
testSession.addSuite(ts);
report = testSession.getReport(0);
manager.displaySuiteRunning(testSession, ts.getName());
}
Testcase testCase = new Testcase(testResult.getTestCaseName()+"."+testResult.getTestName(), null, testSession); //NOI18N
testCase.setStatus(convert(testResult.getResult()));
testCase.setTimeMillis(testResult.getDuration());
if (testResult.getResult() == TestResult.Result.failed || testResult.getResult() == TestResult.Result.error) {
Trouble t = new Trouble(true);
if (testResult.getStack().length() > 0) {
t.setStackTrace(trimArray(testResult.getStack().split("\\u000d"))); //NOI18N
testCase.addOutputLines(Arrays.asList(testResult.getStack().split("\\u000d"))); //NOI18N
//manager.displayOutput(testSession, testResult.getStack(), true);
}
if (testResult.getMessage().length() > 0) {
//manager.displayOutput(testSession, testResult.getMessage(), true);
}
if (testResult.getLog().length() > 0) {
//manager.displayOutput(testSession, testResult.getLog(), true);
}
testCase.setTrouble(t);
}
testSession.addTestCase(testCase);
report.update(testSession.getReport(0));
manager.displayReport(testSession, report, false);
}
private Status convert(TestResult.Result res) {
switch (res) {
case passed:
return Status.PASSED;
case failed:
return Status.FAILED;
case error:
return Status.ERROR;
case started:
return Status.PENDING;
default:
throw new AssertionError(res.name());
}
}
@Override
public void onTestingFinished() {
manager.sessionFinished(testSession);
if (report == null) {
// no tests were run; generate empty report:
testSession.addSuite(TestSuite.ANONYMOUS_TEST_SUITE);
report = testSession.getReport(0);
}
manager.displayReport(testSession, report, true);
}
private String[] trimArray(String[] split) {
if (split == null) {
return null;
}
List<String> r = new ArrayList<String>();
for (int i = 0; i < split.length; i++) {
String s = split[i].trim();
if (s.length() > 0) {
r.add(s);
}
}
return r.toArray(new String[r.size()]);
}
}
private static class Rerun implements RerunHandler {
private Project project;
private File baseFolder;
private File configFile;
private RequestProcessor RP = new RequestProcessor("js-test-driver rerun", 5); //NOI18N
public Rerun(Project project, File baseFolder, File configFile) {
this.project = project;
this.baseFolder = baseFolder;
this.configFile = configFile;
}
@Override
public void rerun() {
RP.post(new Runnable() {
@Override
public void run() {
LifecycleManager.getDefault().saveAll();
JSTestDriverSupport.getDefault().runAllTests(project, baseFolder, configFile, "all"); //NOI18N
}
});
}
@Override
public void rerun(Set<Testcase> tests) {
}
@Override
public boolean enabled(RerunType type) {
return RerunType.ALL.equals(type);
}
@Override
public void addChangeListener(ChangeListener listener) {
}
@Override
public void removeChangeListener(ChangeListener listener) {
}
}
public static void logUsage(Class srcClass, String message, Object[] params) {
Parameters.notNull("message", message); // NOI18N
LogRecord logRecord = new LogRecord(Level.INFO, message);
logRecord.setLoggerName(USG_LOGGER.getName());
logRecord.setResourceBundle(NbBundle.getBundle(srcClass));
logRecord.setResourceBundleName(srcClass.getPackage().getName() + ".Bundle"); // NOI18N
if (params != null) {
logRecord.setParameters(params);
}
USG_LOGGER.log(logRecord);
}
//~ Inner classes
private static final class CallStackCallback implements JumpToCallStackAction.Callback {
private static final Pattern FILE_LINE_PATTERN = Pattern.compile(" [(](?<FILE>[^:]+):(?<LINE>\\d+):(?<COLUMN>\\d+)[)]"); // NOI18N
private final File projectDir;
public CallStackCallback(Project project) {
assert project != null;
projectDir = FileUtil.toFile(project.getProjectDirectory());
}
@Override
public Pair<File, int[]> 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;
}
}
int[] lineColumn = {Integer.parseInt(matcher.group("LINE")), Integer.parseInt(matcher.group("COLUMN"))};
return Pair.of(file, lineColumn); // NOI18N
}
return null;
}
}
}