blob: fcf71154090f966535cd585835fb46eaf2165f2d [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.exec;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.extexecution.ExecutionDescriptor;
import org.netbeans.api.extexecution.print.ConvertedLine;
import org.netbeans.api.extexecution.print.LineConvertor;
import org.netbeans.api.options.OptionsDisplayer;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.modules.javascript.karma.browsers.Browser;
import org.netbeans.modules.javascript.karma.browsers.Browsers;
import org.netbeans.modules.javascript.karma.options.KarmaOptions;
import org.netbeans.modules.javascript.karma.options.KarmaOptionsValidator;
import org.netbeans.modules.javascript.karma.preferences.KarmaPreferencesValidator;
import org.netbeans.modules.javascript.karma.run.KarmaRunInfo;
import org.netbeans.modules.javascript.karma.run.TestRunner;
import org.netbeans.modules.javascript.karma.ui.KarmaErrorsDialog;
import org.netbeans.modules.javascript.karma.ui.options.KarmaOptionsPanelController;
import org.netbeans.modules.javascript.karma.util.FileUtils;
import org.netbeans.modules.javascript.karma.util.StringUtils;
import org.netbeans.modules.web.clientproject.api.jstesting.JsTestingProviders;
import org.netbeans.modules.web.common.api.ValidationResult;
import org.netbeans.modules.web.common.ui.api.ExternalExecutable;
import org.netbeans.spi.project.ui.CustomizerProvider2;
import org.openide.filesystems.FileUtil;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.windows.InputOutput;
import org.openide.windows.OutputEvent;
import org.openide.windows.OutputListener;
public class KarmaExecutable {
private static final Logger LOGGER = Logger.getLogger(KarmaExecutable.class.getName());
public static final String KARMA_NAME;
private static final String START_COMMAND = "start";
private static final String RUN_COMMAND = "run";
private static final String PORT_PARAMETER = "--port";
protected final String karmaPath;
protected final Project project;
static {
if (Utilities.isWindows()) {
KARMA_NAME = "karma.cmd"; // NOI18N
} else {
KARMA_NAME = "karma"; // NOI18N
}
}
KarmaExecutable(String karmaPath, Project project) {
assert karmaPath != null;
assert project != null;
this.karmaPath = karmaPath;
this.project = project;
}
@CheckForNull
public static KarmaExecutable getDefault(Project project, boolean showOptionsOrCustomizer) {
assert project != null;
// options
ValidationResult result = new KarmaOptionsValidator()
.validateKarma()
.getResult();
if (validateResult(result) != null) {
if (showOptionsOrCustomizer) {
OptionsDisplayer.getDefault().open(KarmaOptionsPanelController.OPTIONS_PATH);
}
return null;
}
// customizer
result = new KarmaPreferencesValidator()
.validate(project)
.getResult();
if (validateResult(result) != null) {
if (showOptionsOrCustomizer) {
project.getLookup().lookup(CustomizerProvider2.class).showCustomizer(JsTestingProviders.CUSTOMIZER_IDENT, null);
}
return null;
}
return createExecutable(KarmaOptions.getInstance().getKarma(), project);
}
private static KarmaExecutable createExecutable(String karma, Project project) {
if (Utilities.isMac()) {
return new MacKarmaExecutable(karma, project);
}
return new KarmaExecutable(karma, project);
}
@NbBundle.Messages({
"# {0} - project name",
"KarmaExecutable.start=Karma ({0})",
})
@CheckForNull
public Future<Integer> start(int port, KarmaRunInfo karmaRunInfo) {
final CountDownLatch countDownLatch = new CountDownLatch(1);
Runnable countDownTask = new Runnable() {
@Override
public void run() {
countDownLatch.countDown();
}
};
Future<Integer> task = getExecutable(Bundle.KarmaExecutable_start(ProjectUtils.getInformation(project).getDisplayName()), getProjectDir())
.additionalParameters(getStartParams(port, karmaRunInfo))
.environmentVariables(karmaRunInfo.getEnvVars())
.run(getStartDescriptor(karmaRunInfo, countDownTask));
assert task != null : karmaPath;
try {
countDownLatch.await(15, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
if (task.isDone()) {
// some error, task is not running
return null;
}
return task;
}
public void runTests(int port) {
getExecutable("karma run...", getProjectDir()) // NOI18N
.additionalParameters(getRunParams(port))
// XXX wait?
.run(getRunDescriptor());
}
private File getProjectDir() {
return FileUtil.toFile(project.getProjectDirectory());
}
String getCommand() {
return karmaPath;
}
private ExternalExecutable getExecutable(String title, File workDir) {
return new ExternalExecutable(getCommand())
.workDir(workDir)
.displayName(title)
.noOutput(false);
}
private ExecutionDescriptor getStartDescriptor(KarmaRunInfo karmaRunInfo, Runnable serverStartTask) {
return new ExecutionDescriptor()
.frontWindow(false)
.frontWindowOnError(false)
.outLineBased(true)
.errLineBased(true)
.outConvertorFactory(new ServerLineConvertorFactory(karmaRunInfo, serverStartTask))
.postExecution(serverStartTask);
}
private ExecutionDescriptor getRunDescriptor() {
return new ExecutionDescriptor()
.inputOutput(InputOutput.NULL);
}
List<String> getStartParams(int port, KarmaRunInfo karmaRunInfo) {
List<String> params = new ArrayList<>(4);
params.add(START_COMMAND);
params.add(karmaRunInfo.getNbConfigFile());
params.add(PORT_PARAMETER);
params.add(Integer.toString(port));
return params;
}
List<String> getRunParams(int port) {
List<String> params = new ArrayList<>(3);
params.add(RUN_COMMAND);
params.add(PORT_PARAMETER);
params.add(Integer.toString(port));
return params;
}
@CheckForNull
private static String validateResult(ValidationResult result) {
if (result.isFaultless()) {
return null;
}
if (result.hasErrors()) {
return result.getErrors().get(0).getMessage();
}
return result.getWarnings().get(0).getMessage();
}
//~ Inner classes
// #238974
private static final class MacKarmaExecutable extends KarmaExecutable {
private static final String BASH_COMMAND = "/bin/bash -lc"; // NOI18N
MacKarmaExecutable(String karmaPath, Project project) {
super(karmaPath, project);
}
@Override
String getCommand() {
return BASH_COMMAND;
}
@Override
List<String> getStartParams(int port, KarmaRunInfo karmaRunInfo) {
return getParams(super.getStartParams(port, karmaRunInfo));
}
@Override
List<String> getRunParams(int port) {
return getParams(super.getRunParams(port));
}
private List<String> getParams(List<String> originalParams) {
StringBuilder sb = new StringBuilder(200);
sb.append("\""); // NOI18N
sb.append(karmaPath);
sb.append("\" \""); // NOI18N
sb.append(StringUtils.implode(originalParams, "\" \"")); // NOI18N
sb.append("\""); // NOI18N
return Collections.singletonList(sb.toString());
}
}
private static final class ServerLineConvertorFactory implements ExecutionDescriptor.LineConvertorFactory {
private final LineConvertor serverLineConvertor;
public ServerLineConvertorFactory(KarmaRunInfo karmaRunInfo, Runnable startFinishedTask) {
assert karmaRunInfo != null;
assert startFinishedTask != null;
serverLineConvertor = new ServerLineConvertor(karmaRunInfo, startFinishedTask);
}
@Override
public LineConvertor newLineConvertor() {
return serverLineConvertor;
}
}
private static final class ServerLineConvertor implements LineConvertor {
private static final boolean DEBUG = Boolean.getBoolean("nb.karma.debug"); // NOI18N
private static final String NB_BROWSERS = "$NB$netbeans browsers "; // NOI18N
private static final String KARMA_ERROR = "[31mERROR ["; // NOI18N
private static final String KARMA_WARN = "[33mWARN ["; // NOI18N
private final KarmaRunInfo karmaRunInfo;
private final Runnable startFinishedTask;
private final TestRunner testRunner;
private boolean firstLine = true;
private boolean startFinishedTaskRun = false;
private Collection<Browser> browsers = null;
private int browserCount = -1;
private int connectedBrowsers = 0;
public ServerLineConvertor(KarmaRunInfo karmaRunInfo, Runnable startFinishedTask) {
assert karmaRunInfo != null;
assert startFinishedTask != null;
this.karmaRunInfo = karmaRunInfo;
this.startFinishedTask = startFinishedTask;
testRunner = new TestRunner(karmaRunInfo);
}
@Override
public List<ConvertedLine> convert(String line) {
// info
if (firstLine
&& line.contains(karmaRunInfo.getNbConfigFile())) {
firstLine = false;
return Collections.singletonList(ConvertedLine.forText(
line.replace(karmaRunInfo.getNbConfigFile(), karmaRunInfo.getProjectConfigFile()), null));
}
// startup
if (browsers == null) {
// server start
if (line.startsWith(NB_BROWSERS)) {
List<String> allBrowsers = StringUtils.explode(line.substring(NB_BROWSERS.length()), ","); // NOI18N
browserCount = allBrowsers.size();
browsers = Browsers.getBrowsers(allBrowsers);
return Collections.emptyList();
}
}
if (startFinishedTask != null
&& !startFinishedTaskRun
&& line.contains("Connected on socket")) { // NOI18N
assert browsers != null;
connectedBrowsers++;
if (connectedBrowsers == browserCount) {
startFinishedTask.run();
startFinishedTaskRun = true;
}
} else if (line.startsWith(TestRunner.NB_LINE)) {
// test result
testRunner.process(line);
if (DEBUG) {
return Collections.singletonList(ConvertedLine.forText(line, null));
}
return Collections.emptyList();
}
// some error before browser startup?
if (connectedBrowsers < browserCount
&& (line.contains(KARMA_ERROR) || line.contains(KARMA_WARN))) {
KarmaErrorsDialog.getInstance().show();
}
// process output
OutputListener outputListener = null;
if (browsers == null) {
// some error?
Pair<String, Integer> fileLine = FileLineParser.getOutputFileLine(line);
if (fileLine != null) {
outputListener = new FileOutputListener(fileLine.first(), fileLine.second());
}
return Collections.singletonList(ConvertedLine.forText(line, outputListener));
} else {
// karma log
for (Browser browser : browsers) {
Pair<String, Integer> fileLine = browser.getOutputFileLine(line);
if (fileLine != null) {
outputListener = new FileOutputListener(fileLine.first(), fileLine.second());
break;
}
}
}
testRunner.logMessageToTestResultsWindowOutputView(line);
return Collections.singletonList(ConvertedLine.forText(line, outputListener));
}
}
private static final class FileOutputListener implements OutputListener {
final String file;
final int line;
public FileOutputListener(String file, int line) {
assert file != null;
this.file = file;
this.line = line;
}
@Override
public void outputLineSelected(OutputEvent ev) {
// noop
}
@Override
public void outputLineAction(OutputEvent ev) {
RequestProcessor.getDefault().post(new Runnable() {
@Override
public void run() {
FileUtils.openFile(new File(file), line);
}
});
}
@Override
public void outputLineCleared(OutputEvent ev) {
// noop
}
}
static final class FileLineParser {
// (/usr/lib/node_modules/karma/node_modules/coffee-script/lib/coffee-script/coffee-script.js:211:36)
// ^/home/gapon/NetBeansProjects/Calculator-PHPUnit5/README.md:1$
static final Pattern OUTPUT_FILE_LINE_PATTERN = Pattern.compile("(?:^|\\()(?<FILE>[^(]+?):(?<LINE>\\d+)(?::\\d+)?(?:$|\\))"); // NOI18N
static Pair<String, Integer> getOutputFileLine(String line) {
Matcher matcher = OUTPUT_FILE_LINE_PATTERN.matcher(line);
if (!matcher.find()) {
return null;
}
String file = matcher.group("FILE"); // NOI18N
if (!new File(file).isFile()) {
// incomplete path
return null;
}
return Pair.of(file, Integer.valueOf(matcher.group("LINE"))); // NOI18N
}
}
}