aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-css-plugin/src
diff options
context:
space:
mode:
authorElena Vilchik2019-12-18 17:10:10 +0100
committerAlban Auzeill2019-12-18 17:10:10 +0100
commitc8f0071c4f5336dfe0efc5d3c218ab49f2401264 (patch)
tree254cd5ed9531d7c62bab4f8ec082e085795ecb8f /sonar-css-plugin/src
parent13fe08e87c8a70ffe6e248b774ef826bbe1f779d (diff)
downloadsonar-css-c8f0071c4f5336dfe0efc5d3c218ab49f2401264.tar.bz2
Rely on NodeJS API of Stylelint to execute CSS rules (#221)
Diffstat (limited to 'sonar-css-plugin/src')
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java50
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java235
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java10
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java72
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/Bundle.java (renamed from sonar-css-plugin/src/main/java/org/sonar/css/plugin/LinterCommandProvider.java)17
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java82
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java54
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java64
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java232
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java64
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java (renamed from sonar-css-plugin/src/test/java/org/sonar/css/plugin/AnalysisWarningsWrapperTest.java)24
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java (renamed from sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/BundleHandler.java)11
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/package-info.java (renamed from sonar-css-plugin/src/main/java/org/sonar/css/plugin/AnalysisWarningsWrapper.java)26
-rw-r--r--sonar-css-plugin/src/sonarcss-assembly.xml7
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java24
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java290
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java17
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintCommandProviderTest.java63
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssAnalyzerBundleTest.java85
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssBundleHandlerTest.java55
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServerTest.java254
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/NetUtilsTest.java55
-rw-r--r--sonar-css-plugin/src/test/resources/.DS_Storebin8196 -> 0 bytes
-rw-r--r--sonar-css-plugin/src/test/resources/bundle/.DS_Storebin6148 -> 0 bytes
-rw-r--r--sonar-css-plugin/src/test/resources/bundle/invalid-zip-file.zip0
-rw-r--r--sonar-css-plugin/src/test/resources/bundle/test-bundle.zipbin185 -> 0 bytes
-rw-r--r--sonar-css-plugin/src/test/resources/bundle/test-css-bundle.zipbin0 -> 575 bytes
-rw-r--r--sonar-css-plugin/src/test/resources/executables/mockError.js2
-rw-r--r--sonar-css-plugin/src/test/resources/executables/mockExit.js5
-rw-r--r--sonar-css-plugin/src/test/resources/executables/mockStylelint.js19
-rw-r--r--sonar-css-plugin/src/test/resources/executables/mockSyntaxError.js19
-rw-r--r--sonar-css-plugin/src/test/resources/executables/mockThrow.js3
-rw-r--r--sonar-css-plugin/src/test/resources/executables/mockUnknownRule.js19
-rw-r--r--sonar-css-plugin/src/test/resources/executables/oldNodeVersion.js1
-rw-r--r--sonar-css-plugin/src/test/resources/mock-start-server/startServer.js70
-rw-r--r--sonar-css-plugin/src/test/resources/mock-start-server/throw.js3
36 files changed, 1219 insertions, 713 deletions
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java
index d627527..a5df2fc 100644
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssPlugin.java
@@ -20,17 +20,13 @@
package org.sonar.css.plugin;
import org.sonar.api.Plugin;
-import org.sonar.api.SonarProduct;
-import org.sonar.api.SonarRuntime;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.resources.Qualifiers;
-import org.sonar.api.utils.Version;
-import org.sonar.css.plugin.bundle.CssBundleHandler;
+import org.sonar.css.plugin.bundle.CssAnalyzerBundle;
+import org.sonar.css.plugin.server.CssAnalyzerBridgeServer;
public class CssPlugin implements Plugin {
- private static final Version ANALYSIS_WARNINGS_MIN_SUPPORTED_SQ_VERSION = Version.create(7, 4);
-
static final String FILE_SUFFIXES_KEY = "sonar.css.file.suffixes";
public static final String FILE_SUFFIXES_DEFVALUE = ".css,.less,.scss";
@@ -45,16 +41,14 @@ public class CssPlugin implements Plugin {
@Override
public void define(Context context) {
- boolean externalIssuesSupported = context.getSonarQubeVersion().isGreaterThanOrEqual(Version.create(7, 2));
-
context.addExtensions(
MetricSensor.class,
CssLanguage.class,
SonarWayProfile.class,
- new CssRulesDefinition(externalIssuesSupported),
- CssBundleHandler.class,
+ CssRulesDefinition.class,
+ CssAnalyzerBundle.class,
+ CssAnalyzerBridgeServer.class,
CssRuleSensor.class,
- StylelintCommandProvider.class,
StylelintReportSensor.class,
MinifiedFilesFilter.class,
@@ -69,30 +63,16 @@ public class CssPlugin implements Plugin {
.build()
);
-
- if (externalIssuesSupported) {
- context.addExtension(
- PropertyDefinition.builder(STYLELINT_REPORT_PATHS)
- .defaultValue(STYLELINT_REPORT_PATHS_DEFAULT_VALUE)
- .name("Stylelint Report Files")
- .description("Paths (absolute or relative) to the JSON files with stylelint issues.")
- .onQualifiers(Qualifiers.PROJECT)
- .subCategory(LINTER_SUBCATEGORY)
- .category(CSS_CATEGORY)
- .multiValues(true)
- .build());
- }
-
- if (isAnalysisWarningsSupported(context.getRuntime())) {
- context.addExtension(AnalysisWarningsWrapper.class);
- }
+ context.addExtension(
+ PropertyDefinition.builder(STYLELINT_REPORT_PATHS)
+ .defaultValue(STYLELINT_REPORT_PATHS_DEFAULT_VALUE)
+ .name("Stylelint Report Files")
+ .description("Paths (absolute or relative) to the JSON files with stylelint issues.")
+ .onQualifiers(Qualifiers.PROJECT)
+ .subCategory(LINTER_SUBCATEGORY)
+ .category(CSS_CATEGORY)
+ .multiValues(true)
+ .build());
}
- /**
- * Drop this and related when the minimum supported version of SonarQube API reaches 7.4.
- */
- private static boolean isAnalysisWarningsSupported(SonarRuntime runtime) {
- return runtime.getApiVersion().isGreaterThanOrEqual(ANALYSIS_WARNINGS_MIN_SUPPORTED_SQ_VERSION)
- && runtime.getProduct() != SonarProduct.SONARLINT;
- }
}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java
index 87328d7..644ece8 100644
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRuleSensor.java
@@ -21,16 +21,22 @@ package org.sonar.css.plugin;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSyntaxException;
import java.io.File;
import java.io.IOException;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
-import java.nio.file.Paths;
import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
+import org.sonar.api.batch.fs.FilePredicate;
+import org.sonar.api.batch.fs.FilePredicates;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.rule.CheckFactory;
@@ -39,109 +45,180 @@ import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
+import org.sonar.api.notifications.AnalysisWarnings;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.css.plugin.CssRules.StylelintConfig;
-import org.sonar.css.plugin.StylelintReport.Issue;
-import org.sonar.css.plugin.StylelintReport.IssuesPerFile;
-import org.sonar.css.plugin.bundle.BundleHandler;
-import org.sonarsource.nodejs.NodeCommand;
+import org.sonar.css.plugin.server.AnalyzerBridgeServer.Issue;
+import org.sonar.css.plugin.server.CssAnalyzerBridgeServer;
+import org.sonar.css.plugin.server.AnalyzerBridgeServer.Request;
+import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException;
+import org.sonarsource.analyzer.commons.ProgressReport;
import org.sonarsource.nodejs.NodeCommandException;
public class CssRuleSensor implements Sensor {
private static final Logger LOG = Loggers.get(CssRuleSensor.class);
+ private static final String CONFIG_PATH = "css-bundle/stylelintconfig.json";
- private final BundleHandler bundleHandler;
private final CssRules cssRules;
- private final LinterCommandProvider linterCommandProvider;
- @Nullable
- private final AnalysisWarningsWrapper analysisWarnings;
+ private final CssAnalyzerBridgeServer cssAnalyzerBridgeServer;
+ private final AnalysisWarnings analysisWarnings;
+
public CssRuleSensor(
- BundleHandler bundleHandler,
CheckFactory checkFactory,
- LinterCommandProvider linterCommandProvider,
- @Nullable AnalysisWarningsWrapper analysisWarnings
+ CssAnalyzerBridgeServer cssAnalyzerBridgeServer,
+ @Nullable AnalysisWarnings analysisWarnings
) {
- this.bundleHandler = bundleHandler;
- this.linterCommandProvider = linterCommandProvider;
this.cssRules = new CssRules(checkFactory);
+ this.cssAnalyzerBridgeServer = cssAnalyzerBridgeServer;
this.analysisWarnings = analysisWarnings;
}
- public CssRuleSensor(
- BundleHandler bundleHandler,
- CheckFactory checkFactory,
- LinterCommandProvider linterCommandProvider
- ) {
- this(bundleHandler, checkFactory, linterCommandProvider, null);
- }
-
@Override
public void describe(SensorDescriptor descriptor) {
descriptor
+ .createIssuesForRuleRepository("css")
.name("SonarCSS Rules");
}
@Override
public void execute(SensorContext context) {
+ reportOldNodeProperty(context);
+
+ boolean failFast = context.config().getBoolean("sonar.internal.analysis.failFast").orElse(false);
+
+ try {
+ List<InputFile> inputFiles = getInputFiles(context);
+ if (inputFiles.isEmpty()) {
+ LOG.info("No CSS, PHP or HTML files are found in the project. CSS analysis is skipped.");
+ } else {
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ File configFile = createLinterConfig(context);
+ analyzeFiles(context, inputFiles, configFile);
+ }
+ } catch (CancellationException e) {
+ // do not propagate the exception
+ LOG.info(e.toString());
+ } catch (ServerAlreadyFailedException e) {
+ LOG.debug("Skipping start of css-bundle server due to the failure during first analysis");
+ LOG.debug("Skipping execution of CSS rules due to the problems with css-bundle server");
+ } catch (NodeCommandException e) {
+ LOG.error(e.getMessage(), e);
+ reportAnalysisWarning("CSS rules were not executed. " + e.getMessage());
+ if (failFast) {
+ throw new IllegalStateException("Analysis failed (\"sonar.internal.analysis.failFast\"=true)", e);
+ }
+ } catch (Exception e) {
+ LOG.error("Failure during analysis, " + cssAnalyzerBridgeServer.getCommandInfo(), e);
+ if (failFast) {
+ throw new IllegalStateException("Analysis failed (\"sonar.internal.analysis.failFast\"=true)", e);
+ }
+ }
+ }
+
+ private void reportOldNodeProperty(SensorContext context) {
if (context.config().hasKey(CssPlugin.FORMER_NODE_EXECUTABLE)) {
String msg = "Property '" + CssPlugin.FORMER_NODE_EXECUTABLE + "' is ignored, 'sonar.nodejs.executable' should be used instead";
LOG.warn(msg);
- if (analysisWarnings != null) {
- analysisWarnings.addUnique(msg);
+ reportAnalysisWarning(msg);
+ }
+ }
+
+ void analyzeFiles(SensorContext context, List<InputFile> inputFiles, File configFile) throws InterruptedException, IOException {
+ ProgressReport progressReport = new ProgressReport("Analysis progress", TimeUnit.SECONDS.toMillis(10));
+ boolean success = false;
+ try {
+ progressReport.start(inputFiles.stream().map(InputFile::toString).collect(Collectors.toList()));
+ for (InputFile inputFile : inputFiles) {
+ if (context.isCancelled()) {
+ throw new CancellationException("Analysis interrupted because the SensorContext is in cancelled state");
+ }
+ if (cssAnalyzerBridgeServer.isAlive()) {
+ try {
+ analyzeFile(context, inputFile, configFile);
+ } catch (IOException | RuntimeException e) {
+ throw new IOException("Failure during analysis of " + inputFile.uri() + ": " + e.getMessage());
+ }
+ progressReport.nextFile();
+ } else {
+ throw new IllegalStateException("css-bundle server is not answering");
+ }
}
+ success = true;
+ } finally {
+ if (success) {
+ progressReport.stop();
+ } else {
+ progressReport.cancel();
+ }
+ progressReport.join();
}
+ }
- if (cssRules.isEmpty()) {
- LOG.warn("No rules are activated in CSS Quality Profile");
+ void analyzeFile(SensorContext context, InputFile inputFile, File configFile) throws IOException {
+ URI uri = inputFile.uri();
+ if (!"file".equalsIgnoreCase(uri.getScheme())) {
+ LOG.debug("Skipping {} as it has not 'file' scheme", uri);
return;
}
+ Request request = new Request(new File(uri).getAbsolutePath(), configFile.toString());
+ LOG.debug("Analyzing " + request.filePath);
+ Issue[] issues = cssAnalyzerBridgeServer.analyze(request);
+ LOG.debug("Found {} issue(s)", issues.length);
+ saveIssues(context, inputFile, issues);
+ }
- File deployDestination = context.fileSystem().workDir();
+ private void saveIssues(SensorContext context, InputFile inputFile, Issue[] issues) {
+ for (Issue issue : issues) {
+ NewIssue sonarIssue = context.newIssue();
- try {
- bundleHandler.deployBundle(deployDestination);
- createLinterConfig(deployDestination);
- StringBuilder output = new StringBuilder();
+ RuleKey ruleKey = cssRules.getActiveSonarKey(issue.rule);
- NodeCommand nodeCommand = linterCommandProvider.nodeCommand(deployDestination, context, output::append, LOG::error);
- LOG.debug("Starting process: " + nodeCommand.toString());
- nodeCommand.start();
+ if (ruleKey == null) {
+ if ("CssSyntaxError".equals(issue.rule)) {
+ String errorMessage = issue.text.replace("(CssSyntaxError)", "").trim();
+ LOG.error("Failed to parse {}, line {}, {}", inputFile.uri(), issue.line, errorMessage);
+ } else {
+ LOG.error("Unknown stylelint rule or rule not enabled: '" + issue.rule + "'");
+ }
- if (isSuccessful(nodeCommand.waitFor())) {
- saveIssues(context, output.toString());
- }
- } catch (NodeCommandException e) {
- LOG.error(e.getMessage() + " No CSS files will be analyzed.", e);
- if (analysisWarnings != null) {
- analysisWarnings.addUnique("CSS files were not analyzed. " + e.getMessage());
+ } else {
+ NewIssueLocation location = sonarIssue.newLocation()
+ .on(inputFile)
+ .at(inputFile.selectLine(issue.line))
+ .message(normalizeMessage(issue.text));
+
+ sonarIssue
+ .at(location)
+ .forRule(ruleKey)
+ .save();
}
- } catch (Exception e) {
- LOG.error("Failed to run external linting process", e);
}
}
- private boolean isSuccessful(int exitValue) {
- // exit codes 0 and 2 are expected. 0 - means no issues were found, 2 - means that at least one "error-level" rule found issue
- // see https://github.com/stylelint/stylelint/blob/master/docs/user-guide/cli.md#exit-codes
- boolean isSuccessful = exitValue == 0 || exitValue == 2;
- if (!isSuccessful) {
- LOG.error("Analysis didn't terminate normally, please verify ERROR and WARN logs above. Exit code {}", exitValue);
- }
- return isSuccessful;
+ private static List<InputFile> getInputFiles(SensorContext context) {
+ FileSystem fileSystem = context.fileSystem();
+ FilePredicates predicates = context.fileSystem().predicates();
+ FilePredicate mainFilePredicate = predicates.and(
+ fileSystem.predicates().hasType(InputFile.Type.MAIN),
+ fileSystem.predicates().hasLanguages(CssLanguage.KEY, "php", "web"));
+ return StreamSupport.stream(fileSystem.inputFiles(mainFilePredicate).spliterator(), false)
+ .collect(Collectors.toList());
}
- private void createLinterConfig(File deployDestination) throws IOException {
- String configPath = linterCommandProvider.configPath(deployDestination);
+ private File createLinterConfig(SensorContext context) throws IOException {
StylelintConfig config = cssRules.getConfig();
final GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(StylelintConfig.class, config);
final Gson gson = gsonBuilder.create();
String configAsJson = gson.toJson(config);
- Files.write(Paths.get(configPath), Collections.singletonList(configAsJson), StandardCharsets.UTF_8);
+ File configFile = new File(context.fileSystem().workDir(), CONFIG_PATH).getAbsoluteFile();
+ Files.createDirectories(configFile.toPath().getParent());
+ Files.write(configFile.toPath(), Collections.singletonList(configAsJson), StandardCharsets.UTF_8);
+ return configFile;
}
private static String normalizeMessage(String message) {
@@ -155,51 +232,9 @@ public class CssRuleSensor implements Sensor {
}
}
- private void saveIssues(SensorContext context, String issuesAsJson) {
- IssuesPerFile[] issues;
- try {
- issues = new Gson().fromJson(issuesAsJson, IssuesPerFile[].class);
- } catch (JsonSyntaxException e) {
- throw new IllegalStateException("Failed to parse JSON result of external linting process execution: \n-------\n" + issuesAsJson + "\n-------", e);
- }
-
- FileSystem fileSystem = context.fileSystem();
-
- for (IssuesPerFile issuesPerFile : issues) {
- InputFile inputFile = fileSystem.inputFile(fileSystem.predicates().hasAbsolutePath(issuesPerFile.source));
-
- if (inputFile != null) {
- for (Issue issue : issuesPerFile.warnings) {
- saveIssue(context, inputFile, issue);
- }
- }
+ private void reportAnalysisWarning(String message) {
+ if (analysisWarnings != null) {
+ analysisWarnings.addUnique(message);
}
}
-
- private void saveIssue(SensorContext context, InputFile inputFile, Issue issue) {
- NewIssue sonarIssue = context.newIssue();
-
- RuleKey ruleKey = cssRules.getActiveSonarKey(issue.rule);
-
- if (ruleKey == null) {
- if ("CssSyntaxError".equals(issue.rule)) {
- String errorMessage = issue.text.replace("(CssSyntaxError)", "").trim();
- LOG.error("Failed to parse {}, line {}, {}", inputFile.uri(), issue.line, errorMessage);
- } else {
- LOG.error("Unknown stylelint rule or rule not enabled: '" + issue.rule + "'");
- }
-
- } else {
- NewIssueLocation location = sonarIssue.newLocation()
- .on(inputFile)
- .at(inputFile.selectLine(issue.line))
- .message(normalizeMessage(issue.text));
-
- sonarIssue
- .at(location)
- .forRule(ruleKey)
- .save();
- }
- }
-
}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java
index cc88ad3..4d0d959 100644
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/CssRulesDefinition.java
@@ -31,12 +31,6 @@ public class CssRulesDefinition implements RulesDefinition {
public static final String RESOURCE_FOLDER = "org/sonar/l10n/css/rules/";
- private final boolean externalIssuesSupported;
-
- public CssRulesDefinition(boolean externalIssuesSupported) {
- this.externalIssuesSupported = externalIssuesSupported;
- }
-
@Override
public void define(Context context) {
NewRepository repository = context
@@ -47,8 +41,6 @@ public class CssRulesDefinition implements RulesDefinition {
ruleMetadataLoader.addRulesByAnnotatedClass(repository, CssRules.getRuleClasses());
repository.done();
- if (externalIssuesSupported) {
- StylelintReportSensor.getStylelintRuleLoader().createExternalRuleRepository(context);
- }
+ StylelintReportSensor.getStylelintRuleLoader().createExternalRuleRepository(context);
}
}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java
deleted file mode 100644
index c43af5c..0000000
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/StylelintCommandProvider.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * SonarCSS
- * Copyright (C) 2018-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.css.plugin;
-
-import java.io.File;
-import java.nio.file.Paths;import java.util.Arrays;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.sonar.api.batch.ScannerSide;
-import org.sonar.api.batch.sensor.SensorContext;
-import org.sonarsource.nodejs.NodeCommand;
-
-@ScannerSide
-public class StylelintCommandProvider implements LinterCommandProvider {
-
- private static final String CONFIG_PATH = "css-bundle/stylelintconfig.json";
- private static final List<String> LANGUAGES_TO_ANALYZE = Arrays.asList("css", "html", "php");
-
- @Override
- public NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error) {
- String projectBaseDir = context.fileSystem().baseDir().getAbsolutePath();
-
- List<String> suffixes = LANGUAGES_TO_ANALYZE.stream()
- .map(language -> context.config().getStringArray("sonar." + language + ".file.suffixes"))
- .flatMap(Stream::of)
- .collect(Collectors.toList());
-
- String filesGlob = "**" + File.separator + "*{" + String.join(",", suffixes) + "}";
- String filesToAnalyze = Paths.get(projectBaseDir, "TOREPLACE").toString();
- filesToAnalyze = filesToAnalyze.replace("TOREPLACE", filesGlob);
-
- String[] args = {
- new File(deployDestination, "css-bundle/node_modules/stylelint/bin/stylelint").getAbsolutePath(),
- filesToAnalyze,
- "--config", new File(deployDestination, CONFIG_PATH).getAbsolutePath(),
- "-f", "json"
- };
-
- return NodeCommand.builder()
- .outputConsumer(output)
- .errorConsumer(error)
- .minNodeVersion(6)
- .configuration(context.config())
- .nodeJsArgs(args)
- .build();
- }
-
- @Override
- public String configPath(File deployDestination) {
- return new File(deployDestination, CONFIG_PATH).getAbsolutePath();
- }
-
-}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/LinterCommandProvider.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/Bundle.java
index 74a343c..8e9ce9b 100644
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/LinterCommandProvider.java
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/Bundle.java
@@ -17,17 +17,18 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.css.plugin;
+package org.sonar.css.plugin.bundle;
-import java.io.File;
-import java.util.function.Consumer;
-import org.sonar.api.batch.sensor.SensorContext;
-import org.sonarsource.nodejs.NodeCommand;
+import java.io.IOException;
+import java.nio.file.Path;
-public interface LinterCommandProvider {
+public interface Bundle {
- NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error);
+ void deploy(Path deployLocation) throws IOException;
- String configPath(File deployDestination);
+ /**
+ * should be called after deploy(Path deployLocation)
+ */
+ String startServerScript();
}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java
new file mode 100644
index 0000000..ba5d714
--- /dev/null
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssAnalyzerBundle.java
@@ -0,0 +1,82 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin.bundle;
+
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.sonar.api.internal.google.common.annotations.VisibleForTesting;
+import org.sonar.api.scanner.ScannerSide;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.css.plugin.Zip;
+import org.sonarsource.api.sonarlint.SonarLintSide;
+
+import static org.sonarsource.api.sonarlint.SonarLintSide.MULTIPLE_ANALYSES;
+
+@ScannerSide
+@SonarLintSide(lifespan = MULTIPLE_ANALYSES)
+public class CssAnalyzerBundle implements Bundle {
+
+ private static final Logger LOG = Loggers.get(CssAnalyzerBundle.class);
+ private static final Profiler PROFILER = Profiler.createIfDebug(LOG);
+
+ // this archive is created in css-bundle module
+ private static final String DEFAULT_BUNDLE_LOCATION = "/css-bundle.zip";
+ private static final Path DEFAULT_STARTUP_SCRIPT = Paths.get("css-bundle", "bin", "server");
+
+ final String bundleLocation;
+
+ private String startServerScript = DEFAULT_STARTUP_SCRIPT.toString();
+
+ public CssAnalyzerBundle() {
+ this(DEFAULT_BUNDLE_LOCATION);
+ }
+
+ @VisibleForTesting
+ CssAnalyzerBundle(String bundleLocation) {
+ this.bundleLocation = bundleLocation;
+ }
+
+ @Override
+ public void deploy(Path deployLocation) {
+ PROFILER.startDebug("Deploying bundle");
+ LOG.debug("Deploying css-bundle into {}", deployLocation);
+ InputStream bundle = getClass().getResourceAsStream(bundleLocation);
+ if (bundle == null) {
+ throw new IllegalStateException("css-bundle not found in " + bundleLocation);
+ }
+ try {
+ LOG.debug("Deploying css-bundle to {}", deployLocation.toAbsolutePath());
+ Zip.extract(bundle, deployLocation.toFile());
+ startServerScript = deployLocation.resolve(DEFAULT_STARTUP_SCRIPT).toAbsolutePath().toString();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to deploy css-bundle (with classpath '" + bundleLocation + "')", e);
+ }
+ PROFILER.stopDebug();
+ }
+
+ @Override
+ public String startServerScript() {
+ return startServerScript;
+ }
+
+}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java
deleted file mode 100644
index 41019b5..0000000
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/CssBundleHandler.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * SonarCSS
- * Copyright (C) 2018-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.css.plugin.bundle;
-
-import java.io.File;
-import java.io.InputStream;
-import org.sonar.api.batch.ScannerSide;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.css.plugin.Zip;
-
-@ScannerSide
-public class CssBundleHandler implements BundleHandler {
-
- private static final String BUNDLE_LOCATION = "/css-bundle.zip";
- private static final Logger LOG = Loggers.get(CssBundleHandler.class);
- String bundleLocation = BUNDLE_LOCATION;
-
- /**
- * Extracting "css-bundle.zip" (containing stylelint)
- * to deployDestination (".sonar" directory of the analyzed project).
- */
- @Override
- public void deployBundle(File deployDestination) {
- InputStream bundle = getClass().getResourceAsStream(bundleLocation);
- if (bundle == null) {
- throw new IllegalStateException("CSS bundle not found at " + bundleLocation);
- }
- try {
- LOG.debug("Deploying bundle to {}", deployDestination.getAbsolutePath());
- Zip.extract(bundle, deployDestination);
- } catch (Exception e) {
- throw new IllegalStateException("Failed to deploy CSS bundle (with classpath '" + bundleLocation + "')", e);
- }
- }
-
-}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java
new file mode 100644
index 0000000..0656b06
--- /dev/null
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java
@@ -0,0 +1,64 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin.server;
+
+import java.io.IOException;
+import org.sonar.api.Startable;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.scanner.ScannerSide;
+import org.sonarsource.api.sonarlint.SonarLintSide;
+
+import static org.sonarsource.api.sonarlint.SonarLintSide.MULTIPLE_ANALYSES;
+
+@ScannerSide
+@SonarLintSide(lifespan = MULTIPLE_ANALYSES)
+public interface AnalyzerBridgeServer extends Startable {
+
+ void startServerLazily(SensorContext context) throws IOException;
+
+ Issue[] analyze(Request request) throws IOException;
+
+ String getCommandInfo();
+
+ boolean isAlive();
+
+ class Request {
+ public final String filePath;
+ public final String configFile;
+
+ public Request(String filePath, String configFile) {
+ this.filePath = filePath;
+ this.configFile = configFile;
+ }
+ }
+
+ class Issue {
+ public final Integer line;
+ public final String rule;
+ public final String text;
+
+ public Issue(Integer line, String rule, String text) {
+ this.line = line;
+ this.rule = rule;
+ this.text = text;
+ }
+ }
+
+}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java
new file mode 100644
index 0000000..fb8ee45
--- /dev/null
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServer.java
@@ -0,0 +1,232 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin.server;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import java.io.File;
+import java.io.IOException;
+import java.time.Duration;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.css.plugin.bundle.Bundle;
+import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException;
+import org.sonarsource.nodejs.NodeCommand;
+import org.sonarsource.nodejs.NodeCommandBuilder;
+import org.sonarsource.nodejs.NodeCommandException;
+
+public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer {
+
+ private static final Logger LOG = Loggers.get(CssAnalyzerBridgeServer.class);
+ private static final Profiler PROFILER = Profiler.createIfDebug(LOG);
+
+ private static final int DEFAULT_TIMEOUT_SECONDS = 60;
+ // internal property to set "--max-old-space-size" for Node process running this server
+ private static final String MAX_OLD_SPACE_SIZE_PROPERTY = "sonar.css.node.maxspace";
+ private static final Gson GSON = new Gson();
+
+ private final OkHttpClient client;
+ private final NodeCommandBuilder nodeCommandBuilder;
+ final int timeoutSeconds;
+ private final Bundle bundle;
+ private int port;
+ private NodeCommand nodeCommand;
+ private boolean failedToStart;
+
+ // Used by pico container for dependency injection
+ @SuppressWarnings("unused")
+ public CssAnalyzerBridgeServer(Bundle bundle) {
+ this(NodeCommand.builder(), DEFAULT_TIMEOUT_SECONDS, bundle);
+ }
+
+ protected CssAnalyzerBridgeServer(NodeCommandBuilder nodeCommandBuilder, int timeoutSeconds, Bundle bundle) {
+ this.nodeCommandBuilder = nodeCommandBuilder;
+ this.timeoutSeconds = timeoutSeconds;
+ this.bundle = bundle;
+ this.client = new OkHttpClient.Builder()
+ .callTimeout(Duration.ofSeconds(timeoutSeconds))
+ .readTimeout(Duration.ofSeconds(timeoutSeconds))
+ .build();
+ }
+
+ // for testing purposes
+ public void deploy(File deployLocation) throws IOException {
+ bundle.deploy(deployLocation.toPath());
+ }
+
+ public void startServer(SensorContext context) throws IOException {
+ PROFILER.startDebug("Starting server");
+ port = NetUtils.findOpenPort();
+
+ File scriptFile = new File(bundle.startServerScript());
+ if (!scriptFile.exists()) {
+ throw new NodeCommandException("Node.js script to start css-bundle server doesn't exist: " + scriptFile.getAbsolutePath());
+ }
+
+ initNodeCommand(context, scriptFile);
+
+ LOG.debug("Starting Node.js process to start css-bundle server at port " + port);
+ nodeCommand.start();
+
+ if (!NetUtils.waitServerToStart("localhost", port, timeoutSeconds * 1000)) {
+ throw new NodeCommandException("Failed to start server (" + timeoutSeconds + "s timeout)");
+ }
+ PROFILER.stopDebug();
+ }
+
+ private void initNodeCommand(SensorContext context, File scriptFile) {
+ nodeCommandBuilder
+ .outputConsumer(message -> {
+ if (message.startsWith("DEBUG")) {
+ LOG.debug(message.substring(5).trim());
+ } else if (message.startsWith("WARN")) {
+ LOG.warn(message.substring(4).trim());
+ } else {
+ LOG.info(message);
+ }
+ })
+ .minNodeVersion(8)
+ .configuration(context.config())
+ .script(scriptFile.getAbsolutePath())
+ .scriptArgs(String.valueOf(port));
+
+ context.config()
+ .getInt(MAX_OLD_SPACE_SIZE_PROPERTY)
+ .ifPresent(nodeCommandBuilder::maxOldSpaceSize);
+
+ nodeCommand = nodeCommandBuilder.build();
+ }
+
+ @Override
+ public void startServerLazily(SensorContext context) throws IOException {
+ // required for SonarLint context to avoid restarting already failed server
+ if (failedToStart) {
+ throw new ServerAlreadyFailedException();
+ }
+
+ try {
+ if (isAlive()) {
+ LOG.debug("css-bundle server is up, no need to start.");
+ return;
+ }
+ deploy(context.fileSystem().workDir());
+ startServer(context);
+ } catch (NodeCommandException e) {
+ failedToStart = true;
+ throw e;
+ }
+ }
+
+ @Override
+ public Issue[] analyze(Request request) throws IOException {
+ String json = GSON.toJson(request);
+ return parseResponse(request(json));
+ }
+
+ private String request(String json) throws IOException {
+ okhttp3.Request request = new okhttp3.Request.Builder()
+ .url(url("analyze"))
+ .post(RequestBody.create(MediaType.get("application/json"), json))
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ // in this case response.body() is never null (according to docs)
+ return response.body().string();
+ }
+ }
+
+ private static Issue[] parseResponse(String result) {
+ try {
+ return GSON.fromJson(result, Issue[].class);
+ } catch (JsonSyntaxException e) {
+ String msg = "Failed to parse response: \n-----\n" + result + "\n-----\n";
+ LOG.error(msg, e);
+ throw new IllegalStateException("Failed to parse response", e);
+ }
+ }
+
+ public boolean isAlive() {
+ if (nodeCommand == null) {
+ return false;
+ }
+ okhttp3.Request request = new okhttp3.Request.Builder()
+ .url(url("status"))
+ .get()
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ String body = response.body().string();
+ // in this case response.body() is never null (according to docs)
+ return "OK!".equals(body);
+ } catch (IOException e) {
+ LOG.error("Error requesting server status. Server is probably dead.", e);
+ return false;
+ }
+ }
+
+ @Override
+ public String getCommandInfo() {
+ if (nodeCommand == null) {
+ return "Node.js command to start css-bundle server was not built yet.";
+ } else {
+ return "Node.js command to start css-bundle was: " + nodeCommand.toString();
+ }
+ }
+
+ @Override
+ public void start() {
+ // Server is started lazily by the sensor
+ }
+
+ @Override
+ public void stop() {
+ clean();
+ }
+
+ void clean() {
+ if (nodeCommand != null) {
+ nodeCommand.destroy();
+ nodeCommand = null;
+ }
+ }
+
+ private HttpUrl url(String endpoint) {
+ HttpUrl.Builder builder = new HttpUrl.Builder();
+ return builder
+ .scheme("http")
+ .host("localhost")
+ .port(port)
+ .addPathSegment(endpoint)
+ .build();
+ }
+
+ // for testing purposes
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java
new file mode 100644
index 0000000..fd3ab85
--- /dev/null
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/NetUtils.java
@@ -0,0 +1,64 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin.server;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketAddress;
+
+public class NetUtils {
+
+ private NetUtils() {
+ }
+
+ public static int findOpenPort() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return socket.getLocalPort();
+ }
+ }
+
+ public static boolean waitServerToStart(String host, int port, int timeoutMs) {
+ int sleepStep = 20;
+ SocketAddress address = new InetSocketAddress(host, port);
+ long start = System.currentTimeMillis();
+ try {
+ while (!serverListening(address)) {
+ if (System.currentTimeMillis() - start > timeoutMs) {
+ return false;
+ }
+ Thread.sleep(sleepStep);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return true;
+ }
+
+ private static boolean serverListening(SocketAddress address) {
+ try (Socket s = new Socket()) {
+ s.connect(address, 1);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/AnalysisWarningsWrapperTest.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java
index fc93670..0865aa4 100644
--- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/AnalysisWarningsWrapperTest.java
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java
@@ -17,23 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.css.plugin;
+package org.sonar.css.plugin.server.exception;
-import org.junit.Test;
-import org.sonar.api.notifications.AnalysisWarnings;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-public class AnalysisWarningsWrapperTest {
- @Test
- public void delegate_to_analysisWarnings() {
- AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
-
- AnalysisWarningsWrapper wrapper = new AnalysisWarningsWrapper(analysisWarnings);
-
- String warning = "some warning";
- wrapper.addUnique(warning);
- verify(analysisWarnings).addUnique(warning);
- }
+/**
+ * This exception is required to inform sensor about analyzer bridge server start up failure in SonarLint
+ * It is required to not try to start it again
+ */
+public class ServerAlreadyFailedException extends RuntimeException {
}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/BundleHandler.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java
index 3000751..4f5b753 100644
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/bundle/BundleHandler.java
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java
@@ -17,12 +17,5 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.css.plugin.bundle;
-
-import java.io.File;
-
-public interface BundleHandler {
-
- void deployBundle(File deployDestination);
-
-}
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.css.plugin.server.exception;
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/AnalysisWarningsWrapper.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/package-info.java
index 84871bf..66fd848 100644
--- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/AnalysisWarningsWrapper.java
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/package-info.java
@@ -17,27 +17,5 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-package org.sonar.css.plugin;
-
-import org.sonar.api.batch.InstantiationStrategy;
-import org.sonar.api.batch.ScannerSide;
-import org.sonar.api.notifications.AnalysisWarnings;
-
-/**
- * Wrap an AnalysisWarnings instance, available since SQ API 7.4.
- * Do not load this class on older runtimes.
- * Drop this class when the minimum supported version of SonarQube API reaches 7.4.
- */
-@ScannerSide
-@InstantiationStrategy("PER_BATCH")
-public class AnalysisWarningsWrapper {
- private final AnalysisWarnings analysisWarnings;
-
- public AnalysisWarningsWrapper(AnalysisWarnings analysisWarnings) {
- this.analysisWarnings = analysisWarnings;
- }
-
- public void addUnique(String text) {
- this.analysisWarnings.addUnique(text);
- }
-}
+@javax.annotation.ParametersAreNonnullByDefault
+package org.sonar.css.plugin.server;
diff --git a/sonar-css-plugin/src/sonarcss-assembly.xml b/sonar-css-plugin/src/sonarcss-assembly.xml
index dcbab0f..3bd1f9a 100644
--- a/sonar-css-plugin/src/sonarcss-assembly.xml
+++ b/sonar-css-plugin/src/sonarcss-assembly.xml
@@ -10,11 +10,10 @@
<fileSet>
<directory>css-bundle</directory>
<includes>
- <include>**/*</include>
+ <include>lib/**/*</include>
+ <include>bin/**/*</include>
+ <include>node_modules/**/*</include>
</includes>
- <excludes>
- <exclude>package-lock.json</exclude>
- </excludes>
</fileSet>
</fileSets>
</assembly>
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java
index a2e420e..b0f1b28 100644
--- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java
+++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssPluginTest.java
@@ -33,16 +33,7 @@ public class CssPluginTest {
@Test
public void count_extensions() {
- SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(6, 7), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY);
- Plugin.Context context = new Plugin.Context(runtime);
- Plugin underTest = new CssPlugin();
- underTest.define(context);
- assertThat(context.getExtensions()).hasSize(10);
- }
-
- @Test
- public void count_extensions_7_2() {
- SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 2), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY);
+ SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 9), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY);
Plugin.Context context = new Plugin.Context(runtime);
Plugin underTest = new CssPlugin();
underTest.define(context);
@@ -50,17 +41,8 @@ public class CssPluginTest {
}
@Test
- public void count_extensions_7_4() {
- SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.create(7, 4), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY);
- Plugin.Context context = new Plugin.Context(runtime);
- Plugin underTest = new CssPlugin();
- underTest.define(context);
- assertThat(context.getExtensions()).hasSize(12);
- }
-
- @Test
- public void count_extensions_7_4_sonarlint() {
- SonarRuntime runtime = SonarRuntimeImpl.forSonarLint(Version.create(7, 4));
+ public void count_extensions_sonarlint() {
+ SonarRuntime runtime = SonarRuntimeImpl.forSonarLint(Version.create(7, 9));
Plugin.Context context = new Plugin.Context(runtime);
Plugin underTest = new CssPlugin();
underTest.define(context);
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java
index a0a41f8..0dd35d8 100644
--- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java
+++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRuleSensorTest.java
@@ -21,41 +21,40 @@ package org.sonar.css.plugin;
import java.io.File;
import java.io.IOException;
+import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import javax.annotation.Nullable;
-import org.awaitility.Awaitility;
+import java.util.Collections;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.InputFile.Type;
import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
import org.sonar.api.batch.rule.CheckFactory;
-import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor;
import org.sonar.api.batch.sensor.internal.SensorContextTester;
-import org.sonar.api.batch.sensor.issue.Issue;
+import org.sonar.api.notifications.AnalysisWarnings;
import org.sonar.api.utils.log.LogTester;
import org.sonar.api.utils.log.LoggerLevel;
-import org.sonar.css.plugin.bundle.BundleHandler;
-import org.sonar.css.plugin.bundle.CssBundleHandler;
-import org.sonarsource.nodejs.NodeCommand;
-import org.sonarsource.nodejs.NodeCommandException;
+import org.sonar.css.plugin.server.CssAnalyzerBridgeServer;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.css.plugin.server.CssAnalyzerBridgeServerTest.createCssAnalyzerBridgeServer;
public class CssRuleSensorTest {
@@ -66,235 +65,212 @@ public class CssRuleSensorTest {
public TemporaryFolder tmpDir = new TemporaryFolder();
@Rule
- public ExpectedException thrown= ExpectedException.none();
+ public ExpectedException thrown = ExpectedException.none();
- private static CheckFactory checkFactory = new CheckFactory(new TestActiveRules("S4647", "S4656"));
+ private static final CheckFactory CHECK_FACTORY = new CheckFactory(new TestActiveRules("S4647", "S4656", "S4658"));
private static final File BASE_DIR = new File("src/test/resources").getAbsoluteFile();
private SensorContextTester context = SensorContextTester.create(BASE_DIR);
- private DefaultInputFile inputFile = createInputFile(context, "some css content\n on 2 lines", "dir/file.css");
- private AnalysisWarningsWrapper analysisWarnings = mock(AnalysisWarningsWrapper.class);
+ private AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class);
+ private CssAnalyzerBridgeServer cssAnalyzerBridgeServer;
+ private CssRuleSensor sensor;
@Before
public void setUp() {
context.fileSystem().setWorkDir(tmpDir.getRoot().toPath());
- Awaitility.setDefaultTimeout(5, TimeUnit.MINUTES);
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer();
+ sensor = new CssRuleSensor(CHECK_FACTORY, cssAnalyzerBridgeServer, analysisWarnings);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (cssAnalyzerBridgeServer != null) {
+ cssAnalyzerBridgeServer.stop();
+ }
}
@Test
public void test_descriptor() {
- CssRuleSensor sensor = new CssRuleSensor(new CssBundleHandler(), checkFactory, new StylelintCommandProvider(), analysisWarnings);
DefaultSensorDescriptor sensorDescriptor = new DefaultSensorDescriptor();
sensor.describe(sensorDescriptor);
assertThat(sensorDescriptor.name()).isEqualTo("SonarCSS Rules");
assertThat(sensorDescriptor.languages()).isEmpty();
+ assertThat(sensorDescriptor.configurationPredicate()).isNull();
+ assertThat(sensorDescriptor.ruleRepositories()).containsOnly("css");
}
@Test
public void test_execute() throws IOException {
- TestLinterCommandProvider commandProvider = getCommandProvider();
- CssRuleSensor sensor = createCssRuleSensor(commandProvider);
+ addInputFile("file.css");
+ addInputFile("file-with-rule-id-message.css");
sensor.execute(context);
- assertThat(context.allIssues()).hasSize(1);
- Issue issue = context.allIssues().iterator().next();
- assertThat(issue.primaryLocation().message()).isEqualTo("some message");
-
- Path configPath = Paths.get(context.fileSystem().workDir().getAbsolutePath(), "testconfig.json");
- assertThat(Files.readAllLines(configPath)).containsOnly("{\"rules\":{\"color-no-invalid-hex\":true,\"declaration-block-no-duplicate-properties\":[true,{\"ignore\":[\"consecutive-duplicates-with-different-values\"]}]}}");
+ assertThat(context.allIssues()).hasSize(2);
+ assertThat(context.allIssues()).extracting("primaryLocation.message")
+ .containsOnly("some message", "Unexpected empty block");
+
+ assertThat(String.join("\n", logTester.logs(LoggerLevel.DEBUG)))
+ .matches("(?s).*Analyzing \\S*file\\.css.*")
+ .matches("(?s).*Found 1 issue\\(s\\).*");
+
+ Path configPath = Paths.get(context.fileSystem().workDir().getAbsolutePath(), "css-bundle", "stylelintconfig.json");
+ assertThat(Files.readAllLines(configPath)).containsOnly(
+ "{\"rules\":{" +
+ "\"block-no-empty\":true," +
+ "\"color-no-invalid-hex\":true," +
+ "\"declaration-block-no-duplicate-properties\":[true,{\"ignore\":[\"consecutive-duplicates-with-different-values\"]}]" +
+ "}}");
verifyZeroInteractions(analysisWarnings);
}
@Test
- public void test_old_property_is_provided() {
- TestLinterCommandProvider commandProvider = getCommandProvider();
- CssRuleSensor sensor = createCssRuleSensor(commandProvider, analysisWarnings);
- context.settings().setProperty(CssPlugin.FORMER_NODE_EXECUTABLE, "foo");
- sensor.execute(context);
+ public void test_non_css_files() {
+ DefaultInputFile fileCss = addInputFile("file.css");
+ DefaultInputFile fileHtml = addInputFile("file.web");
+ DefaultInputFile filePhp = addInputFile("file.php");
+ addInputFile("file.js");
- assertThat(logTester.logs(LoggerLevel.WARN)).contains("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead");
- verify(analysisWarnings).addUnique(eq("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead"));
+ sensor.execute(context);
- assertThat(context.allIssues()).hasSize(1);
+ assertThat(context.allIssues()).hasSize(3);
+ assertThat(context.allIssues())
+ .extracting("primaryLocation.component")
+ .containsOnly(fileCss, fileHtml, filePhp);
}
@Test
- public void test_invalid_node() {
- InvalidCommandProvider commandProvider = new InvalidCommandProvider();
- CssRuleSensor sensor = createCssRuleSensor(commandProvider);
+ public void test_no_file_to_analyze() throws IOException {
sensor.execute(context);
-
assertThat(context.allIssues()).hasSize(0);
- assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Some problem happened. No CSS files will be analyzed.");
- verifyZeroInteractions(analysisWarnings);
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ assertThat(logTester.logs(LoggerLevel.INFO)).contains("No CSS, PHP or HTML files are found in the project. CSS analysis is skipped.");
}
@Test
- public void test_execute_with_analysisWarnings() throws IOException {
- TestLinterCommandProvider commandProvider = getCommandProvider();
- CssRuleSensor sensor = createCssRuleSensor(commandProvider, analysisWarnings);
+ public void bridge_server_fail_to_start() {
+ CssAnalyzerBridgeServer badServer = createCssAnalyzerBridgeServer("throw.js");
+ sensor = new CssRuleSensor(CHECK_FACTORY, badServer, analysisWarnings);
+ addInputFile("file.css");
sensor.execute(context);
+ assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Failed to start server (1s timeout)");
- assertThat(context.allIssues()).hasSize(1);
- Issue issue = context.allIssues().iterator().next();
- assertThat(issue.primaryLocation().message()).isEqualTo("some message");
-
- Path configPath = Paths.get(context.fileSystem().workDir().getAbsolutePath(), "testconfig.json");
- assertThat(Files.readAllLines(configPath)).containsOnly("{\"rules\":{\"color-no-invalid-hex\":true,\"declaration-block-no-duplicate-properties\":[true,{\"ignore\":[\"consecutive-duplicates-with-different-values\"]}]}}");
- verifyZeroInteractions(analysisWarnings);
+ assertThat(logTester.logs(LoggerLevel.DEBUG))
+ .doesNotContain("Skipping start of css-bundle server due to the failure during first analysis");
+ sensor.execute(context);
+ assertThat(logTester.logs(LoggerLevel.DEBUG))
+ .contains("Skipping start of css-bundle server due to the failure during first analysis");
}
@Test
- public void test_invalid_node_command_with_analysisWarnings() {
- InvalidCommandProvider commandProvider = new InvalidCommandProvider();
- CssRuleSensor sensor = createCssRuleSensor(commandProvider, analysisWarnings);
+ public void should_log_when_bridge_server_receives_invalid_response() {
+ addInputFile("invalid-json-response.css");
+ addInputFile("file.css");
sensor.execute(context);
+ assertThat(String.join("\n", logTester.logs(LoggerLevel.ERROR)))
+ .contains("Failed to parse response");
+ assertThat(context.allIssues()).hasSize(1);
+ }
- assertThat(context.allIssues()).hasSize(0);
- assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Some problem happened. No CSS files will be analyzed.");
- verify(analysisWarnings).addUnique(eq("CSS files were not analyzed. Some problem happened."));
+ @Test
+ public void should_fail_fast_when_server_fail_to_start() {
+ context.settings().setProperty("sonar.internal.analysis.failFast", "true");
+ CssAnalyzerBridgeServer badServer = createCssAnalyzerBridgeServer("throw.js");
+ sensor = new CssRuleSensor(CHECK_FACTORY, badServer, analysisWarnings);
+ addInputFile("file.css");
+
+ assertThatThrownBy(() -> sensor.execute(context))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Analysis failed");
}
@Test
- public void test_error() {
- TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockError.js", inputFile.absolutePath());
- CssRuleSensor sensor = createCssRuleSensor(commandProvider);
- sensor.execute(context);
+ public void should_fail_fast_when_bridge_server_receives_invalid_response() {
+ context.settings().setProperty("sonar.internal.analysis.failFast", "true");
+ addInputFile("invalid-json-response.css");
+
+ assertThatThrownBy(() -> sensor.execute(context))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Analysis failed");
+ }
- assertThat(logTester.logs(LoggerLevel.ERROR)).anyMatch(s -> s.startsWith("Failed to run external linting process"));
+ @Test
+ public void should_not_analyze_files_with_not_file_uri() throws URISyntaxException, IOException {
+ InputFile httpFile = mock(InputFile.class);
+ when(httpFile.filename()).thenReturn("file.css");
+ when(httpFile.uri()).thenReturn(new URI("http://lost-on-earth.com/file.css"));
+ sensor.analyzeFile(context, httpFile, new File("config.json"));
+ assertThat(String.join("\n", logTester.logs(LoggerLevel.DEBUG)))
+ .matches("(?s).*Skipping \\S*file.css as it has not 'file' scheme.*")
+ .doesNotMatch("(?s).*\nAnalyzing \\S*file.css.*");
}
@Test
- public void test_not_execute_rules_if_nothing_enabled() {
- TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockError.js", inputFile.absolutePath());
- CssRuleSensor sensor = new CssRuleSensor(new TestBundleHandler(), new CheckFactory(new TestActiveRules()), commandProvider, analysisWarnings);
+ public void analysis_stop_when_server_is_not_anymore_alive() throws IOException, InterruptedException {
+ File configFile = new File("config.json");
+ DefaultInputFile inputFile = addInputFile("dir/file.css");
sensor.execute(context);
+ cssAnalyzerBridgeServer.setPort(43);
- assertThat(logTester.logs(LoggerLevel.WARN)).contains("No rules are activated in CSS Quality Profile");
+ assertThatThrownBy(() -> sensor.analyzeFiles(context, Collections.singletonList(inputFile), configFile))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("css-bundle server is not answering");
}
@Test
- public void test_stylelint_throws() {
- TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockThrow.js", inputFile.absolutePath());
- CssRuleSensor sensor = createCssRuleSensor(commandProvider);
+ public void should_stop_execution_when_sensor_context_is_cancelled() throws IOException {
+ addInputFile("file.css");
+ context.setCancelled(true);
sensor.execute(context);
-
- await().until(() -> logTester.logs(LoggerLevel.ERROR)
- .contains("throw new Error('houps!');"));
+ assertThat(context.allIssues()).isEmpty();
+ assertThat(logTester.logs(LoggerLevel.INFO))
+ .contains("java.util.concurrent.CancellationException: Analysis interrupted because the SensorContext is in cancelled state");
}
@Test
- public void test_stylelint_exitvalue() {
- TestLinterCommandProvider commandProvider = new TestLinterCommandProvider().nodeScript("/executables/mockExit.js", "1");
- CssRuleSensor sensor = createCssRuleSensor(commandProvider);
+ public void test_old_property_is_provided() {
+ context.settings().setProperty(CssPlugin.FORMER_NODE_EXECUTABLE, "foo");
+ addInputFile("file.css");
sensor.execute(context);
- await().until(() -> logTester.logs(LoggerLevel.ERROR)
- .contains("Analysis didn't terminate normally, please verify ERROR and WARN logs above. Exit code 1"));
+ assertThat(logTester.logs(LoggerLevel.WARN)).contains("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead");
+ verify(analysisWarnings).addUnique(eq("Property 'sonar.css.node' is ignored, 'sonar.nodejs.executable' should be used instead"));
+
+ assertThat(context.allIssues()).hasSize(1);
+
+ sensor = new CssRuleSensor(CHECK_FACTORY, cssAnalyzerBridgeServer, null);
+ sensor.execute(context);
+ verifyNoMoreInteractions(analysisWarnings);
}
@Test
public void test_syntax_error() {
- SensorContextTester context = SensorContextTester.create(BASE_DIR);
- context.fileSystem().setWorkDir(tmpDir.getRoot().toPath());
- DefaultInputFile inputFile = createInputFile(context, "some css content\n on 2 lines", "dir/file.css");
- TestLinterCommandProvider rulesExecution = new TestLinterCommandProvider().nodeScript("/executables/mockSyntaxError.js", inputFile.absolutePath());
- CssRuleSensor sensor = createCssRuleSensor(rulesExecution);
+ InputFile inputFile = addInputFile("syntax-error.css");
sensor.execute(context);
-
+ assertThat(context.allIssues()).isEmpty();
assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Failed to parse " + inputFile.uri() + ", line 2, Missed semicolon");
}
@Test
public void test_unknown_rule() {
- SensorContextTester context = SensorContextTester.create(BASE_DIR);
- context.fileSystem().setWorkDir(tmpDir.getRoot().toPath());
- DefaultInputFile inputFile = createInputFile(context, "some css content\n on 2 lines", "dir/file.css");
- TestLinterCommandProvider rulesExecution = new TestLinterCommandProvider().nodeScript("/executables/mockUnknownRule.js", inputFile.absolutePath());
- CssRuleSensor sensor = createCssRuleSensor(rulesExecution);
+ addInputFile("unknown-rule.css");
sensor.execute(context);
+ assertThat(context.allIssues()).isEmpty();
assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Unknown stylelint rule or rule not enabled: 'unknown-rule-key'");
}
- private static DefaultInputFile createInputFile(SensorContextTester sensorContext, String content, String relativePath) {
+ private DefaultInputFile addInputFile(String relativePath) {
DefaultInputFile inputFile = new TestInputFileBuilder("moduleKey", relativePath)
- .setModuleBaseDir(sensorContext.fileSystem().baseDirPath())
+ .setModuleBaseDir(context.fileSystem().baseDirPath())
.setType(Type.MAIN)
- .setLanguage(CssLanguage.KEY)
+ .setLanguage(relativePath.split("\\.")[1])
.setCharset(StandardCharsets.UTF_8)
- .setContents(content)
+ .setContents("some css content\n on 2 lines")
.build();
- sensorContext.fileSystem().add(inputFile);
+ context.fileSystem().add(inputFile);
return inputFile;
}
- private CssRuleSensor createCssRuleSensor(LinterCommandProvider commandProvider) {
- return new CssRuleSensor(new TestBundleHandler(), checkFactory, commandProvider);
- }
-
- private CssRuleSensor createCssRuleSensor(LinterCommandProvider commandProvider, @Nullable AnalysisWarningsWrapper analysisWarnings) {
- return new CssRuleSensor(new TestBundleHandler(), checkFactory, commandProvider, analysisWarnings);
- }
-
- private TestLinterCommandProvider getCommandProvider() {
- return new TestLinterCommandProvider().nodeScript("/executables/mockStylelint.js", inputFile.absolutePath());
- }
-
- private static class TestLinterCommandProvider implements LinterCommandProvider {
-
- private String[] elements;
-
- private static String resourceScript(String script) {
- try {
- return new File(TestLinterCommandProvider.class.getResource(script).toURI()).getAbsolutePath();
- } catch (URISyntaxException e) {
- throw new IllegalStateException(e);
- }
- }
-
- TestLinterCommandProvider nodeScript(String script, String args) {
- this.elements = new String[]{ resourceScript(script), args};
- return this;
- }
-
- @Override
- public NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error) {
- return NodeCommand.builder()
- .outputConsumer(output)
- .errorConsumer(error)
- .minNodeVersion(6)
- .configuration(context.config())
- .nodeJsArgs(elements)
- .build();
- }
-
- @Override
- public String configPath(File deployDestination) {
- return new File(deployDestination, "testconfig.json").getAbsolutePath();
- }
- }
-
- private static class InvalidCommandProvider implements LinterCommandProvider {
-
- @Override
- public NodeCommand nodeCommand(File deployDestination, SensorContext context, Consumer<String> output, Consumer<String> error) {
- throw new NodeCommandException("Some problem happened.");
- }
-
- @Override
- public String configPath(File deployDestination) {
- return new File(deployDestination, "testconfig.json").getAbsolutePath();
- }
- }
-
- private static class TestBundleHandler implements BundleHandler {
- @Override
- public void deployBundle(File deployDestination) {
- // do nothing
- }
- }
-
}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java
index 8521561..1522c99 100644
--- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java
+++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/CssRulesDefinitionTest.java
@@ -28,7 +28,7 @@ public class CssRulesDefinitionTest {
@Test
public void test_with_external_rules() {
- CssRulesDefinition rulesDefinition = new CssRulesDefinition(true);
+ CssRulesDefinition rulesDefinition = new CssRulesDefinition();
RulesDefinition.Context context = new RulesDefinition.Context();
rulesDefinition.define(context);
@@ -46,19 +46,4 @@ public class CssRulesDefinitionTest {
assertThat(mainRepository.isExternal()).isEqualTo(false);
assertThat(mainRepository.rules()).hasSize(CssRules.getRuleClasses().size());
}
-
- @Test
- public void test_no_external_rules() throws Exception {
- CssRulesDefinition rulesDefinition = new CssRulesDefinition(false);
- RulesDefinition.Context context = new RulesDefinition.Context();
- rulesDefinition.define(context);
-
- assertThat(context.repositories()).hasSize(1);
-
- RulesDefinition.Repository repository = context.repository("css");
- assertThat(repository.name()).isEqualTo("SonarAnalyzer");
- assertThat(repository.language()).isEqualTo("css");
- assertThat(repository.isExternal()).isEqualTo(false);
- assertThat(repository.rules()).hasSize(CssRules.getRuleClasses().size());
- }
}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintCommandProviderTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintCommandProviderTest.java
deleted file mode 100644
index f2d2fed..0000000
--- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/StylelintCommandProviderTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SonarCSS
- * Copyright (C) 2018-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.css.plugin;
-
-import java.io.File;
-import java.util.function.Consumer;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.api.batch.sensor.internal.SensorContextTester;
-import org.sonar.api.utils.log.LogTester;
-import org.sonarsource.nodejs.NodeCommand;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class StylelintCommandProviderTest {
-
- @Rule
- public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- @Rule
- public final LogTester logTester = new LogTester();
-
- @Test
- public void test() {
- StylelintCommandProvider stylelintCommandProvider = new StylelintCommandProvider();
- File deployDestination = new File("deploy_destination");
- File baseDir = new File("src/test/resources").getAbsoluteFile();
- SensorContextTester context = SensorContextTester.create(baseDir);
- context.settings().setProperty(CssPlugin.FILE_SUFFIXES_KEY, ".foo,.bar")
- .setProperty("sonar.javascript.file.suffixes", ".js")
- .setProperty("sonar.php.file.suffixes", ".php")
- .setProperty("sonar.java.file.suffixes", ".java");
- Consumer<String> noop = a -> {};
- NodeCommand nodeCommand = stylelintCommandProvider.nodeCommand(deployDestination, context, noop, noop);
- assertThat(nodeCommand.toString()).endsWith(
- String.join(" ",
- new File(deployDestination, "css-bundle/node_modules/stylelint/bin/stylelint").getAbsolutePath(),
- baseDir.getAbsolutePath() + File.separator + "**" + File.separator + "*{.foo,.bar,.php}",
- "--config",
- new File(deployDestination, "css-bundle/stylelintconfig.json").getAbsolutePath(),
- "-f",
- "json")
- );
- }
-}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssAnalyzerBundleTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssAnalyzerBundleTest.java
new file mode 100644
index 0000000..dadb16c
--- /dev/null
+++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssAnalyzerBundleTest.java
@@ -0,0 +1,85 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin.bundle;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.internal.JUnitTempFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class CssAnalyzerBundleTest {
+
+ @Rule
+ public JUnitTempFolder tempFolder = new JUnitTempFolder();
+
+ @Test
+ public void default_css_bundle_location() throws Exception {
+ CssAnalyzerBundle bundle = new CssAnalyzerBundle();
+ assertThat(bundle.bundleLocation).isEqualTo("/css-bundle.zip");
+ }
+
+ @Test
+ public void almost_empty_css_bundle() throws Exception {
+ Bundle bundle = new CssAnalyzerBundle("/bundle/test-css-bundle.zip");
+ Path deployLocation = tempFolder.newDir().toPath();
+ String expectedStartServer = deployLocation.resolve(Paths.get("css-bundle", "bin", "server")).toString();
+ bundle.deploy(deployLocation);
+ String script = bundle.startServerScript();
+ assertThat(script).isEqualTo(expectedStartServer);
+ File scriptFile = new File(script);
+ assertThat(scriptFile).exists();
+ String content = new String(Files.readAllBytes(scriptFile.toPath()), StandardCharsets.UTF_8);
+ assertThat(content).startsWith("#!/usr/bin/env node");
+ }
+
+ @Test
+ public void missing_bundle() throws Exception {
+ Bundle bundle = new CssAnalyzerBundle("/bundle/invalid-bundle-path.zip");
+ assertThatThrownBy(() -> bundle.deploy(tempFolder.newDir().toPath()))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("css-bundle not found in /bundle/invalid-bundle-path.zip");
+ }
+
+ @Test
+ public void invalid_bundle_zip() throws Exception {
+ Bundle bundle = new CssAnalyzerBundle("/bundle/invalid-zip-file.zip");
+ assertThatThrownBy(() -> bundle.deploy(tempFolder.newDir().toPath()))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessage("Failed to deploy css-bundle (with classpath '/bundle/invalid-zip-file.zip')");
+ }
+
+ @Test
+ public void should_not_fail_when_deployed_twice() throws Exception {
+ Bundle bundle = new CssAnalyzerBundle("/bundle/test-css-bundle.zip");
+ Path deployLocation = tempFolder.newDir().toPath();
+ assertThatCode(() -> {
+ bundle.deploy(deployLocation);
+ bundle.deploy(deployLocation);
+ }).doesNotThrowAnyException();
+ }
+}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssBundleHandlerTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssBundleHandlerTest.java
deleted file mode 100644
index f76c912..0000000
--- a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/bundle/CssBundleHandlerTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarCSS
- * Copyright (C) 2018-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.css.plugin.bundle;
-
-import java.io.File;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class CssBundleHandlerTest {
-
- @Rule
- public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private File DEPLOY_DESTINATION;
-
- @Before
- public void setUp() throws Exception {
- DEPLOY_DESTINATION = temporaryFolder.newFolder("deployDestination");
- }
-
- @Test
- public void test() {
- CssBundleHandler bundleHandler = new CssBundleHandler();
- bundleHandler.bundleLocation = "/bundle/test-bundle.zip";
- bundleHandler.deployBundle(DEPLOY_DESTINATION);
-
- assertThat(new File(DEPLOY_DESTINATION, "test-bundle.js").exists()).isTrue();
- }
-
-}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServerTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServerTest.java
new file mode 100644
index 0000000..302b6bb
--- /dev/null
+++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/CssAnalyzerBridgeServerTest.java
@@ -0,0 +1,254 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin.server;
+
+import java.nio.file.Path;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
+import org.sonar.api.batch.sensor.internal.SensorContextTester;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.utils.internal.JUnitTempFolder;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.css.plugin.bundle.Bundle;
+import org.sonar.css.plugin.server.AnalyzerBridgeServer.Issue;
+import org.sonar.css.plugin.server.AnalyzerBridgeServer.Request;
+import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException;
+import org.sonarsource.nodejs.NodeCommand;
+import org.sonarsource.nodejs.NodeCommandBuilder;
+import org.sonarsource.nodejs.NodeCommandException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.sonar.api.utils.log.LoggerLevel.DEBUG;
+import static org.sonar.api.utils.log.LoggerLevel.INFO;
+import static org.sonar.api.utils.log.LoggerLevel.WARN;
+
+public class CssAnalyzerBridgeServerTest {
+
+ private static final String START_SERVER_SCRIPT = "startServer.js";
+ private static final String CONFIG_FILE = "config.json";
+ private static final int TEST_TIMEOUT_SECONDS = 1;
+
+ @org.junit.Rule
+ public LogTester logTester = new LogTester();
+
+ @org.junit.Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @org.junit.Rule
+ public JUnitTempFolder tempFolder = new JUnitTempFolder();
+
+ private SensorContextTester context;
+ private CssAnalyzerBridgeServer cssAnalyzerBridgeServer;
+
+ @Before
+ public void setUp() throws Exception {
+ context = SensorContextTester.create(tempFolder.newDir());
+ context.fileSystem().setWorkDir(tempFolder.newDir().toPath());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (cssAnalyzerBridgeServer != null) {
+ cssAnalyzerBridgeServer.clean();
+ }
+ }
+
+ @Test
+ public void default_timeout() {
+ CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(mock(Bundle.class));
+ assertThat(server.timeoutSeconds).isEqualTo(60);
+ }
+
+ @Test
+ public void issue_constructor() {
+ Issue issue = new Issue(2, "r", "t");
+ assertThat(issue.line).isEqualTo(2);
+ assertThat(issue.rule).isEqualTo("r");
+ assertThat(issue.text).isEqualTo("t");
+ }
+
+ @Test
+ public void should_throw_when_not_existing_start_script() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer("NOT_EXISTING.js");
+
+ thrown.expect(NodeCommandException.class);
+ thrown.expectMessage("Node.js script to start css-bundle server doesn't exist");
+
+ cssAnalyzerBridgeServer.startServer(context);
+ }
+
+ @Test
+ public void should_throw_if_failed_to_build_node_command() throws Exception {
+ NodeCommandBuilder nodeCommandBuilder = mock(NodeCommandBuilder.class, invocation -> {
+ if (NodeCommandBuilder.class.equals(invocation.getMethod().getReturnType())) {
+ return invocation.getMock();
+ } else {
+ throw new NodeCommandException("msg");
+ }
+ });
+
+ cssAnalyzerBridgeServer = new CssAnalyzerBridgeServer(nodeCommandBuilder, TEST_TIMEOUT_SECONDS, new TestBundle(START_SERVER_SCRIPT));
+
+ thrown.expect(NodeCommandException.class);
+ thrown.expectMessage("msg");
+
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ }
+
+ @Test
+ public void should_forward_process_streams() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer();
+ cssAnalyzerBridgeServer.startServerLazily(context);
+
+ assertThat(logTester.logs(DEBUG)).contains("testing debug log");
+ assertThat(logTester.logs(WARN)).contains("testing warn log");
+ assertThat(logTester.logs(INFO)).contains("testing info log");
+ }
+
+ @Test
+ public void should_get_answer_from_server() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer();
+ cssAnalyzerBridgeServer.startServerLazily(context);
+
+ Request request = new Request("/absolute/path/file.css", CONFIG_FILE);
+ Issue[] issues = cssAnalyzerBridgeServer.analyze(request);
+ assertThat(issues).hasSize(1);
+ assertThat(issues[0].line).isEqualTo(2);
+ assertThat(issues[0].rule).isEqualTo("block-no-empty");
+ assertThat(issues[0].text).isEqualTo("Unexpected empty block");
+
+ request = new Request("/absolute/path/empty.css", CONFIG_FILE);
+ issues = cssAnalyzerBridgeServer.analyze(request);
+ assertThat(issues).isEmpty();
+ }
+
+ @Test
+ public void should_throw_if_failed_to_start() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer("throw.js");
+
+ thrown.expect(NodeCommandException.class);
+ thrown.expectMessage("Failed to start server (" + TEST_TIMEOUT_SECONDS + "s timeout)");
+
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ }
+
+ @Test
+ public void should_return_command_info() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer();
+ assertThat(cssAnalyzerBridgeServer.getCommandInfo()).isEqualTo("Node.js command to start css-bundle server was not built yet.");
+
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ assertThat(cssAnalyzerBridgeServer.getCommandInfo()).contains("Node.js command to start css-bundle was: ", "node", START_SERVER_SCRIPT);
+ assertThat(cssAnalyzerBridgeServer.getCommandInfo()).doesNotContain("--max-old-space-size");
+ }
+
+ @Test
+ public void should_set_max_old_space_size() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer();
+ context.setSettings(new MapSettings().setProperty("sonar.css.node.maxspace", 2048));
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ assertThat(cssAnalyzerBridgeServer.getCommandInfo()).contains("--max-old-space-size=2048");
+ }
+
+ @Test
+ public void test_isAlive() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer();
+ assertThat(cssAnalyzerBridgeServer.isAlive()).isFalse();
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ assertThat(cssAnalyzerBridgeServer.isAlive()).isTrue();
+ cssAnalyzerBridgeServer.stop();
+ assertThat(cssAnalyzerBridgeServer.isAlive()).isFalse();
+ }
+
+ @Test
+ public void test_lazy_start() throws Exception {
+ String alreadyStarted = "css-bundle server is up, no need to start.";
+ String starting = "Starting Node.js process to start css-bundle server at port";
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer();
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ assertThat(logTester.logs(DEBUG).stream().anyMatch(s -> s.startsWith(starting))).isTrue();
+ assertThat(logTester.logs(DEBUG)).doesNotContain(alreadyStarted);
+ logTester.clear();
+ cssAnalyzerBridgeServer.startServerLazily(context);
+ assertThat(logTester.logs(DEBUG).stream().noneMatch(s -> s.startsWith(starting))).isTrue();
+ assertThat(logTester.logs(DEBUG)).contains(alreadyStarted);
+ }
+
+ @Test
+ public void should_throw_special_exception_when_failed_already() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer("throw.js");
+ String failedToStartExceptionMessage = "Failed to start server (" + TEST_TIMEOUT_SECONDS + "s timeout)";
+ assertThatThrownBy(() -> cssAnalyzerBridgeServer.startServerLazily(context))
+ .isInstanceOf(NodeCommandException.class)
+ .hasMessage(failedToStartExceptionMessage);
+
+ assertThatThrownBy(() -> cssAnalyzerBridgeServer.startServerLazily(context))
+ .isInstanceOf(ServerAlreadyFailedException.class);
+ }
+
+ @Test
+ public void should_fail_if_bad_json_response() throws Exception {
+ cssAnalyzerBridgeServer = createCssAnalyzerBridgeServer(START_SERVER_SCRIPT);
+ cssAnalyzerBridgeServer.deploy(context.fileSystem().workDir());
+ cssAnalyzerBridgeServer.startServerLazily(context);
+
+ DefaultInputFile inputFile = TestInputFileBuilder.create("foo", "invalid-json-response.css")
+ .build();
+ Request request = new Request(inputFile.absolutePath(), CONFIG_FILE);
+ assertThatThrownBy(() -> cssAnalyzerBridgeServer.analyze(request)).isInstanceOf(IllegalStateException.class);
+ assertThat(context.allIssues()).isEmpty();
+ }
+
+
+ public static CssAnalyzerBridgeServer createCssAnalyzerBridgeServer(String startServerScript) {
+ CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(NodeCommand.builder(), TEST_TIMEOUT_SECONDS, new TestBundle(startServerScript));
+ server.start();
+ return server;
+ }
+
+ public static CssAnalyzerBridgeServer createCssAnalyzerBridgeServer() {
+ return createCssAnalyzerBridgeServer(START_SERVER_SCRIPT);
+ }
+
+ static class TestBundle implements Bundle {
+
+ final String startServerScript;
+
+ TestBundle(String startServerScript) {
+ this.startServerScript = startServerScript;
+ }
+
+ @Override
+ public void deploy(Path deployLocation) {
+ // no-op for unit test
+ }
+
+ @Override
+ public String startServerScript() {
+ return "src/test/resources/mock-start-server/" + startServerScript;
+ }
+ }
+}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/NetUtilsTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/NetUtilsTest.java
new file mode 100644
index 0000000..0aee145
--- /dev/null
+++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/server/NetUtilsTest.java
@@ -0,0 +1,55 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin.server;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import org.awaitility.Awaitility;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+public class NetUtilsTest {
+
+ @Test
+ public void findOpenPort_should_not_return_zero() throws IOException {
+ assertThat(NetUtils.findOpenPort())
+ .isGreaterThan(0)
+ .isLessThanOrEqualTo(65535);
+ }
+
+ @Test
+ public void waitServerToStart_can_be_interrupted() throws InterruptedException {
+ // try to connect to a port that does not exists
+ Thread worker = new Thread(() -> NetUtils.waitServerToStart("localhost", 8, 1000));
+ worker.start();
+ Awaitility.setDefaultTimeout(1, TimeUnit.SECONDS);
+ // wait for the worker thread to start and to be blocked on Thread.sleep(20);
+ await().until(() -> worker.getState() == Thread.State.TIMED_WAITING);
+
+ long start = System.currentTimeMillis();
+ worker.interrupt();
+ worker.join();
+ long timeToInterrupt = System.currentTimeMillis() - start;
+ assertThat(timeToInterrupt).isLessThan(20);
+ }
+
+}
diff --git a/sonar-css-plugin/src/test/resources/.DS_Store b/sonar-css-plugin/src/test/resources/.DS_Store
deleted file mode 100644
index 63066fe..0000000
--- a/sonar-css-plugin/src/test/resources/.DS_Store
+++ /dev/null
Binary files differ
diff --git a/sonar-css-plugin/src/test/resources/bundle/.DS_Store b/sonar-css-plugin/src/test/resources/bundle/.DS_Store
deleted file mode 100644
index 5008ddf..0000000
--- a/sonar-css-plugin/src/test/resources/bundle/.DS_Store
+++ /dev/null
Binary files differ
diff --git a/sonar-css-plugin/src/test/resources/bundle/invalid-zip-file.zip b/sonar-css-plugin/src/test/resources/bundle/invalid-zip-file.zip
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sonar-css-plugin/src/test/resources/bundle/invalid-zip-file.zip
diff --git a/sonar-css-plugin/src/test/resources/bundle/test-bundle.zip b/sonar-css-plugin/src/test/resources/bundle/test-bundle.zip
deleted file mode 100644
index 446e89f..0000000
--- a/sonar-css-plugin/src/test/resources/bundle/test-bundle.zip
+++ /dev/null
Binary files differ
diff --git a/sonar-css-plugin/src/test/resources/bundle/test-css-bundle.zip b/sonar-css-plugin/src/test/resources/bundle/test-css-bundle.zip
new file mode 100644
index 0000000..3a0a924
--- /dev/null
+++ b/sonar-css-plugin/src/test/resources/bundle/test-css-bundle.zip
Binary files differ
diff --git a/sonar-css-plugin/src/test/resources/executables/mockError.js b/sonar-css-plugin/src/test/resources/executables/mockError.js
deleted file mode 100644
index 23849a4..0000000
--- a/sonar-css-plugin/src/test/resources/executables/mockError.js
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/usr/bin/env node
-console.log("Incorrect json might appear if exception thrown during analysis")
diff --git a/sonar-css-plugin/src/test/resources/executables/mockExit.js b/sonar-css-plugin/src/test/resources/executables/mockExit.js
deleted file mode 100644
index b18a959..0000000
--- a/sonar-css-plugin/src/test/resources/executables/mockExit.js
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env node
-
-console.log("[]");
-
-process.exit(process.argv[2]);
diff --git a/sonar-css-plugin/src/test/resources/executables/mockStylelint.js b/sonar-css-plugin/src/test/resources/executables/mockStylelint.js
deleted file mode 100644
index 70959dc..0000000
--- a/sonar-css-plugin/src/test/resources/executables/mockStylelint.js
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env node
-var testFile = process.argv[2];
-
-var result = [
- {
- source: testFile,
-
- warnings: [
- {
- text: "some message (color-no-invalid-hex)",
- line: 2,
- rule: "color-no-invalid-hex"
- }
- ]
- }
-];
-
-var json = JSON.stringify(result);
-console.log(json);
diff --git a/sonar-css-plugin/src/test/resources/executables/mockSyntaxError.js b/sonar-css-plugin/src/test/resources/executables/mockSyntaxError.js
deleted file mode 100644
index cd32af2..0000000
--- a/sonar-css-plugin/src/test/resources/executables/mockSyntaxError.js
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env node
-var testFile = process.argv[2];
-
-var result = [
- {
- source: testFile,
-
- warnings: [
- {
- text: "Missed semicolon (CssSyntaxError)",
- line: 2,
- rule: "CssSyntaxError"
- }
- ]
- }
-];
-
-var json = JSON.stringify(result);
-console.log(json);
diff --git a/sonar-css-plugin/src/test/resources/executables/mockThrow.js b/sonar-css-plugin/src/test/resources/executables/mockThrow.js
deleted file mode 100644
index ca88c27..0000000
--- a/sonar-css-plugin/src/test/resources/executables/mockThrow.js
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env node
-
-throw new Error('houps!');
diff --git a/sonar-css-plugin/src/test/resources/executables/mockUnknownRule.js b/sonar-css-plugin/src/test/resources/executables/mockUnknownRule.js
deleted file mode 100644
index 844b38b..0000000
--- a/sonar-css-plugin/src/test/resources/executables/mockUnknownRule.js
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env node
-var testFile = process.argv[2];
-
-var result = [
- {
- source: testFile,
-
- warnings: [
- {
- text: "some message",
- line: 2,
- rule: "unknown-rule-key"
- }
- ]
- }
-];
-
-var json = JSON.stringify(result);
-console.log(json);
diff --git a/sonar-css-plugin/src/test/resources/executables/oldNodeVersion.js b/sonar-css-plugin/src/test/resources/executables/oldNodeVersion.js
deleted file mode 100644
index 817b98a..0000000
--- a/sonar-css-plugin/src/test/resources/executables/oldNodeVersion.js
+++ /dev/null
@@ -1 +0,0 @@
-console.log("3.2.1");
diff --git a/sonar-css-plugin/src/test/resources/mock-start-server/startServer.js b/sonar-css-plugin/src/test/resources/mock-start-server/startServer.js
new file mode 100644
index 0000000..c8b8d38
--- /dev/null
+++ b/sonar-css-plugin/src/test/resources/mock-start-server/startServer.js
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+
+const http = require('http');
+const port = process.argv[2];
+
+console.log(`DEBUG testing debug log`)
+console.log(`WARN testing warn log`)
+console.log(`testing info log`)
+
+const requestHandler = (request, response) => {
+ let data = [];
+ request.on('data', chunk => {
+ data.push(chunk);
+ });
+ request.on('end', () => {
+ let fileName = null;
+ if (data.length > 0) {
+ const analysisRequest = JSON.parse(data.join());
+ fileName = analysisRequest.filePath.replace(/.*[\/\\]/g,"");
+ }
+ if (request.url === '/status') {
+ response.writeHead(200, { 'Content-Type': 'text/plain' });
+ response.end('OK!');
+ } else {
+ switch (fileName) {
+ case "file.css":
+ case "file.web":
+ case "file.php":
+ case "file.js": // to test that we will not save this issue even if it's provided by response
+ response.end(JSON.stringify([
+ {line: 2, rule: "block-no-empty", text: "Unexpected empty block"}
+ ]));
+ break;
+ case "file-with-rule-id-message.css":
+ response.end(JSON.stringify([
+ {line: 2, rule: "color-no-invalid-hex", text: "some message (color-no-invalid-hex)"}
+ ]));
+ break;
+ case "empty.css":
+ response.end(JSON.stringify([]));
+ break;
+ case "syntax-error.css":
+ response.end(JSON.stringify([
+ {line: 2, rule: "CssSyntaxError", text: "Missed semicolon (CssSyntaxError)"}
+ ]));
+ break;
+ case "unknown-rule.css":
+ response.end(JSON.stringify([
+ {line: 2, rule: "unknown-rule-key", text: "some message"}
+ ]));
+ break;
+ case "invalid-json-response.css":
+ response.end("[");
+ break;
+ default:
+ throw "Unexpected fileName: " + fileName;
+ }
+ }
+ });
+};
+
+const server = http.createServer(requestHandler);
+
+server.listen(port, (err) => {
+ if (err) {
+ return console.log('something bad happened', err)
+ }
+
+ console.log(`server is listening on ${port}`)
+});
diff --git a/sonar-css-plugin/src/test/resources/mock-start-server/throw.js b/sonar-css-plugin/src/test/resources/mock-start-server/throw.js
new file mode 100644
index 0000000..028dbc8
--- /dev/null
+++ b/sonar-css-plugin/src/test/resources/mock-start-server/throw.js
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+throw "Something wrong happened"