blob: 40ebd46895ffa1b6cfe77661eebaab3788a90e55 [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.selenium2.webclient.protractor;
import java.awt.EventQueue;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.extexecution.ExecutionDescriptor;
import org.netbeans.api.extexecution.print.ConvertedLine;
import org.netbeans.api.extexecution.print.LineConvertor;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.modules.javascript.nodejs.api.NodeJsSupport;
import org.netbeans.modules.javascript.v8debug.api.Connector;
import org.netbeans.modules.selenium2.api.Utils;
import org.netbeans.modules.selenium2.webclient.api.RunInfo;
import org.netbeans.modules.selenium2.webclient.api.SeleniumRerunHandler;
import org.netbeans.modules.selenium2.webclient.api.SeleniumTestingProviders;
import org.netbeans.modules.selenium2.webclient.api.TestRunnerReporter;
import org.netbeans.modules.selenium2.webclient.api.Utilities;
import org.netbeans.modules.selenium2.webclient.protractor.preferences.ProtractorPreferences;
import org.netbeans.modules.selenium2.webclient.protractor.preferences.ProtractorPreferencesValidator;
import org.netbeans.modules.web.clientproject.api.WebClientProjectConstants;
import org.netbeans.modules.web.common.api.ValidationResult;
import org.netbeans.modules.web.common.ui.api.ExternalExecutable;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.Exceptions;
import org.openide.util.Pair;
import org.openide.util.RequestProcessor;
import org.openide.windows.OutputEvent;
import org.openide.windows.OutputListener;
/**
*
* @author Theofanis Oikonomou
*/
class ProtractorRunner {
private static final Logger LOG = Logger.getLogger(ProtractorRunner.class.getName());
private static final String DEBUG_HOST = "localhost"; // NOI18N
private static final int DEBUG_PORT = 5858;
public static void runTests(FileObject[] activatedFOs) {
internalProtractorRunner(activatedFOs, false);
}
public static void debugTests(FileObject[] activatedFOs) {
internalProtractorRunner(activatedFOs, true);
}
private static void internalProtractorRunner(FileObject[] activatedFOs, boolean debug) {
assert !EventQueue.isDispatchThread();
Project p = FileOwnerQuery.getOwner(activatedFOs[0]);
if (p == null) {
return;
}
File protractorJasmineNBReporter = InstalledFileLocator.getDefault().locate(
"protractor/jasmine-netbeans-reporter.js", "org.netbeans.modules.selenium2.webclient.protractor", false); // NOI18N
if(protractorJasmineNBReporter == null) {
return;
}
File protractorNBConfig = InstalledFileLocator.getDefault().locate(
"protractor/netbeans-configuration.js", "org.netbeans.modules.selenium2.webclient.protractor", false); // NOI18N
if(protractorNBConfig == null) {
return;
}
String node = getNode(p);
if(node == null) {
openNodeSettings(p);
return;
}
FileObject testsFolder = Utilities.getTestsSeleniumFolder(p, true);
if(testsFolder == null) {
Utilities.openCustomizer(p, WebClientProjectConstants.CUSTOMIZER_SOURCES_IDENT);
return;
}
String protractor = ProtractorPreferences.getProtractor(p);
String userConfigurationFile = ProtractorPreferences.getUserConfigurationFile(p);
ValidationResult validationResult = new ProtractorPreferencesValidator()
.validateProtractor(protractor)
.validateUserConfigurationFile(p, userConfigurationFile)
.getResult();
if(protractor == null || protractor.isEmpty() || !validationResult.isFaultless()) {
Utilities.openCustomizer(p, SeleniumTestingProviders.CUSTOMIZER_SELENIUM_TESTING_IDENT);
return;
}
boolean testProject = activatedFOs.length == 1 && activatedFOs[0].equals(p.getProjectDirectory());
String specs;
if(testProject) {
specs = FileUtil.toFile(testsFolder).getAbsolutePath() + "/**/*.js";
} else {
ArrayList<String> files2test = new ArrayList<>();
for(FileObject fo : activatedFOs) {
if(fo.isFolder()) { // recursively add all specs under this folder
Enumeration<? extends FileObject> children = fo.getChildren(true);
while (children.hasMoreElements()) {
add2specs(children.nextElement(), files2test);
}
} else {
add2specs(fo, files2test);
}
}
String specs2test = files2test.toString();
specs = specs2test.substring(1, specs2test.length() - 1);
}
RunInfo runInfo = getRunInfo(p, activatedFOs, testsFolder, specs, protractorJasmineNBReporter.getAbsolutePath(), userConfigurationFile, testProject);
String displayname = ProjectUtils.getInformation(runInfo.getProject()).getDisplayName() + " Selenium Tests"; // NOI18N
final ExternalExecutable externalexecutable = new ExternalExecutable(node)
.workDir(FileUtil.toFile(p.getProjectDirectory()))
.displayName(displayname)
.additionalParameters(getAdditionalArguments(protractor, protractorNBConfig.getAbsolutePath(), debug))
.environmentVariables(runInfo.getEnvVars());
TestRunnerReporter testRunnerReporter = new TestRunnerReporter(runInfo, "jasmine-netbeans-reporter "); // NOI18N
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutionDescriptor.LineConvertorFactory outputLineConvertorFactory = getOutputLineConvertorFactory(testRunnerReporter, runInfo, countDownLatch, debug);
final ExecutionDescriptor descriptor = new ExecutionDescriptor()
.frontWindow(true)
.controllable(true)
.showProgress(true)
.showSuspended(true)
.outLineBased(true)
.errLineBased(true)
.outConvertorFactory(outputLineConvertorFactory);
final Future<Integer> task = externalexecutable.run(descriptor);
if (debug) {
try {
countDownLatch.await(15, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
Connector.Properties props = createConnectorProperties(DEBUG_HOST, DEBUG_PORT, p);
try {
Connector.connect(props, new Runnable() {
@Override
public void run() {
task.cancel(true);
}
});
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
}
/**
* Gets file path (<b>possibly with parameters!</b>) representing <tt>node</tt> executable of the given project
* or {@code null} if not set/found. If the project is {@code null}, the default <tt>node</tt>
* path is returned (path set in IDE Options).
* @param project project to get <tt>node</tt> for, can be {@code null}
* @return file path (<b>possibly with parameters!</b>) representing <tt>node</tt> executable, can be {@code null} if not set/found
*/
@CheckForNull
private static String getNode(@NullAllowed Project project) {
return NodeJsSupport.getInstance().getNode(project);
}
/**
* Opens <tt>node</tt> settings for the given project - if project specific <tt>node</tt> is used
* (and node.js is enabled in the given project), Project Properties dialog is opened (otherwise
* IDE Options). If project is {@code null}, opens IDE Options.
* @param project project to be used, can be {@code null}
*/
private static void openNodeSettings(@NullAllowed Project project) {
NodeJsSupport.getInstance().openNodeSettings(project);
}
private static void add2specs(FileObject fo, ArrayList<String> files2test) {
String file2test = FileUtil.toFile(fo).getAbsolutePath();
if (file2test != null) {
if (!files2test.contains(file2test)) {
files2test.add(file2test);
}
}
}
private static ExecutionDescriptor.LineConvertorFactory getOutputLineConvertorFactory(final TestRunnerReporter testRunnerReporter, final RunInfo runInfo, final CountDownLatch countDownLatch, boolean debug) {
if (!debug) {
return new ExecutionDescriptor.LineConvertorFactory() {
@Override
public LineConvertor newLineConvertor() {
return new OutputLineConvertor(testRunnerReporter, runInfo, null);
}
};
}
final Runnable countDownTask = new Runnable() {
@Override
public void run() {
countDownLatch.countDown();
}
};
return new ExecutionDescriptor.LineConvertorFactory() {
@Override
public LineConvertor newLineConvertor() {
return new OutputLineConvertor(testRunnerReporter, runInfo, countDownTask);
}
};
}
private static ArrayList<String> getAdditionalArguments(String protractor, String config, boolean debug) {
ArrayList<String> arguments = new ArrayList<>();
arguments.add(protractor);
if(debug) {
arguments.add("--debug-brk");
}
arguments.add(config);
return arguments;
}
private static RunInfo getRunInfo(Project p, FileObject[] activatedFOs, FileObject testsFolder, String specs, String jasmineNBReporter, String userConfigurationFile, boolean testProject) {
RunInfo.Builder builder = new RunInfo.Builder(activatedFOs).setTestingProject(testProject)
.addEnvVar("SPECS", specs)
.addEnvVar("JASMINE_NB_REPORTER", jasmineNBReporter)
.addEnvVar("USER_CONFIGURATION_FILE", userConfigurationFile)
.setRerunHandler(new SeleniumRerunHandler(p, activatedFOs, CustomizerProtractorPanel.IDENTIFIER, true))
.setIsSelenium(true)
.setShowOutput(false);
if(activatedFOs.length == 1 && !activatedFOs[0].equals(p.getProjectDirectory())) {
String testFile = FileUtil.getRelativePath(testsFolder, activatedFOs[0]);
builder = builder.setTestFile(testFile);
}
return builder.build();
}
private static Connector.Properties createConnectorProperties(String host, int port, Project project) {
List<File> sourceRoots = getRoots(project, WebClientProjectConstants.SOURCES_TYPE_HTML5);
List<File> siteRoots = getRoots(project, WebClientProjectConstants.SOURCES_TYPE_HTML5_SITE_ROOT);
List<File> testRoots = getRoots(project, WebClientProjectConstants.SOURCES_TYPE_HTML5_TEST);
List<String> localPaths = new ArrayList<>(sourceRoots.size());
List<String> localPathsExclusionFilter = Collections.emptyList();
for (File src : sourceRoots) {
localPaths.add(src.getAbsolutePath());
for (File site : siteRoots) {
if (isSubdirectoryOf(src, site)) {
if (localPathsExclusionFilter.isEmpty()) {
localPathsExclusionFilter = new ArrayList<>();
}
localPathsExclusionFilter.add(site.getAbsolutePath());
}
}
for (File site : testRoots) {
if (isSubdirectoryOf(src, site)) {
if (localPathsExclusionFilter.isEmpty()) {
localPathsExclusionFilter = new ArrayList<>();
}
localPathsExclusionFilter.add(site.getAbsolutePath());
}
}
}
return new Connector.Properties(host, port, localPaths, Collections.<String>emptyList(), localPathsExclusionFilter);
}
private static List<File> getRoots(Project project, String type) {
SourceGroup[] sourceGroups = ProjectUtils.getSources(project).getSourceGroups(type);
List<File> roots = new ArrayList<>(sourceGroups.length);
for (SourceGroup sourceGroup : sourceGroups) {
FileObject rootFolder = sourceGroup.getRootFolder();
File root = FileUtil.toFile(rootFolder);
assert root != null : rootFolder;
roots.add(root);
}
return roots;
}
private static boolean isSubdirectoryOf(File folder, File child) {
if (!folder.isDirectory()) {
return false;
}
String fp;
try {
fp = folder.getCanonicalPath();
} catch (IOException ioex) {
fp = folder.getAbsolutePath();
}
String chp;
try {
chp = child.getCanonicalPath();
} catch (IOException ioex) {
chp = child.getAbsolutePath();
}
if (!chp.startsWith(fp)) {
return false;
}
int fl = fp.length();
if (chp.length() == fl) {
return true;
}
char separ = chp.charAt(fl);
if (File.separatorChar == separ) {
return true;
} else {
return false;
}
}
private static class OutputLineConvertor implements LineConvertor {
private final TestRunnerReporter testRunnerReporter;
private final RunInfo runInfo;
private Runnable debuggerStartTask;
public OutputLineConvertor(TestRunnerReporter testRunnerReporter, RunInfo runInfo, Runnable debuggerStartTask) {
this.testRunnerReporter = testRunnerReporter;
this.runInfo = runInfo;
this.debuggerStartTask = debuggerStartTask;
}
@Override
public List<ConvertedLine> convert(String line) {
// debugger?
if (debuggerStartTask != null
&& line.startsWith("debugger listening on port")) { // NOI18N
debuggerStartTask.run();
debuggerStartTask = null;
}
String output2display = testRunnerReporter.processLine(line);
if(output2display == null) {
return Collections.<ConvertedLine>emptyList();
}
TestRunnerReporter.CallStackCallback callStackCallback = new TestRunnerReporter.CallStackCallback(runInfo.getProject());
Pair<File, int[]> parsedLocation = callStackCallback.parseLocation(line, false);
FileOutputListener fileOutputListener = parsedLocation == null ? null : new FileOutputListener(parsedLocation.first(), parsedLocation.second()[0], parsedLocation.second()[1]);
return Collections.singletonList(ConvertedLine.forText(output2display, fileOutputListener));
}
}
private static final class FileOutputListener implements OutputListener {
final File file;
final int line;
final int column;
public FileOutputListener(File file, int line, int column) {
assert file != null;
this.file = file;
this.line = line;
this.column =column;
}
@Override
public void outputLineSelected(OutputEvent ev) {
// noop
}
@Override
public void outputLineAction(OutputEvent ev) {
RequestProcessor.getDefault().post(new Runnable() {
@Override
public void run() {
Utils.openFile(file, line, column);
}
});
}
@Override
public void outputLineCleared(OutputEvent ev) {
// noop
}
}
}