]> source.dussan.org Git - sonar-scanner-cli.git/commitdiff
SCANCLI-146 Log using slf4j + logback
authorJulien HENRY <julien.henry@sonarsource.com>
Fri, 3 May 2024 09:41:04 +0000 (11:41 +0200)
committerJulien HENRY <julien.henry@sonarsource.com>
Mon, 6 May 2024 12:21:26 +0000 (14:21 +0200)
22 files changed:
pom.xml
src/main/java/org/sonarsource/scanner/cli/Cli.java
src/main/java/org/sonarsource/scanner/cli/Conf.java
src/main/java/org/sonarsource/scanner/cli/Logs.java [deleted file]
src/main/java/org/sonarsource/scanner/cli/Main.java
src/main/java/org/sonarsource/scanner/cli/PropertyResolver.java
src/main/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactory.java
src/main/java/org/sonarsource/scanner/cli/ScannerVersion.java
src/main/java/org/sonarsource/scanner/cli/Slf4jLogOutput.java [new file with mode: 0644]
src/main/java/org/sonarsource/scanner/cli/Stats.java
src/main/java/org/sonarsource/scanner/cli/SystemInfo.java
src/main/resources/logback.xml [new file with mode: 0644]
src/test/java/org/sonarsource/scanner/cli/CliTest.java
src/test/java/org/sonarsource/scanner/cli/ConfTest.java
src/test/java/org/sonarsource/scanner/cli/LogsTest.java [deleted file]
src/test/java/org/sonarsource/scanner/cli/MainTest.java
src/test/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactoryTest.java
src/test/java/org/sonarsource/scanner/cli/Slf4jLogOutputTest.java [new file with mode: 0644]
src/test/java/org/sonarsource/scanner/cli/StatsTest.java
src/test/java/org/sonarsource/scanner/cli/SystemInfoTest.java
src/test/java/testutils/ConcurrentListAppender.java [new file with mode: 0644]
src/test/java/testutils/LogTester.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index fcd792fbbed3813e7e2ed42c254457fe37b3f558..dbe822e44242f2559adec7c7f64db67d14748fcf 100644 (file)
--- a/pom.xml
+++ b/pom.xml
       <artifactId>sonar-scanner-java-library</artifactId>
       <version>3.0.0.114</version>
     </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.5.6</version>
+    </dependency>
     <dependency>
       <groupId>com.google.code.findbugs</groupId>
       <artifactId>jsr305</artifactId>
       <version>5.10.1</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+    <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <version>5.10.1</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.assertj</groupId>
       <artifactId>assertj-core</artifactId>
             <configuration>
               <createDependencyReducedPom>true</createDependencyReducedPom>
               <minimizeJar>true</minimizeJar>
+              <filters>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                    <exclude>META-INF/LICENSE.txt</exclude>
+                    <exclude>META-INF/NOTICE.txt</exclude>
+                    <exclude>META-INF/MANIFEST.MF</exclude>
+                    <exclude>**/module-info.class</exclude>
+                  </excludes>
+                </filter>
+                <filter>
+                  <artifact>ch.qos.logback:logback-classic</artifact>
+                  <includes>
+                    <include>**</include>
+                  </includes>
+                </filter>
+              </filters>
             </configuration>
           </execution>
         </executions>
             <configuration>
               <rules>
                 <requireFilesSize>
-                  <minsize>3400000</minsize>
-                  <maxsize>3500000</maxsize>
+                  <minsize>4100000</minsize>
+                  <maxsize>4200000</maxsize>
                   <files>
                     <file>${project.build.directory}/sonar-scanner-${project.version}.zip</file>
                   </files>
index 4de2a36f2bb33204fbd80b3f87dfb8de4cc9b700..a39b0af72764b0bc3290153465e20cf96d4e0523 100644 (file)
  */
 package org.sonarsource.scanner.cli;
 
