diff options
| author | Elena Vilchik | 2019-12-18 17:10:10 +0100 |
|---|---|---|
| committer | Alban Auzeill | 2019-12-18 17:10:10 +0100 |
| commit | c8f0071c4f5336dfe0efc5d3c218ab49f2401264 (patch) | |
| tree | 254cd5ed9531d7c62bab4f8ec082e085795ecb8f /sonar-css-plugin/src | |
| parent | 13fe08e87c8a70ffe6e248b774ef826bbe1f779d (diff) | |
| download | sonar-css-c8f0071c4f5336dfe0efc5d3c218ab49f2401264.tar.bz2 | |
Rely on NodeJS API of Stylelint to execute CSS rules (#221)
Diffstat (limited to 'sonar-css-plugin/src')
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 Binary files differdeleted file mode 100644 index 63066fe..0000000 --- a/sonar-css-plugin/src/test/resources/.DS_Store +++ /dev/null diff --git a/sonar-css-plugin/src/test/resources/bundle/.DS_Store b/sonar-css-plugin/src/test/resources/bundle/.DS_Store Binary files differdeleted file mode 100644 index 5008ddf..0000000 --- a/sonar-css-plugin/src/test/resources/bundle/.DS_Store +++ /dev/null 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 Binary files differdeleted file mode 100644 index 446e89f..0000000 --- a/sonar-css-plugin/src/test/resources/bundle/test-bundle.zip +++ /dev/null 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 Binary files differnew file mode 100644 index 0000000..3a0a924 --- /dev/null +++ b/sonar-css-plugin/src/test/resources/bundle/test-css-bundle.zip 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" |
