diff options
Diffstat (limited to 'sonar-css-plugin')
9 files changed, 237 insertions, 240 deletions
| 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 3b32501..db961df 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 @@ -37,7 +37,6 @@ import java.util.stream.StreamSupport;  import javax.annotation.Nullable;  import org.sonar.api.SonarProduct;  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; @@ -51,12 +50,10 @@ 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.server.AnalyzerBridgeServer.Issue; -import org.sonar.css.plugin.server.AnalyzerBridgeServer.Request;  import org.sonar.css.plugin.server.CssAnalyzerBridgeServer; -import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException; +import org.sonar.css.plugin.server.CssAnalyzerBridgeServer.Issue; +import org.sonar.css.plugin.server.CssAnalyzerBridgeServer.Request;  import org.sonarsource.analyzer.commons.ProgressReport; -import org.sonarsource.nodejs.NodeCommandException;  public class CssRuleSensor implements Sensor { @@ -89,34 +86,34 @@ public class CssRuleSensor implements Sensor {    public void execute(SensorContext context) {      reportOldNodeProperty(context); -    boolean failFast = context.config().getBoolean("sonar.internal.analysis.failFast").orElse(false); +    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."); +      return; +    } + +    File configFile = null; +    boolean serverRunning = 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); -      } +      serverRunning = cssAnalyzerBridgeServer.startServerLazily(context); +      configFile = createLinterConfig(context);      } catch (Exception e) { -      LOG.error("Failure during analysis, " + cssAnalyzerBridgeServer.getCommandInfo(), e); -      if (failFast) { -        throw new IllegalStateException("Analysis failed (\"sonar.internal.analysis.failFast\"=true)", e); -      } +      // we can end up here in the following cases: problem during bundle unpacking, or config file creation, or socket creation +      String msg = "Failure during CSS analysis preparation, " + cssAnalyzerBridgeServer.getCommandInfo(); +      logErrorOrWarn(context, msg, e); +      throwFailFast(context, e); +    } + +    if (serverRunning && configFile != null) { +      analyzeFiles(context, inputFiles, configFile); +    } +  } + +  public static void throwFailFast(SensorContext context, Exception e) { +    boolean failFast = context.config().getBoolean("sonar.internal.analysis.failFast").orElse(false); +    if (failFast) { +      throw new IllegalStateException("Analysis failed (\"sonar.internal.analysis.failFast\"=true)", e);      }    } @@ -128,34 +125,58 @@ public class CssRuleSensor implements Sensor {      }    } -  void analyzeFiles(SensorContext context, List<InputFile> inputFiles, File configFile) throws InterruptedException, IOException { +  private void analyzeFiles(SensorContext context, List<InputFile> inputFiles, File configFile) {      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(), e); -          } -          progressReport.nextFile(); -        } else { -          throw new IllegalStateException("css-bundle server is not answering"); -        } +        analyzeFileWithContextCheck(inputFile, context, configFile); +        progressReport.nextFile();        }        success = true; + +    } catch (CancellationException e) { +      // do not propagate the exception +      LOG.info(e.toString()); + +    } catch (Exception e) { +      // we can end up here in the following cases: file analysis request sending, or response parsing, server is not answering +      // or some unpredicted state +      String msg = "Failure during CSS analysis, " + cssAnalyzerBridgeServer.getCommandInfo(); +      logErrorOrWarn(context, msg, e); +      throwFailFast(context, e);      } finally { -      if (success) { -        progressReport.stop(); -      } else { -        progressReport.cancel(); -      } +      finishProgressReport(progressReport, success); +    } +  } + +  private static void finishProgressReport(ProgressReport progressReport, boolean success) { +    if (success) { +      progressReport.stop(); +    } else { +      progressReport.cancel(); +    } +    try {        progressReport.join(); +    } catch (InterruptedException e) { +      LOG.error(e.getMessage(), e); +      Thread.currentThread().interrupt(); +    } +  } + +  void analyzeFileWithContextCheck(InputFile inputFile, SensorContext context, File configFile) throws IOException { +    if (context.isCancelled()) { +      throw new CancellationException("Analysis interrupted because the SensorContext is in cancelled state"); +    } +    if (!cssAnalyzerBridgeServer.isAlive()) { +      throw new IllegalStateException("css-bundle server is not answering"); +    } +    try { +      analyzeFile(context, inputFile, configFile); +    } catch (IOException | RuntimeException e) { +      throw new IllegalStateException("Failure during analysis of " + inputFile.uri(), e);      }    } @@ -187,9 +208,9 @@ public class CssRuleSensor implements Sensor {        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); +          logErrorOrDebug(inputFile, "Failed to parse {}, line {}, {}", inputFile.uri(), issue.line, errorMessage);          } else { -          LOG.error("Unknown stylelint rule or rule not enabled: '" + issue.rule + "'"); +          logErrorOrDebug(inputFile,"Unknown stylelint rule or rule not enabled: '" + issue.rule + "'");          }        } else { @@ -206,16 +227,39 @@ public class CssRuleSensor implements Sensor {      }    } +  private static void logErrorOrDebug(InputFile file, String msg, Object ... arguments) { +    if (CssLanguage.KEY.equals(file.language())) { +      LOG.error(msg, arguments); +    } else { +      LOG.debug(msg, arguments); +    } +  } + +  private static void logErrorOrWarn(SensorContext context, String msg, Throwable e) { +    if (hasCssFiles(context)) { +      LOG.error(msg, e); +    } else { +      LOG.warn(msg); +    } +  } +    private static List<InputFile> getInputFiles(SensorContext context) {      FileSystem fileSystem = context.fileSystem(); -    FilePredicates predicates = context.fileSystem().predicates(); -    FilePredicate mainFilePredicate = predicates.and( +    FilePredicate mainFilePredicate = fileSystem.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());    } +  public static boolean hasCssFiles(SensorContext context) { +    FileSystem fileSystem = context.fileSystem(); +    FilePredicate mainFilePredicate = fileSystem.predicates().and( +      fileSystem.predicates().hasType(InputFile.Type.MAIN), +      fileSystem.predicates().hasLanguages(CssLanguage.KEY)); +    return fileSystem.inputFiles(mainFilePredicate).iterator().hasNext(); +  } +    private File createLinterConfig(SensorContext context) throws IOException {      StylelintConfig config = cssRules.getConfig();      final GsonBuilder gsonBuilder = new GsonBuilder(); 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 deleted file mode 100644 index 56481ec..0000000 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/AnalyzerBridgeServer.java +++ /dev/null @@ -1,73 +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.server; - -import java.io.IOException; -import javax.annotation.Nullable; -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; -    /** -     * The fileContent is sent only in the SonarLint context or when the encoding -     * of the file is not utf-8. Otherwise, for performance reason, it's more efficient to -     * not have the fileContent and let the server getting it using filePath. -     */ -    @Nullable -    public final String fileContent; -    public final String configFile; - -    public Request(String filePath, @Nullable String fileContent, String configFile) { -      this.filePath = filePath; -      this.fileContent = fileContent; -      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 index 05760f8..271d8f8 100644 --- 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 @@ -24,22 +24,31 @@ import com.google.gson.JsonSyntaxException;  import java.io.File;  import java.io.IOException;  import java.time.Duration; +import javax.annotation.Nullable;  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.notifications.AnalysisWarnings; +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.server.bundle.Bundle; -import org.sonar.css.plugin.server.exception.ServerAlreadyFailedException; +import org.sonarsource.api.sonarlint.SonarLintSide;  import org.sonarsource.nodejs.NodeCommand;  import org.sonarsource.nodejs.NodeCommandBuilder;  import org.sonarsource.nodejs.NodeCommandException; -public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer { +import static org.sonar.css.plugin.CssRuleSensor.hasCssFiles; +import static org.sonar.css.plugin.CssRuleSensor.throwFailFast; +import static org.sonarsource.api.sonarlint.SonarLintSide.MULTIPLE_ANALYSES; + +@ScannerSide +@SonarLintSide(lifespan = MULTIPLE_ANALYSES) +public class CssAnalyzerBridgeServer {    private static final Logger LOG = Loggers.get(CssAnalyzerBridgeServer.class);    private static final Profiler PROFILER = Profiler.createIfDebug(LOG); @@ -53,28 +62,29 @@ public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer {    private final NodeCommandBuilder nodeCommandBuilder;    final int timeoutSeconds;    private final Bundle bundle; +  private final AnalysisWarnings analysisWarnings;    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); +  public CssAnalyzerBridgeServer(Bundle bundle, @Nullable AnalysisWarnings analysisWarnings) { +    this(NodeCommand.builder(), DEFAULT_TIMEOUT_SECONDS, bundle, analysisWarnings);    } -  protected CssAnalyzerBridgeServer(NodeCommandBuilder nodeCommandBuilder, int timeoutSeconds, Bundle bundle) { +  protected CssAnalyzerBridgeServer(NodeCommandBuilder nodeCommandBuilder, int timeoutSeconds, Bundle bundle, @Nullable AnalysisWarnings analysisWarnings) {      this.nodeCommandBuilder = nodeCommandBuilder;      this.timeoutSeconds = timeoutSeconds;      this.bundle = bundle; +    this.analysisWarnings = analysisWarnings;      this.client = new OkHttpClient.Builder()        .callTimeout(Duration.ofSeconds(timeoutSeconds))        .readTimeout(Duration.ofSeconds(timeoutSeconds))        .build();    } -  // for testing purposes -  public void deploy(File deployLocation) throws IOException { +  public void deploy(File deployLocation) {      bundle.deploy(deployLocation.toPath());    } @@ -121,27 +131,45 @@ public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer {      nodeCommand = nodeCommandBuilder.build();    } -  @Override -  public void startServerLazily(SensorContext context) throws IOException { +  /** +   * @return true when server is up and running normally, false otherwise +   */ +  public boolean startServerLazily(SensorContext context) throws IOException {      // required for SonarLint context to avoid restarting already failed server      if (failedToStart) { -      throw new ServerAlreadyFailedException(); +      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"); +      return false;      }      try {        if (isAlive()) {          LOG.debug("css-bundle server is up, no need to start."); -        return; +        return true;        }        deploy(context.fileSystem().workDir());        startServer(context); +      return true;      } catch (NodeCommandException e) {        failedToStart = true; -      throw e; +      processNodeCommandException(e, context); +      return false;      }    } -  @Override +  // happens for example when NodeJS is not available, or version is too old +  private void processNodeCommandException(NodeCommandException e, SensorContext context) { +    String message = "CSS rules were not executed. " + e.getMessage(); +    if (hasCssFiles(context)) { +      LOG.error(message, e); +      reportAnalysisWarning(message); +    } else { +      // error logs are often blocking (esp. in Azure), so we log at warning level if there is no CSS files in the project +      LOG.warn(message); +    } +    throwFailFast(context, e); +  } +    public Issue[] analyze(Request request) throws IOException {      String json = GSON.toJson(request);      return parseResponse(request(json)); @@ -164,8 +192,8 @@ public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer {        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); +      LOG.debug(msg); +      throw new IllegalStateException("Failed to parse response (check DEBUG logs for the response content)", e);      }    } @@ -183,12 +211,11 @@ public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer {        // 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); +      LOG.warn("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."; @@ -197,12 +224,10 @@ public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer {      }    } -  @Override    public void start() {      // Server is started lazily by the sensor    } -  @Override    public void stop() {      clean();    } @@ -229,4 +254,39 @@ public class CssAnalyzerBridgeServer implements AnalyzerBridgeServer {      this.port = port;    } +  private void reportAnalysisWarning(String message) { +    if (analysisWarnings != null) { +      analysisWarnings.addUnique(message); +    } +  } + +  public static class Request { +    public final String filePath; +    /** +     * The fileContent is sent only in the SonarLint context or when the encoding +     * of the file is not utf-8. Otherwise, for performance reason, it's more efficient to +     * not have the fileContent and let the server getting it using filePath. +     */ +    @Nullable +    public final String fileContent; +    public final String configFile; + +    public Request(String filePath, @Nullable String fileContent, String configFile) { +      this.filePath = filePath; +      this.fileContent = fileContent; +      this.configFile = configFile; +    } +  } + +  public static 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/bundle/Bundle.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/bundle/Bundle.java index 513131f..ef82a2b 100644 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/bundle/Bundle.java +++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/bundle/Bundle.java @@ -19,12 +19,11 @@   */  package org.sonar.css.plugin.server.bundle; -import java.io.IOException;  import java.nio.file.Path;  public interface Bundle { -  void deploy(Path deployLocation) throws IOException; +  void deploy(Path deployLocation);    /**     * should be called after deploy(Path deployLocation) diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java deleted file mode 100644 index 0865aa4..0000000 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/ServerAlreadyFailedException.java +++ /dev/null @@ -1,27 +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.server.exception; - -/** - * 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/server/exception/package-info.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java deleted file mode 100644 index 4f5b753..0000000 --- a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/server/exception/package-info.java +++ /dev/null @@ -1,21 +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. - */ -@javax.annotation.ParametersAreNonnullByDefault -package org.sonar.css.plugin.server.exception; 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 0b37c92..429722d 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 @@ -27,7 +27,6 @@ import java.nio.charset.StandardCharsets;  import java.nio.file.Files;  import java.nio.file.Path;  import java.nio.file.Paths; -import java.util.Collections;  import org.junit.After;  import org.junit.Before;  import org.junit.Rule; @@ -149,28 +148,13 @@ public class CssRuleSensorTest {    }    @Test -  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(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 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))) +    assertThat(String.join("\n", logTester.logs(LoggerLevel.DEBUG)))        .contains("Failed to parse response"); -    assertThat(context.allIssues()).hasSize(1); +    assertThat(String.join("\n", logTester.logs(LoggerLevel.ERROR))) +      .contains("Failure during CSS analysis");    }    @Test @@ -183,6 +167,32 @@ public class CssRuleSensorTest {      assertThatThrownBy(() -> sensor.execute(context))        .isInstanceOf(IllegalStateException.class)        .hasMessageContaining("Analysis failed"); +    assertThat(logTester.logs(LoggerLevel.ERROR)).contains("CSS rules were not executed. Failed to start server (1s timeout)"); +  } + +  @Test +  public void should_fail_fast_when_server_fail_to_start_no_css() { +    context.settings().setProperty("sonar.internal.analysis.failFast", "true"); +    CssAnalyzerBridgeServer badServer = createCssAnalyzerBridgeServer("throw.js"); +    sensor = new CssRuleSensor(CHECK_FACTORY, badServer, analysisWarnings); +    addInputFile("file.web"); + +    assertThatThrownBy(() -> sensor.execute(context)) +      .isInstanceOf(IllegalStateException.class) +      .hasMessageContaining("Analysis failed"); +    assertThat(logTester.logs(LoggerLevel.WARN)).contains("CSS rules were not executed. Failed to start server (1s timeout)"); +  } + +  @Test +  public void should_not_fail_fast_when_server_fail_to_start_without_property() { +    context.settings().setProperty("sonar.internal.analysis.failFast", "false"); +    CssAnalyzerBridgeServer badServer = createCssAnalyzerBridgeServer("throw.js"); +    sensor = new CssRuleSensor(CHECK_FACTORY, badServer, analysisWarnings); +    addInputFile("file.web"); + +    sensor.execute(context); +    // log as Warning level is there is no CSS files +    assertThat(logTester.logs(LoggerLevel.WARN)).contains("CSS rules were not executed. Failed to start server (1s timeout)");    }    @Test @@ -207,13 +217,13 @@ public class CssRuleSensorTest {    }    @Test -  public void analysis_stop_when_server_is_not_anymore_alive() throws IOException, InterruptedException { +  public void analysis_stop_when_server_is_not_anymore_alive() {      File configFile = new File("config.json");      DefaultInputFile inputFile = addInputFile("dir/file.css");      sensor.execute(context);      cssAnalyzerBridgeServer.setPort(43); -    assertThatThrownBy(() -> sensor.analyzeFiles(context, Collections.singletonList(inputFile), configFile)) +    assertThatThrownBy(() -> sensor.analyzeFileWithContextCheck(inputFile, context, configFile))        .isInstanceOf(IllegalStateException.class)        .hasMessageContaining("css-bundle server is not answering");    } @@ -247,9 +257,11 @@ public class CssRuleSensorTest {    @Test    public void test_syntax_error() {      InputFile inputFile = addInputFile("syntax-error.css"); +    InputFile inputFileNotCss = addInputFile("syntax-error.web");      sensor.execute(context);      assertThat(context.allIssues()).isEmpty();      assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Failed to parse " + inputFile.uri() + ", line 2, Missed semicolon"); +    assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Failed to parse " + inputFileNotCss.uri() + ", line 2, Missed semicolon");    }    @Test 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 index 99b14ff..3d42261 100644 --- 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 @@ -28,12 +28,12 @@ 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.notifications.AnalysisWarnings;  import org.sonar.api.utils.internal.JUnitTempFolder;  import org.sonar.api.utils.log.LogTester; +import org.sonar.css.plugin.server.CssAnalyzerBridgeServer.Issue; +import org.sonar.css.plugin.server.CssAnalyzerBridgeServer.Request;  import org.sonar.css.plugin.server.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; @@ -41,6 +41,7 @@ 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.mockito.Mockito.verify;  import static org.sonar.api.utils.log.LoggerLevel.DEBUG;  import static org.sonar.api.utils.log.LoggerLevel.ERROR;  import static org.sonar.api.utils.log.LoggerLevel.INFO; @@ -79,7 +80,7 @@ public class CssAnalyzerBridgeServerTest {    @Test    public void default_timeout() { -    CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(mock(Bundle.class)); +    CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(mock(Bundle.class), null);      assertThat(server.timeoutSeconds).isEqualTo(60);    } @@ -102,7 +103,11 @@ public class CssAnalyzerBridgeServerTest {    }    @Test -  public void should_throw_if_failed_to_build_node_command() throws Exception { +  public void should_return_false_if_failed_to_build_node_command() throws Exception { +    context.fileSystem().add(new TestInputFileBuilder("moduleKey", "file.css") +      .setLanguage("css") +      .build()); +      NodeCommandBuilder nodeCommandBuilder = mock(NodeCommandBuilder.class, invocation -> {        if (NodeCommandBuilder.class.equals(invocation.getMethod().getReturnType())) {          return invocation.getMock(); @@ -111,12 +116,11 @@ public class CssAnalyzerBridgeServerTest {        }      }); -    cssAnalyzerBridgeServer = new CssAnalyzerBridgeServer(nodeCommandBuilder, TEST_TIMEOUT_SECONDS, new TestBundle(START_SERVER_SCRIPT)); - -    thrown.expect(NodeCommandException.class); -    thrown.expectMessage("msg"); - -    cssAnalyzerBridgeServer.startServerLazily(context); +    AnalysisWarnings analysisWarnings = mock(AnalysisWarnings.class); +    cssAnalyzerBridgeServer = new CssAnalyzerBridgeServer(nodeCommandBuilder, TEST_TIMEOUT_SECONDS, new TestBundle(START_SERVER_SCRIPT), analysisWarnings); +    assertThat(cssAnalyzerBridgeServer.startServerLazily(context)).isFalse(); +    assertThat(logTester.logs(ERROR)).contains("CSS rules were not executed. msg"); +    verify(analysisWarnings).addUnique("CSS rules were not executed. msg");    }    @Test @@ -150,11 +154,8 @@ public class CssAnalyzerBridgeServerTest {    @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); +    assertThat(cssAnalyzerBridgeServer.startServerLazily(context)).isFalse(); +    assertThat(logTester.logs(WARN)).contains("CSS rules were not executed. Failed to start server (" + TEST_TIMEOUT_SECONDS + "s timeout)");    }    @Test @@ -200,15 +201,16 @@ public class CssAnalyzerBridgeServerTest {    }    @Test -  public void should_throw_special_exception_when_failed_already() throws Exception { +  public void should_return_false_and_log_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); +    String failedToStartExceptionMessage = "CSS rules were not executed. Failed to start server (" + TEST_TIMEOUT_SECONDS + "s timeout)"; + +    assertThat(cssAnalyzerBridgeServer.startServerLazily(context)).isFalse(); +    assertThat(logTester.logs(WARN)).contains(failedToStartExceptionMessage); -    assertThatThrownBy(() -> cssAnalyzerBridgeServer.startServerLazily(context)) -      .isInstanceOf(ServerAlreadyFailedException.class); +    assertThat(cssAnalyzerBridgeServer.startServerLazily(context)).isFalse(); +    assertThat(logTester.logs(DEBUG)).contains("Skipping start of css-bundle server due to the failure during first analysis", +      "Skipping execution of CSS rules due to the problems with css-bundle server");    }    @Test @@ -226,7 +228,7 @@ public class CssAnalyzerBridgeServerTest {    public static CssAnalyzerBridgeServer createCssAnalyzerBridgeServer(String startServerScript) { -    CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(NodeCommand.builder(), TEST_TIMEOUT_SECONDS, new TestBundle(startServerScript)); +    CssAnalyzerBridgeServer server = new CssAnalyzerBridgeServer(NodeCommand.builder(), TEST_TIMEOUT_SECONDS, new TestBundle(startServerScript), null);      server.start();      return server;    } 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 index f7f40c5..87298a9 100644 --- a/sonar-css-plugin/src/test/resources/mock-start-server/startServer.js +++ b/sonar-css-plugin/src/test/resources/mock-start-server/startServer.js @@ -38,6 +38,7 @@ const requestHandler = (request, response) => {            response.end(JSON.stringify([]));            break;          case "syntax-error.css": +        case "syntax-error.web":            response.end(JSON.stringify([              {line: 2, rule: "CssSyntaxError", text: "Missed semicolon (CssSyntaxError)"}            ])); | 