+import ch.qos.logback.classic.Level;
 import java.util.Properties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import static java.util.Arrays.asList;
 
 class Cli {
 
+  private static final Logger LOG = LoggerFactory.getLogger(Cli.class);
+
   private boolean debugEnabled = false;
   private boolean displayVersionOnly = false;
   private boolean embedded = false;
   private String invokedFrom = "";
   private final Properties props = new Properties();
   private final Exit exit;
-  private final Logs logger;
 
-  public Cli(Exit exit, Logs logger) {
+  public Cli(Exit exit) {
     this.exit = exit;
-    this.logger = logger;
   }
 
   boolean isDebugEnabled() {
@@ -80,19 +83,19 @@ class Cli {
       displayVersionOnly = true;
 
     } else if (asList("-e", "--errors").contains(arg)) {
-      logger
+      LOG
         .info("Option -e/--errors is no longer supported and will be ignored");
 
     } else if (asList("-X", "--debug").contains(arg)) {
       props.setProperty("sonar.verbose", "true");
       debugEnabled = true;
-      logger.setDebugEnabled(true);
-
+      var rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+      rootLogger.setLevel(Level.DEBUG);
     } else if (asList("-D", "--define").contains(arg)) {
       return processProp(args, pos);
 
     } else if ("--embedded".equals(arg)) {
-      logger.info(
+      LOG.info(
         "Option --embedded is deprecated and will be removed in a future release.");
       embedded = true;
 
@@ -128,7 +131,7 @@ class Cli {
     displayVersionOnly = false;
   }
 
-  private void appendPropertyTo(String arg, Properties props) {
+  private static void appendPropertyTo(String arg, Properties props) {
     final String key;
     final String value;
     int j = arg.indexOf('=');
@@ -141,25 +144,24 @@ class Cli {
     }
     Object oldValue = props.setProperty(key, value);
     if (oldValue != null) {
-      logger.warn("Property '" + key + "' with value '" + oldValue + "' is "
-        + "overridden with value '" + value + "'");
+      LOG.warn("Property '{}' with value '{}' is overridden with value '{}'", key, oldValue, value);
     }
   }
 
   private void printErrorAndExit(String message) {
-    logger.error(message);
+    LOG.error(message);
     printUsage();
     exit.exit(Exit.INTERNAL_ERROR);
   }
 
-  private void printUsage() {
-    logger.info("");
-    logger.info("usage: sonar-scanner [options]");
-    logger.info("");
-    logger.info("Options:");
-    logger.info(" -D,--define <arg>     Define property");
-    logger.info(" -h,--help             Display help information");
-    logger.info(" -v,--version          Display version information");
-    logger.info(" -X,--debug            Produce execution debug output");
+  private static void printUsage() {
+    System.out.println();
+    System.out.println("usage: sonar-scanner [options]");
+    System.out.println();
+    System.out.println("Options:");
+    System.out.println(" -D,--define <arg>     Define property");
+    System.out.println(" -h,--help             Display help information");
+    System.out.println(" -v,--version          Display version information");
+    System.out.println(" -X,--debug            Produce execution debug output");
   }
 }
index 4e9a53ab725246461b367420547b3e2f9b14e8f2..b1cbe620fd152f81dfb2782cce45eb33b33f2759 100644 (file)
@@ -31,9 +31,13 @@ import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.sonarsource.scanner.lib.EnvironmentConfig;
 
 class Conf {
+  private static final Logger LOG = LoggerFactory.getLogger(Conf.class);
+
   private static final String SCANNER_HOME = "scanner.home";
   private static final String SCANNER_SETTINGS = "scanner.settings";
   private static final String PROJECT_HOME = "project.home";
@@ -46,13 +50,11 @@ class Conf {
   private static final String BOOTSTRAP_START_TIME = "sonar.scanner.bootstrapStartTime";
 
   private final Cli cli;
-  private final Logs logger;
   private final Map<String, String> env;
   private final long startTimeMs;
 
-  Conf(Cli cli, Logs logger, Map<String, String> env) {
+  Conf(Cli cli, Map<String, String> env) {
     this.cli = cli;
-    this.logger = logger;
     this.env = env;
     this.startTimeMs = System.currentTimeMillis();
   }
@@ -80,7 +82,7 @@ class Conf {
   }
 
   private Map<String, String> loadEnvironmentProperties() {
-    return EnvironmentConfig.load(logger.getLogOutputAdapter());
+    return EnvironmentConfig.load(new Slf4jLogOutput());
   }
 
   private Properties loadGlobalProperties() {
@@ -93,10 +95,10 @@ class Conf {
     Path settingsFile = locatePropertiesFile(knownPropsAtThatPoint, SCANNER_HOME, "conf/sonar-scanner.properties",
       SCANNER_SETTINGS);
     if (settingsFile != null && Files.isRegularFile(settingsFile)) {
-      logger.info("Scanner configuration file: " + settingsFile);
+      LOG.info("Scanner configuration file: {}", settingsFile);
       return toProperties(settingsFile);
     }
-    logger.info("Scanner configuration file: NONE");
+    LOG.info("Scanner configuration file: NONE");
     return new Properties();
   }
 
@@ -111,10 +113,10 @@ class Conf {
     Path defaultRootSettingsFile = getRootProjectBaseDir(knownPropsAtThatPoint).resolve(SONAR_PROJECT_PROPERTIES_FILENAME);
     Path rootSettingsFile = locatePropertiesFile(defaultRootSettingsFile, knownPropsAtThatPoint, PROJECT_SETTINGS);
     if (rootSettingsFile != null && Files.isRegularFile(rootSettingsFile)) {
-      logger.info("Project root configuration file: " + rootSettingsFile);
+      LOG.info("Project root configuration file: {}", rootSettingsFile);
       rootProps.putAll(toProperties(rootSettingsFile));
     } else {
-      logger.info("Project root configuration file: NONE");
+      LOG.info("Project root configuration file: NONE");
     }
 
     Properties projectProps = new Properties();
@@ -302,10 +304,9 @@ class Conf {
   /**
    * Transforms a comma-separated list String property in to a array of
    * trimmed strings.
-   *
+   * <p>
    * This works even if they are separated by whitespace characters (space
    * char, EOL, ...)
-   *
    */
   static String[] getListFromProperty(Properties properties, String key) {
     String value = properties.getProperty(key, "").trim();
@@ -314,12 +315,12 @@ class Conf {
     }
     String[] values = value.split(",");
     List<String> trimmedValues = new ArrayList<>();
-    for (int i = 0; i < values.length; i++) {
-      String trimmedValue = values[i].trim();
+    for (String s : values) {
+      String trimmedValue = s.trim();
       if (!trimmedValue.isEmpty()) {
         trimmedValues.add(trimmedValue);
       }
     }
-    return trimmedValues.toArray(new String[trimmedValues.size()]);
+    return trimmedValues.toArray(new String[0]);
   }
 }
diff --git a/src/main/java/org/sonarsource/scanner/cli/Logs.java b/src/main/java/org/sonarsource/scanner/cli/Logs.java
deleted file mode 100644 (file)
index f8b4ce3..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * SonarScanner CLI
- * Copyright (C) 2011-2024 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.sonarsource.scanner.cli;
-
-import java.io.PrintStream;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
-import org.sonarsource.scanner.lib.LogOutput;
-
-public class Logs {
-  private DateTimeFormatter timeFormatter;
-  private boolean debugEnabled = false;
-  private PrintStream stdOut;
-  private PrintStream stdErr;
-
-  public Logs(PrintStream stdOut, PrintStream stdErr) {
-    this.stdErr = stdErr;
-    this.stdOut = stdOut;
-    this.timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
-  }
-
-  public void setDebugEnabled(boolean debugEnabled) {
-    this.debugEnabled = debugEnabled;
-  }
-
-  public boolean isDebugEnabled() {
-    return debugEnabled;
-  }
-
-  public void debug(String message) {
-    if (isDebugEnabled()) {
-      LocalTime currentTime = LocalTime.now();
-      String timestamp = currentTime.format(timeFormatter);
-      stdOut.println(timestamp + " DEBUG: " + message);
-    }
-  }
-
-  public void info(String message) {
-    print(stdOut, "INFO: " + message);
-  }
-
-  public void warn(String message) {
-    print(stdOut, "WARN: " + message);
-  }
-
-  public void error(String message) {
-    print(stdErr, "ERROR: " + message);
-  }
-
-  public void error(String message, Throwable t) {
-    print(stdErr, "ERROR: " + message);
-    t.printStackTrace(stdErr);
-  }
-
-  private void print(PrintStream stream, String msg) {
-    if (debugEnabled) {
-      LocalTime currentTime = LocalTime.now();
-      String timestamp = currentTime.format(timeFormatter);
-      stream.println(timestamp + " " + msg);
-    } else {
-      stream.println(msg);
-    }
-  }
-
-  /**
-   * Adapter for the scanner library.
-   */
-  public LogOutput getLogOutputAdapter() {
-    return new LogOutputAdapter(this);
-  }
-
-  static class LogOutputAdapter implements LogOutput {
-    private final Logs logs;
-
-    public LogOutputAdapter(Logs logs) {
-      this.logs = logs;
-    }
-
-    @Override
-    public void log(String formattedMessage, Level level) {
-      switch (level) {
-        case TRACE, DEBUG:
-          logs.debug(formattedMessage);
-          break;
-        case ERROR:
-          logs.error(formattedMessage);
-          break;
-        case WARN:
-          logs.warn(formattedMessage);
-          break;
-        case INFO:
-        default:
-          logs.info(formattedMessage);
-      }
-    }
-  }
-}
index 0da03aa957d15d24d46444ed55d94b84d6f976d8..7ec139d20b0045801ef0abfdc57f94eace1ca2b5 100644 (file)
  */
 package org.sonarsource.scanner.cli;
 
+import ch.qos.logback.classic.Level;
 import java.util.Map;
 import java.util.Properties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.sonarsource.scanner.lib.ScannerEngineBootstrapper;
 import org.sonarsource.scanner.lib.ScannerEngineFacade;
 import org.sonarsource.scanner.lib.ScannerProperties;
@@ -38,32 +41,31 @@ import org.sonarsource.scanner.lib.ScannerProperties;
  * @since 1.0
  */
 public class Main {
+  private static final Logger LOG = LoggerFactory.getLogger(Main.class);
+
   private static final String SEPARATOR = "------------------------------------------------------------------------";
   private final Exit exit;
   private final Cli cli;
   private final Conf conf;
   private ScannerEngineBootstrapper scannerEngineBootstrapper;
   private final ScannerEngineBootstrapperFactory bootstrapperFactory;
-  private final Logs logger;
 
-  Main(Exit exit, Cli cli, Conf conf, ScannerEngineBootstrapperFactory bootstrapperFactory, Logs logger) {
+  Main(Exit exit, Cli cli, Conf conf, ScannerEngineBootstrapperFactory bootstrapperFactory) {
     this.exit = exit;
     this.cli = cli;
     this.conf = conf;
     this.bootstrapperFactory = bootstrapperFactory;
-    this.logger = logger;
   }
 
   public static void main(String[] args) {
-    Logs logs = new Logs(System.out, System.err);
     Exit exit = new Exit();
-    Cli cli = new Cli(exit, logs).parse(args);
-    Main main = new Main(exit, cli, new Conf(cli, logs, System.getenv()), new ScannerEngineBootstrapperFactory(logs), logs);
+    Cli cli = new Cli(exit).parse(args);
+    Main main = new Main(exit, cli, new Conf(cli, System.getenv()), new ScannerEngineBootstrapperFactory());
     main.analyze();
   }
 
   void analyze() {
-    Stats stats = new Stats(logger).start();
+    Stats stats = new Stats().start();
 
     int status = Exit.INTERNAL_ERROR;
     try {
@@ -86,24 +88,24 @@ public class Main {
     }
   }
 
-  private void logServerType(ScannerEngineFacade engine) {
+  private static void logServerType(ScannerEngineFacade engine) {
     if (engine.isSonarCloud()) {
-      logger.info("Communicating with SonarCloud");
+      LOG.info("Communicating with SonarCloud");
     } else {
       String serverVersion = engine.getServerVersion();
-      logger.info(String.format("Communicating with SonarQube Server %s", serverVersion));
+      LOG.info("Communicating with SonarQube Server {}", serverVersion);
     }
   }
 
   private void checkSkip(Properties properties) {
     if ("true".equalsIgnoreCase(properties.getProperty(ScannerProperties.SKIP))) {
-      logger.info("SonarScanner CLI analysis skipped");
+      LOG.info("SonarScanner CLI analysis skipped");
       exit.exit(Exit.SUCCESS);
     }
   }
 
   private void init(Properties p) {
-    SystemInfo.print(logger);
+    SystemInfo.print();
     if (cli.isDisplayVersionOnly()) {
       exit.exit(Exit.SUCCESS);
     }
@@ -111,39 +113,40 @@ public class Main {
     scannerEngineBootstrapper = bootstrapperFactory.create(p, cli.getInvokedFrom());
   }
 
-  private void configureLogging(Properties props) {
+  private static void configureLogging(Properties props) {
     if ("true".equals(props.getProperty("sonar.verbose"))
       || "DEBUG".equalsIgnoreCase(props.getProperty("sonar.log.level"))
       || "TRACE".equalsIgnoreCase(props.getProperty("sonar.log.level"))) {
-      logger.setDebugEnabled(true);
+      var rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+      rootLogger.setLevel(Level.DEBUG);
     }
   }
 
-  private void displayExecutionResult(Stats stats, String resultMsg) {
-    logger.info(SEPARATOR);
-    logger.info("EXECUTION " + resultMsg);
-    logger.info(SEPARATOR);
+  private static void displayExecutionResult(Stats stats, String resultMsg) {
+    LOG.info(SEPARATOR);
+    LOG.info("EXECUTION {}", resultMsg);
+    LOG.info(SEPARATOR);
     stats.stop();
-    logger.info(SEPARATOR);
+    LOG.info(SEPARATOR);
   }
 
   private void showError(String message, Throwable e, boolean debug) {
     if (debug || !isUserError(e)) {
-      logger.error(message, e);
+      LOG.error(message, e);
     } else {
-      logger.error(message);
-      logger.error(e.getMessage());
+      LOG.error(message);
+      LOG.error(e.getMessage());
       String previousMsg = "";
       for (Throwable cause = e.getCause(); cause != null
         && cause.getMessage() != null
         && !cause.getMessage().equals(previousMsg); cause = cause.getCause()) {
-        logger.error("Caused by: " + cause.getMessage());
+        LOG.error("Caused by: {}", cause.getMessage());
         previousMsg = cause.getMessage();
       }
     }
 
     if (!cli.isDebugEnabled()) {
-      logger.error("");
+      LOG.error("");
       suggestDebugMode();
     }
   }
@@ -155,7 +158,7 @@ public class Main {
 
   private void suggestDebugMode() {
     if (!cli.isEmbedded()) {
-      logger.error("Re-run SonarScanner CLI using the -X switch to enable full debug logging.");
+      LOG.error("Re-run SonarScanner CLI using the -X switch to enable full debug logging.");
     }
   }
 
index 7e88d1910ba9a9ba12a87eaa4630930a78128424..0b33f8b8ef1d1d74f33d0e897aaa57b29423e0fc 100644 (file)
@@ -31,7 +31,7 @@ public class PropertyResolver {
   private final Properties props;
   private final Properties resolved;
   private final List<String> queue;
-  private Map<String, String> env;
+  private final Map<String, String> env;
 
   public PropertyResolver(Properties props, Map<String, String> env) {
     this.props = props;
@@ -72,7 +72,7 @@ public class PropertyResolver {
     }
 
     Matcher m = placeholderPattern.matcher(propValue);
-    StringBuffer sb = new StringBuffer();
+    var sb = new StringBuilder();
 
     while (m.find()) {
       String varName = (null == m.group(1)) ? m.group(2) : m.group(1);
index f9e7fe73dd563f9cc0dcb1b97856c9e1eb053a11..63ee9d0df05245dd75383a5acdca0e3e3c63d52c 100644 (file)
@@ -25,12 +25,6 @@ import org.sonarsource.scanner.lib.ScannerEngineBootstrapper;
 
 class ScannerEngineBootstrapperFactory {
 
-  private final Logs logger;
-
-  public ScannerEngineBootstrapperFactory(Logs logger) {
-    this.logger = logger;
-  }
-
   ScannerEngineBootstrapper create(Properties props, String isInvokedFrom) {
     String appName = "ScannerCLI";
     String appVersion = ScannerVersion.version();
@@ -44,7 +38,7 @@ class ScannerEngineBootstrapperFactory {
   }
 
   ScannerEngineBootstrapper newScannerEngineBootstrapper(String appName, String appVersion) {
-    return ScannerEngineBootstrapper.create(appName, appVersion, logger.getLogOutputAdapter());
+    return ScannerEngineBootstrapper.create(appName, appVersion, new Slf4jLogOutput());
   }
 
 
index a5f3ede90228513ebcba4ca9518197f9e0bd4322..831841ba0d20fed8fedd2bf30f85b1bc393ec73a 100644 (file)
  */
 package org.sonarsource.scanner.cli;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Scanner;
 
+import static java.util.Objects.requireNonNull;
+
 public enum ScannerVersion {
 
   INSTANCE;
 
-  private String version;
+  private final String version;
 
   ScannerVersion() {
-    try (Scanner scanner = new Scanner(getClass().getResourceAsStream("/version.txt"), "UTF-8")) {
+    try (Scanner scanner = new Scanner(requireNonNull(getClass().getResourceAsStream("/version.txt")), StandardCharsets.UTF_8)) {
       this.version = scanner.next();
     }
   }
diff --git a/src/main/java/org/sonarsource/scanner/cli/Slf4jLogOutput.java b/src/main/java/org/sonarsource/scanner/cli/Slf4jLogOutput.java
new file mode 100644 (file)
index 0000000..2ce5cba
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * SonarScanner CLI
+ * Copyright (C) 2011-2024 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.sonarsource.scanner.cli;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonarsource.scanner.lib.LogOutput;
+
+public class Slf4jLogOutput implements LogOutput {
+
+  private static final Logger LOG = LoggerFactory.getLogger(Slf4jLogOutput.class);
+
+  @Override
+  public void log(String s, Level level) {
+    switch (level) {
+      case TRACE:
+        LOG.trace(s);
+        break;
+      case DEBUG:
+        LOG.debug(s);
+        break;
+      case INFO:
+        LOG.info(s);
+        break;
+      case WARN:
+        LOG.warn(s);
+        break;
+      case ERROR:
+        LOG.error(s);
+        break;
+    }
+  }
+}
index 128877bd97eb42b0246687baea643c846653740a..0d9913a6fa16d7ab5b642624da9eb26c944c0628 100644 (file)
  */
 package org.sonarsource.scanner.cli;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 class Stats {
-  private final Logs logger;
+  private static final Logger LOG = LoggerFactory.getLogger(Stats.class);
   private long startTime;
 
-  Stats(Logs logger) {
-    this.logger = logger;
-  }
-
   Stats start() {
     startTime = System.currentTimeMillis();
     return this;
@@ -34,12 +33,12 @@ class Stats {
 
   Stats stop() {
     long stopTime = System.currentTimeMillis() - startTime;
-    logger.info("Total time: " + formatTime(stopTime));
+    LOG.atInfo().addArgument(() -> formatTime(stopTime)).log("Total time: {}");
 
     System.gc();
     Runtime r = Runtime.getRuntime();
     long mb = 1024L * 1024;
-    logger.info("Final Memory: " + (r.totalMemory() - r.freeMemory()) / mb + "M/" + r.totalMemory() / mb + "M");
+    LOG.atInfo().addArgument((r.totalMemory() - r.freeMemory()) / mb + "M/" + r.totalMemory() / mb + "M").log("Final Memory: {}");
 
     return this;
   }
index 30abee502695e359c31021046da4ea296954a0db..205a9024865c1c5aabc520c86d0704e860d5c8e2 100644 (file)
@@ -22,12 +22,16 @@ package org.sonarsource.scanner.cli;
 import java.util.Set;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 class SystemInfo {
+  private static final Logger LOG = LoggerFactory.getLogger(SystemInfo.class);
+
   private static final Set<String> SENSITIVE_JVM_ARGUMENTS = Set.of(
-      "-Dsonar.login",
-      "-Dsonar.password",
-      "-Dsonar.token");
+    "-Dsonar.login",
+    "-Dsonar.password",
+    "-Dsonar.token");
   private static final Pattern PATTERN_ARGUMENT_SEPARATOR = Pattern.compile("\\s+");
   private static System2 system = new System2();
 
@@ -38,13 +42,13 @@ class SystemInfo {
     SystemInfo.system = system;
   }
 
-  static void print(Logs logger) {
-    logger.info("SonarScanner CLI " + ScannerVersion.version());
-    logger.info(java());
-    logger.info(os());
+  static void print() {
+    LOG.info("SonarScanner CLI {}", ScannerVersion.version());
+    LOG.atInfo().log(SystemInfo::java);
+    LOG.atInfo().log(SystemInfo::os);
     String scannerOpts = system.getenv("SONAR_SCANNER_OPTS");
     if (scannerOpts != null) {
-      logger.info("SONAR_SCANNER_OPTS=" + redactSensitiveArguments(scannerOpts));
+      LOG.atInfo().addArgument(() -> redactSensitiveArguments(scannerOpts)).log("SONAR_SCANNER_OPTS={}");
     }
   }
 
@@ -77,14 +81,11 @@ class SystemInfo {
   }
 
   static String os() {
-    StringBuilder sb = new StringBuilder();
-    sb
-      .append(system.getProperty("os.name"))
-      .append(" ")
-      .append(system.getProperty("os.version"))
-      .append(" ")
-      .append(system.getProperty("os.arch"));
-    return sb.toString();
+    return system.getProperty("os.name")
+      + " "
+      + system.getProperty("os.version")
+      + " "
+      + system.getProperty("os.arch");
   }
 
   static class System2 {
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644 (file)
index 0000000..0a76e8f
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration>
+
+<configuration scan="false">
+  <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
+  <import class="ch.qos.logback.core.ConsoleAppender"/>
+
+  <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
+    <target>System.err</target>
+    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+      <level>ERROR</level>
+    </filter>
+    <encoder class="PatternLayoutEncoder">
+      <pattern>%date{HH:mm:ss.SSS} %-5level %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <target>System.out</target>
+    <filter class="ch.qos.logback.classic.filter.LevelFilter">
+      <level>TRACE</level>
+      <onMatch>ACCEPT</onMatch>
+    </filter>
+    <filter class="ch.qos.logback.classic.filter.LevelFilter">
+      <level>DEBUG</level>
+      <onMatch>ACCEPT</onMatch>
+    </filter>
+    <filter class="ch.qos.logback.classic.filter.LevelFilter">
+      <level>INFO</level>
+      <onMatch>ACCEPT</onMatch>
+    </filter>
+    <filter class="ch.qos.logback.classic.filter.LevelFilter">
+      <level>WARN</level>
+      <onMatch>ACCEPT</onMatch>
+    </filter>
+    <filter class="ch.qos.logback.classic.filter.LevelFilter">
+      <level>ERROR</level>
+      <onMatch>DENY</onMatch>
+    </filter>
+    <encoder class="PatternLayoutEncoder">
+      <pattern>%date{HH:mm:ss.SSS} %-5level %msg%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="INFO">
+    <appender-ref ref="STDOUT"/>
+    <appender-ref ref="STDERR"/>
+  </root>
+
+</configuration>
index 6b2622ad59bb1010633ccceeb7e68ba943dc297c..5cb05d8210d7ff5f94bd8a45f55432507cacac8b 100644 (file)
  */
 package org.sonarsource.scanner.cli;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.event.Level;
+import testutils.LogTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatNoException;
@@ -28,9 +35,12 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 class CliTest {
+
+  @RegisterExtension
+  LogTester logTester = new LogTester();
+
   private final Exit exit = mock(Exit.class);
-  private Logs logs = new Logs(System.out, System.err);
-  private Cli cli = new Cli(exit, logs);
+  private Cli cli = new Cli(exit);
 
   @Test
   void should_parse_empty_arguments() {
@@ -52,18 +62,16 @@ class CliTest {
 
   @Test
   void should_warn_on_duplicate_properties() {
-    logs = mock(Logs.class);
-    cli = new Cli(exit, logs);
+    cli = new Cli(exit);
     cli.parse(new String[]{"-D", "foo=bar", "--define", "foo=baz"});
-    verify(logs).warn("Property 'foo' with value 'bar' is overridden with value 'baz'");
+    assertThat(logTester.logs(Level.WARN)).contains("Property 'foo' with value 'bar' is overridden with value 'baz'");
   }
 
   @Test
   void should_fail_on_missing_prop() {
-    logs = mock(Logs.class);
-    cli = new Cli(exit, logs);
+    cli = new Cli(exit);
     cli.parse(new String[]{"-D"});
-    verify(logs).error("Missing argument for option -D/--define");
+    assertThat(logTester.logs(Level.ERROR)).contains("Missing argument for option -D/--define");
     verify(exit).exit(Exit.INTERNAL_ERROR);
   }
 
@@ -132,31 +140,32 @@ class CliTest {
     assertThat(cli.properties().get("sonar.verbose")).isNull();
   }
 
-  @Test
-  void should_show_usage() {
-    logs = mock(Logs.class);
-    cli = new Cli(exit, logs);
-    cli.parse(new String[]{"-h"});
-    verify(logs).info("usage: sonar-scanner [options]");
+  @ParameterizedTest
+  @ValueSource(strings = {"-h", "--help"})
+  void should_show_usage(String arg) {
+    var baos = parseAndCaptureStdOut(arg);
+    assertThat(baos.toString()).contains("usage: sonar-scanner [options]");
     verify(exit).exit(Exit.SUCCESS);
   }
 
-  @Test
-  void should_show_usage_full() {
-    logs = mock(Logs.class);
-    cli = new Cli(exit, logs);
-    cli.parse(new String[]{"--help"});
-    verify(logs).info("usage: sonar-scanner [options]");
-    verify(exit).exit(Exit.SUCCESS);
+  private ByteArrayOutputStream parseAndCaptureStdOut(String arg) {
+    var baos = new ByteArrayOutputStream();
+    var savedOut = System.out;
+    try {
+      System.setOut(new PrintStream(baos));
+      cli = new Cli(exit);
+      cli.parse(new String[]{arg});
+    } finally {
+      System.setOut(savedOut);
+    }
+    return baos;
   }
 
   @Test
   void should_show_usage_on_bad_syntax() {
-    logs = mock(Logs.class);
-    cli = new Cli(exit, logs);
-    cli.parse(new String[]{"-w"});
-    verify(logs).error("Unrecognized option: -w");
-    verify(logs).info("usage: sonar-scanner [options]");
+    var baos = parseAndCaptureStdOut("-w");
+    assertThat(baos.toString()).contains("usage: sonar-scanner [options]");
+    assertThat(logTester.logs(Level.ERROR)).contains("Unrecognized option: -w");
     verify(exit).exit(Exit.INTERNAL_ERROR);
   }
 
index ea031a05ee74edfc747278fe4e42aa04b3f6fa10..82bd9550caa662b3c88ea428dfcc64bdce55cd66 100644 (file)
@@ -43,9 +43,8 @@ class ConfTest {
 
   private final Map<String, String> env = new HashMap<>();
   private final Properties args = new Properties();
-  private final Logs logs = new Logs(System.out, System.err);
   private final Cli cli = mock(Cli.class);
-  private final Conf conf = new Conf(cli, logs, env);
+  private final Conf conf = new Conf(cli, env);
 
   @BeforeEach
   void initConf() {
diff --git a/src/test/java/org/sonarsource/scanner/cli/LogsTest.java b/src/test/java/org/sonarsource/scanner/cli/LogsTest.java
deleted file mode 100644 (file)
index 1bb1abe..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SonarScanner CLI
- * Copyright (C) 2011-2024 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.sonarsource.scanner.cli;
-
-import java.io.PrintStream;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentMatchers;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.sonarsource.scanner.lib.LogOutput;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-class LogsTest {
-  @Mock
-  private PrintStream stdOut;
-
-  @Mock
-  private PrintStream stdErr;
-
-  private Logs logs;
-
-  @BeforeEach
-  void setUp() {
-    MockitoAnnotations.initMocks(this);
-    logs = new Logs(stdOut, stdErr);
-  }
-
-  @Test
-  void testInfo() {
-    logs.info("info");
-    verify(stdOut).println("INFO: info");
-    verifyNoMoreInteractions(stdOut, stdErr);
-  }
-
-  @Test
-  void testWarn() {
-    logs.warn("warn");
-    verify(stdOut).println("WARN: warn");
-    verifyNoMoreInteractions(stdOut, stdErr);
-  }
-
-  @Test
-  void testWarnWithTimestamp() {
-    logs.setDebugEnabled(true);
-    logs.warn("warn");
-    verify(stdOut).println(ArgumentMatchers.matches("\\d\\d:\\d\\d:\\d\\d.\\d\\d\\d WARN: warn"));
-    verifyNoMoreInteractions(stdOut, stdErr);
-  }
-
-  @Test
-  void testError() {
-    Exception e = new NullPointerException("exception");
-    logs.error("error1");
-    verify(stdErr).println("ERROR: error1");
-
-    logs.error("error2", e);
-    verify(stdErr).println("ERROR: error2");
-    verify(stdErr).println(e);
-    // other interactions to print the exception..
-  }
-
-  @Test
-  void testDebug() {
-    logs.setDebugEnabled(true);
-
-    logs.debug("debug");
-    verify(stdOut).println(ArgumentMatchers.matches("\\d\\d:\\d\\d:\\d\\d.\\d\\d\\d DEBUG: debug$"));
-
-    logs.setDebugEnabled(false);
-    logs.debug("debug");
-    verifyNoMoreInteractions(stdOut, stdErr);
-  }
-
-  @Test
-  void should_forward_logs() {
-    var mockedLogs = mock(Logs.class);
-    var logOutput = new Logs.LogOutputAdapter(mockedLogs);
-
-    String msg = "test";
-
-    logOutput.log(msg, LogOutput.Level.DEBUG);
-    verify(mockedLogs).debug(msg);
-    verifyNoMoreInteractions(mockedLogs);
-    reset(mockedLogs);
-
-    logOutput.log(msg, LogOutput.Level.INFO);
-    verify(mockedLogs).info(msg);
-    verifyNoMoreInteractions(mockedLogs);
-    reset(mockedLogs);
-
-    logOutput.log(msg, LogOutput.Level.ERROR);
-    verify(mockedLogs).error(msg);
-    verifyNoMoreInteractions(mockedLogs);
-    reset(mockedLogs);
-
-    logOutput.log(msg, LogOutput.Level.WARN);
-    verify(mockedLogs).warn(msg);
-    verifyNoMoreInteractions(mockedLogs);
-    reset(mockedLogs);
-
-    logOutput.log(msg, LogOutput.Level.TRACE);
-    verify(mockedLogs).debug(msg);
-    verifyNoMoreInteractions(mockedLogs);
-    reset(mockedLogs);
-  }
-}
index 94bd544458a2d2d93657f23b7c44c7a27d519e2d..7b6ccfd8d137f566d57e9a366e5b35d43f952c0d 100644 (file)
@@ -23,47 +23,42 @@ import java.util.Map;
 import java.util.Properties;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
 import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
-import org.mockito.Mock;
 import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
+import org.slf4j.LoggerFactory;
+import org.slf4j.event.Level;
 import org.sonar.api.utils.MessageException;
 import org.sonarsource.scanner.lib.ScannerEngineBootstrapper;
 import org.sonarsource.scanner.lib.ScannerEngineFacade;
 import org.sonarsource.scanner.lib.ScannerProperties;
+import testutils.LogTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-public class MainTest {
-
-  @Mock
-  private Exit exit;
-  @Mock
-  private Cli cli;
-  @Mock
-  private Conf conf;
-  @Mock
-  private Properties properties;
-  @Mock
-  private ScannerEngineBootstrapperFactory scannerEngineBootstrapperFactory;
-  @Mock
-  private ScannerEngineBootstrapper bootstrapper;
-  @Mock
-  private ScannerEngineFacade engine;
-  @Mock
-  private Logs logs;
+class MainTest {
+
+  @RegisterExtension
+  LogTester logTester = new LogTester();
+
+  private final Exit exit = mock();
+  private final Cli cli = mock();
+  private final Conf conf = mock();
+  private final Properties properties = mock();
+  private final ScannerEngineBootstrapperFactory scannerEngineBootstrapperFactory = mock();
+  private final ScannerEngineBootstrapper bootstrapper = mock();
+  private final ScannerEngineFacade engine = mock();
 
   @BeforeEach
   void setUp() {
-    MockitoAnnotations.initMocks(this);
     when(scannerEngineBootstrapperFactory.create(any(Properties.class), any(String.class))).thenReturn(bootstrapper);
     when(bootstrapper.bootstrap()).thenReturn(engine);
     when(conf.properties()).thenReturn(properties);
@@ -72,7 +67,7 @@ public class MainTest {
   @Test
   void should_execute_scanner_engine() {
     when(cli.getInvokedFrom()).thenReturn("");
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
 
     verify(exit).exit(Exit.SUCCESS);
@@ -89,11 +84,11 @@ public class MainTest {
     doThrow(e).when(engine).analyze(any());
     when(cli.getInvokedFrom()).thenReturn("");
     when(cli.isDebugEnabled()).thenReturn(true);
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
 
     verify(exit).exit(Exit.INTERNAL_ERROR);
-    verify(logs).error("Error during SonarScanner CLI execution", e);
+    assertThat(logTester.logs(Level.ERROR)).contains("Error during SonarScanner CLI execution");
   }
 
   @Test
@@ -104,13 +99,13 @@ public class MainTest {
     when(cli.getInvokedFrom()).thenReturn("");
     when(cli.isDebugEnabled()).thenReturn(true);
 
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
 
     verify(bootstrapper).bootstrap();
     verify(engine, never()).analyze(any());
     verify(exit).exit(Exit.INTERNAL_ERROR);
-    verify(logs).error("Error during SonarScanner CLI execution", e);
+    assertThat(logTester.logs(Level.ERROR)).contains("Error during SonarScanner CLI execution");
   }
 
   @Test
@@ -118,8 +113,8 @@ public class MainTest {
     Exception e = createException(false);
     testException(e, false, false, Exit.INTERNAL_ERROR);
 
-    verify(logs).error("Error during SonarScanner CLI execution", e);
-    verify(logs).error("Re-run SonarScanner CLI using the -X switch to enable full debug logging.");
+    assertThat(logTester.logs(Level.ERROR)).contains("Error during SonarScanner CLI execution");
+    assertThat(logTester.logs(Level.ERROR)).contains("Re-run SonarScanner CLI using the -X switch to enable full debug logging.");
   }
 
   @Test
@@ -127,12 +122,11 @@ public class MainTest {
     Exception e = createException(true);
     testException(e, false, false, Exit.USER_ERROR);
 
-    verify(logs, times(5)).error(anyString());
-    verify(logs).error("Error during SonarScanner CLI execution");
-    verify(logs).error("my message");
-    verify(logs).error("Caused by: A functional cause");
-    verify(logs).error("");
-    verify(logs).error("Re-run SonarScanner CLI using the -X switch to enable full debug logging.");
+    assertThat(logTester.logs(Level.ERROR)).containsOnly("Error during SonarScanner CLI execution",
+      "my message",
+      "Caused by: A functional cause",
+      "",
+      "Re-run SonarScanner CLI using the -X switch to enable full debug logging.");
   }
 
   @Test
@@ -140,11 +134,10 @@ public class MainTest {
     Exception e = createException(true);
     testException(e, false, true, Exit.USER_ERROR);
 
-    verify(logs, times(4)).error(anyString());
-    verify(logs).error("Error during SonarScanner CLI execution");
-    verify(logs).error("my message");
-    verify(logs).error("Caused by: A functional cause");
-    verify(logs).error("");
+    assertThat(logTester.logs(Level.ERROR)).containsOnly("Error during SonarScanner CLI execution",
+      "my message",
+      "Caused by: A functional cause",
+      "");
   }
 
   @Test
@@ -152,8 +145,7 @@ public class MainTest {
     Exception e = createException(true);
     testException(e, true, false, Exit.USER_ERROR);
 
-    verify(logs, times(1)).error(anyString(), any(Throwable.class));
-    verify(logs).error("Error during SonarScanner CLI execution", e);
+    assertThat(logTester.logs(Level.ERROR)).containsOnly("Error during SonarScanner CLI execution");
   }
 
   @Test
@@ -161,8 +153,7 @@ public class MainTest {
     Exception e = createException(true);
     testException(e, true, true, Exit.USER_ERROR);
 
-    verify(logs, times(1)).error(anyString(), any(Throwable.class));
-    verify(logs).error("Error during SonarScanner CLI execution", e);
+    assertThat(logTester.logs(Level.ERROR)).containsOnly("Error during SonarScanner CLI execution");
   }
 
   @Test
@@ -170,8 +161,7 @@ public class MainTest {
     Exception e = createException(false);
     testException(e, true, false, Exit.INTERNAL_ERROR);
 
-    verify(logs).error("Error during SonarScanner CLI execution", e);
-    verify(logs, never()).error("Re-run SonarScanner CLI using the -X switch to enable full debug logging.");
+    assertThat(logTester.logs(Level.ERROR)).containsOnly("Error during SonarScanner CLI execution");
   }
 
   private void testException(Exception e, boolean debugEnabled, boolean isEmbedded, int expectedExitCode) {
@@ -184,7 +174,7 @@ public class MainTest {
 
     when(scannerEngineBootstrapperFactory.create(any(Properties.class), any(String.class))).thenReturn(bootstrapper);
 
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
 
     verify(exit).exit(expectedExitCode);
@@ -208,7 +198,7 @@ public class MainTest {
     when(cli.getInvokedFrom()).thenReturn("");
     when(conf.properties()).thenReturn(p);
 
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
 
     InOrder inOrder = Mockito.inOrder(exit, scannerEngineBootstrapperFactory);
@@ -225,10 +215,10 @@ public class MainTest {
     when(conf.properties()).thenReturn(p);
     when(cli.getInvokedFrom()).thenReturn("");
 
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
 
-    verify(logs).info("SonarScanner CLI analysis skipped");
+    assertThat(logTester.logs(Level.INFO)).contains("SonarScanner CLI analysis skipped");
     InOrder inOrder = Mockito.inOrder(exit, scannerEngineBootstrapperFactory);
 
     inOrder.verify(exit, times(1)).exit(Exit.SUCCESS);
@@ -245,9 +235,9 @@ public class MainTest {
     when(cli.getInvokedFrom()).thenReturn("");
     when(conf.properties()).thenReturn(p);
 
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
-    verify(logs).info("Communicating with SonarQube Server 5.5");
+    assertThat(logTester.logs(Level.INFO)).contains("Communicating with SonarQube Server 5.5");
   }
 
   @Test
@@ -257,9 +247,9 @@ public class MainTest {
     when(conf.properties()).thenReturn(p);
     when(cli.getInvokedFrom()).thenReturn("");
 
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
-    verify(logs).info("Communicating with SonarCloud");
+    assertThat(logTester.logs(Level.INFO)).contains("Communicating with SonarCloud");
   }
 
   @Test
@@ -290,7 +280,7 @@ public class MainTest {
     Properties actualProps = execute(propKey, propValue);
 
     // Logger used for callback should have debug enabled
-    verify(logs).setDebugEnabled(true);
+    assertThat(LoggerFactory.getLogger(getClass()).isDebugEnabled()).isTrue();
 
     return actualProps;
   }
@@ -302,7 +292,7 @@ public class MainTest {
     when(conf.properties()).thenReturn(p);
     when(cli.getInvokedFrom()).thenReturn("");
 
-    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory, logs);
+    Main main = new Main(exit, cli, conf, scannerEngineBootstrapperFactory);
     main.analyze();
 
     ArgumentCaptor<Properties> propertiesCapture = ArgumentCaptor.forClass(Properties.class);
index cfaca6f90c330bfd844c86b5825d653885e6b2ef..c25ddb15395cb18927a97aa31289f64bd2901ee0 100644 (file)
@@ -36,8 +36,7 @@ import static org.mockito.Mockito.when;
 class ScannerEngineBootstrapperFactoryTest {
 
   private final Properties props = new Properties();
-  private final Logs logs = mock(Logs.class);
-  private final ScannerEngineBootstrapperFactory underTest = new ScannerEngineBootstrapperFactory(logs);
+  private final ScannerEngineBootstrapperFactory underTest = new ScannerEngineBootstrapperFactory();
 
   @Test
   void should_create_engine_bootstrapper_and_pass_app_and_properties() {
diff --git a/src/test/java/org/sonarsource/scanner/cli/Slf4jLogOutputTest.java b/src/test/java/org/sonarsource/scanner/cli/Slf4jLogOutputTest.java
new file mode 100644 (file)
index 0000000..adf29f4
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarScanner CLI
+ * Copyright (C) 2011-2024 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.sonarsource.scanner.cli;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.slf4j.event.Level;
+import org.sonarsource.scanner.lib.LogOutput;
+import testutils.LogTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class Slf4jLogOutputTest {
+
+  @RegisterExtension
+  LogTester logTester = new LogTester().setLevel(Level.TRACE);
+
+  @Test
+  void make_coverage_happy() {
+    var underTest = new Slf4jLogOutput();
+    underTest.log("trace", LogOutput.Level.TRACE);
+    underTest.log("debug", LogOutput.Level.DEBUG);
+    underTest.log("info", LogOutput.Level.INFO);
+    underTest.log("warn", LogOutput.Level.WARN);
+    underTest.log("error", LogOutput.Level.ERROR);
+
+    assertThat(logTester.logs(Level.TRACE)).containsOnly("trace");
+    assertThat(logTester.logs(Level.DEBUG)).containsOnly("debug");
+    assertThat(logTester.logs(Level.INFO)).containsOnly("info");
+    assertThat(logTester.logs(Level.WARN)).containsOnly("warn");
+    assertThat(logTester.logs(Level.ERROR)).containsOnly("error");
+  }
+
+}
index a39e9ec5e3b3af6d97e31cf02105c34e66e3087c..489564040323adabf3e3bea12641e1d36fe2d29e 100644 (file)
  */
 package org.sonarsource.scanner.cli;
 
-import java.io.PrintStream;
 import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.slf4j.event.Level;
+import testutils.LogTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
 class StatsTest {
-  private final PrintStream stdOut = mock(PrintStream.class);
-  private final PrintStream stdErr = mock(PrintStream.class);
-  private final Logs logs = new Logs(stdOut, stdErr);
+  @RegisterExtension
+  LogTester logTester = new LogTester();
 
   @Test
   void shouldPrintStats() {
-    new Stats(logs).start().stop();
+    new Stats().start().stop();
 
-    verify(stdOut).println(Mockito.contains("Total time: "));
-    verify(stdOut).println(Mockito.contains("Final Memory: "));
+    assertThat(logTester.logs(Level.INFO)).hasSize(2);
+    assertThat(logTester.logs(Level.INFO).get(0)).startsWith("Total time: ");
+    assertThat(logTester.logs(Level.INFO).get(1)).startsWith("Final Memory: ");
   }
 
   @Test
index 7344270178b834470c69fc58c928f59ed5678fbf..b372d9c4a44bf62ddf6ef4ff419ba2310fc771f4 100644 (file)
@@ -21,18 +21,21 @@ package org.sonarsource.scanner.cli;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.slf4j.event.Level;
 import org.sonarsource.scanner.cli.SystemInfo.System2;
+import testutils.LogTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 class SystemInfoTest {
+  @RegisterExtension
+  LogTester logTester = new LogTester();
+
   private final System2 mockSystem = mock(System2.class);
-  private final Logs logs = mock(Logs.class);
 
   @BeforeEach
   void setUp() {
@@ -76,18 +79,14 @@ class SystemInfoTest {
     mockJava();
     when(mockSystem.getenv("SONAR_SCANNER_OPTS")).thenReturn("arg");
 
-    SystemInfo.print(logs);
+    SystemInfo.print();
 
     verify(mockSystem).getProperty("java.version");
     verify(mockSystem).getProperty("os.version");
     verify(mockSystem).getenv("SONAR_SCANNER_OPTS");
 
-    verify(logs, never()).info("SonarScanner null");
-    verify(logs).info("SonarScanner CLI " + ScannerVersion.version());
-    verify(logs).info("Java 1.9 oracle (64-bit)");
-    verify(logs).info("linux 2.5 x64");
-    verify(logs).info("SONAR_SCANNER_OPTS=arg");
-    verifyNoMoreInteractions(logs);
+    assertThat(logTester.logs(Level.INFO))
+      .containsOnly("SonarScanner CLI " + ScannerVersion.version(), "Java 1.9 oracle (64-bit)", "linux 2.5 x64", "SONAR_SCANNER_OPTS=arg");
   }
 
   @Test
@@ -97,8 +96,8 @@ class SystemInfoTest {
     when(mockSystem.getenv("SONAR_SCANNER_OPTS"))
       .thenReturn("-Dsonar.login=login -Dsonar.whatever=whatever -Dsonar.password=password -Dsonar.whatever2=whatever2 -Dsonar.token=token");
 
-    SystemInfo.print(logs);
+    SystemInfo.print();
 
-    verify(logs).info("SONAR_SCANNER_OPTS=-Dsonar.login=* -Dsonar.whatever=whatever -Dsonar.password=* -Dsonar.whatever2=whatever2 -Dsonar.token=*");
+    assertThat(logTester.logs(Level.INFO)).contains("SONAR_SCANNER_OPTS=-Dsonar.login=* -Dsonar.whatever=whatever -Dsonar.password=* -Dsonar.whatever2=whatever2 -Dsonar.token=*");
   }
 }
diff --git a/src/test/java/testutils/ConcurrentListAppender.java b/src/test/java/testutils/ConcurrentListAppender.java
new file mode 100644 (file)
index 0000000..b31e34e
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarScanner CLI
+ * Copyright (C) 2011-2024 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 testutils;
+
+import ch.qos.logback.core.AppenderBase;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+public class ConcurrentListAppender<E> extends AppenderBase<E> {
+  public final Queue<E> list = new ConcurrentLinkedQueue<E>();
+
+  protected void append(E e) {
+    list.add(e);
+  }
+}
diff --git a/src/test/java/testutils/LogTester.java b/src/test/java/testutils/LogTester.java
new file mode 100644 (file)
index 0000000..1708479
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarScanner CLI
+ * Copyright (C) 2011-2024 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 testutils;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.LoggingEvent;
+import java.util.List;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.slf4j.LoggerFactory;
+import org.slf4j.event.Level;
+
+public class LogTester implements BeforeEachCallback, AfterEachCallback {
+
+  private final ConcurrentListAppender<ILoggingEvent> listAppender = new ConcurrentListAppender<>();
+
+  public LogTester() {
+    setLevel(Level.INFO);
+  }
+
+  /**
+   * Change log level.
+   * By default, INFO logs are enabled when LogTester is started.
+   */
+  public LogTester setLevel(Level level) {
+    getRootLogger().setLevel(ch.qos.logback.classic.Level.fromLocationAwareLoggerInteger(level.toInt()));
+    return this;
+  }
+
+  private static ch.qos.logback.classic.Logger getRootLogger() {
+    return (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
+  }
+
+  /**
+   * Logs in chronological order (item at index 0 is the oldest one)
+   */
+  public List<String> logs() {
+    return listAppender.list.stream().map(e -> (LoggingEvent) e)
+      .map(LoggingEvent::getFormattedMessage)
+      .toList();
+  }
+
+  /**
+   * Logs in chronological order (item at index 0 is the oldest one) for
+   * a given level
+   */
+  public List<String> logs(Level level) {
+    return listAppender.list.stream().map(e -> (LoggingEvent) e)
+      .filter(e -> e.getLevel().equals(ch.qos.logback.classic.Level.fromLocationAwareLoggerInteger(level.toInt())))
+      .map(LoggingEvent::getFormattedMessage)
+      .toList();
+  }
+
+  public LogTester clear() {
+    listAppender.list.clear();
+    return this;
+  }
+
+  @Override
+  public void beforeEach(ExtensionContext context) throws Exception {
+    getRootLogger().addAppender(listAppender);
+    listAppender.start();
+  }
+
+  @Override
+  public void afterEach(ExtensionContext context) throws Exception {
+    listAppender.stop();
+    listAppender.list.clear();
+    getRootLogger().detachAppender(listAppender);
+    // Reset the level for following-up test suites
+    setLevel(Level.INFO);
+  }
+}