From 90a5df59eddccf630deec66f8d487dd1d69114a9 Mon Sep 17 00:00:00 2001 From: Julien HENRY Date: Fri, 3 May 2024 11:41:04 +0200 Subject: [PATCH] SCANCLI-146 Log using slf4j + logback --- pom.xml | 35 ++++- .../java/org/sonarsource/scanner/cli/Cli.java | 42 +++--- .../org/sonarsource/scanner/cli/Conf.java | 27 ++-- .../org/sonarsource/scanner/cli/Logs.java | 114 ---------------- .../org/sonarsource/scanner/cli/Main.java | 53 ++++---- .../scanner/cli/PropertyResolver.java | 4 +- .../cli/ScannerEngineBootstrapperFactory.java | 8 +- .../scanner/cli/ScannerVersion.java | 7 +- .../scanner/cli/Slf4jLogOutput.java | 50 +++++++ .../org/sonarsource/scanner/cli/Stats.java | 13 +- .../sonarsource/scanner/cli/SystemInfo.java | 33 ++--- src/main/resources/logback.xml | 50 +++++++ .../org/sonarsource/scanner/cli/CliTest.java | 61 +++++---- .../org/sonarsource/scanner/cli/ConfTest.java | 3 +- .../org/sonarsource/scanner/cli/LogsTest.java | 128 ------------------ .../org/sonarsource/scanner/cli/MainTest.java | 102 +++++++------- .../ScannerEngineBootstrapperFactoryTest.java | 3 +- .../scanner/cli/Slf4jLogOutputTest.java | 51 +++++++ .../sonarsource/scanner/cli/StatsTest.java | 19 ++- .../scanner/cli/SystemInfoTest.java | 23 ++-- .../testutils/ConcurrentListAppender.java | 32 +++++ src/test/java/testutils/LogTester.java | 91 +++++++++++++ 22 files changed, 505 insertions(+), 444 deletions(-) delete mode 100644 src/main/java/org/sonarsource/scanner/cli/Logs.java create mode 100644 src/main/java/org/sonarsource/scanner/cli/Slf4jLogOutput.java create mode 100644 src/main/resources/logback.xml delete mode 100644 src/test/java/org/sonarsource/scanner/cli/LogsTest.java create mode 100644 src/test/java/org/sonarsource/scanner/cli/Slf4jLogOutputTest.java create mode 100644 src/test/java/testutils/ConcurrentListAppender.java create mode 100644 src/test/java/testutils/LogTester.java diff --git a/pom.xml b/pom.xml index fcd792f..dbe822e 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,11 @@ sonar-scanner-java-library 3.0.0.114 + + ch.qos.logback + logback-classic + 1.5.6 + com.google.code.findbugs jsr305 @@ -82,6 +87,12 @@ 5.10.1 test + + org.junit.jupiter + junit-jupiter-params + 5.10.1 + test + org.assertj assertj-core @@ -139,6 +150,26 @@ true true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + META-INF/LICENSE.txt + META-INF/NOTICE.txt + META-INF/MANIFEST.MF + **/module-info.class + + + + ch.qos.logback:logback-classic + + ** + + + @@ -182,8 +213,8 @@ - 3400000 - 3500000 + 4100000 + 4200000 ${project.build.directory}/sonar-scanner-${project.version}.zip diff --git a/src/main/java/org/sonarsource/scanner/cli/Cli.java b/src/main/java/org/sonarsource/scanner/cli/Cli.java index 4de2a36..a39b0af 100644 --- a/src/main/java/org/sonarsource/scanner/cli/Cli.java +++ b/src/main/java/org/sonarsource/scanner/cli/Cli.java @@ -19,23 +19,26 @@ */ 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 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 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"); } } diff --git a/src/main/java/org/sonarsource/scanner/cli/Conf.java b/src/main/java/org/sonarsource/scanner/cli/Conf.java index 4e9a53a..b1cbe62 100644 --- a/src/main/java/org/sonarsource/scanner/cli/Conf.java +++ b/src/main/java/org/sonarsource/scanner/cli/Conf.java @@ -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 env; private final long startTimeMs; - Conf(Cli cli, Logs logger, Map env) { + Conf(Cli cli, Map env) { this.cli = cli; - this.logger = logger; this.env = env; this.startTimeMs = System.currentTimeMillis(); } @@ -80,7 +82,7 @@ class Conf { } private Map 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. - * + *

* 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 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 index f8b4ce3..0000000 --- a/src/main/java/org/sonarsource/scanner/cli/Logs.java +++ /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); - } - } - } -} diff --git a/src/main/java/org/sonarsource/scanner/cli/Main.java b/src/main/java/org/sonarsource/scanner/cli/Main.java index 0da03aa..7ec139d 100644 --- a/src/main/java/org/sonarsource/scanner/cli/Main.java +++ b/src/main/java/org/sonarsource/scanner/cli/Main.java @@ -19,8 +19,11 @@ */ 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."); } } diff --git a/src/main/java/org/sonarsource/scanner/cli/PropertyResolver.java b/src/main/java/org/sonarsource/scanner/cli/PropertyResolver.java index 7e88d19..0b33f8b 100644 --- a/src/main/java/org/sonarsource/scanner/cli/PropertyResolver.java +++ b/src/main/java/org/sonarsource/scanner/cli/PropertyResolver.java @@ -31,7 +31,7 @@ public class PropertyResolver { private final Properties props; private final Properties resolved; private final List queue; - private Map env; + private final Map env; public PropertyResolver(Properties props, Map 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); diff --git a/src/main/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactory.java b/src/main/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactory.java index f9e7fe7..63ee9d0 100644 --- a/src/main/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactory.java +++ b/src/main/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactory.java @@ -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()); } diff --git a/src/main/java/org/sonarsource/scanner/cli/ScannerVersion.java b/src/main/java/org/sonarsource/scanner/cli/ScannerVersion.java index a5f3ede..831841b 100644 --- a/src/main/java/org/sonarsource/scanner/cli/ScannerVersion.java +++ b/src/main/java/org/sonarsource/scanner/cli/ScannerVersion.java @@ -19,16 +19,19 @@ */ 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 index 0000000..2ce5cba --- /dev/null +++ b/src/main/java/org/sonarsource/scanner/cli/Slf4jLogOutput.java @@ -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; + } + } +} diff --git a/src/main/java/org/sonarsource/scanner/cli/Stats.java b/src/main/java/org/sonarsource/scanner/cli/Stats.java index 128877b..0d9913a 100644 --- a/src/main/java/org/sonarsource/scanner/cli/Stats.java +++ b/src/main/java/org/sonarsource/scanner/cli/Stats.java @@ -19,14 +19,13 @@ */ 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; } diff --git a/src/main/java/org/sonarsource/scanner/cli/SystemInfo.java b/src/main/java/org/sonarsource/scanner/cli/SystemInfo.java index 30abee5..205a902 100644 --- a/src/main/java/org/sonarsource/scanner/cli/SystemInfo.java +++ b/src/main/java/org/sonarsource/scanner/cli/SystemInfo.java @@ -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 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 index 0000000..0a76e8f --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,50 @@ + + + + + + + + + System.err + + ERROR + + + %date{HH:mm:ss.SSS} %-5level %msg%n + + + + + System.out + + TRACE + ACCEPT + + + DEBUG + ACCEPT + + + INFO + ACCEPT + + + WARN + ACCEPT + + + ERROR + DENY + + + %date{HH:mm:ss.SSS} %-5level %msg%n + + + + + + + + + diff --git a/src/test/java/org/sonarsource/scanner/cli/CliTest.java b/src/test/java/org/sonarsource/scanner/cli/CliTest.java index 6b2622a..5cb05d8 100644 --- a/src/test/java/org/sonarsource/scanner/cli/CliTest.java +++ b/src/test/java/org/sonarsource/scanner/cli/CliTest.java @@ -19,7 +19,14 @@ */ 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); } diff --git a/src/test/java/org/sonarsource/scanner/cli/ConfTest.java b/src/test/java/org/sonarsource/scanner/cli/ConfTest.java index ea031a0..82bd955 100644 --- a/src/test/java/org/sonarsource/scanner/cli/ConfTest.java +++ b/src/test/java/org/sonarsource/scanner/cli/ConfTest.java @@ -43,9 +43,8 @@ class ConfTest { private final Map 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 index 1bb1abe..0000000 --- a/src/test/java/org/sonarsource/scanner/cli/LogsTest.java +++ /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); - } -} diff --git a/src/test/java/org/sonarsource/scanner/cli/MainTest.java b/src/test/java/org/sonarsource/scanner/cli/MainTest.java index 94bd544..7b6ccfd 100644 --- a/src/test/java/org/sonarsource/scanner/cli/MainTest.java +++ b/src/test/java/org/sonarsource/scanner/cli/MainTest.java @@ -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 propertiesCapture = ArgumentCaptor.forClass(Properties.class); diff --git a/src/test/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactoryTest.java b/src/test/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactoryTest.java index cfaca6f..c25ddb1 100644 --- a/src/test/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactoryTest.java +++ b/src/test/java/org/sonarsource/scanner/cli/ScannerEngineBootstrapperFactoryTest.java @@ -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 index 0000000..adf29f4 --- /dev/null +++ b/src/test/java/org/sonarsource/scanner/cli/Slf4jLogOutputTest.java @@ -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"); + } + +} diff --git a/src/test/java/org/sonarsource/scanner/cli/StatsTest.java b/src/test/java/org/sonarsource/scanner/cli/StatsTest.java index a39e9ec..4895640 100644 --- a/src/test/java/org/sonarsource/scanner/cli/StatsTest.java +++ b/src/test/java/org/sonarsource/scanner/cli/StatsTest.java @@ -19,25 +19,24 @@ */ 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 diff --git a/src/test/java/org/sonarsource/scanner/cli/SystemInfoTest.java b/src/test/java/org/sonarsource/scanner/cli/SystemInfoTest.java index 7344270..b372d9c 100644 --- a/src/test/java/org/sonarsource/scanner/cli/SystemInfoTest.java +++ b/src/test/java/org/sonarsource/scanner/cli/SystemInfoTest.java @@ -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 index 0000000..b31e34e --- /dev/null +++ b/src/test/java/testutils/ConcurrentListAppender.java @@ -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 extends AppenderBase { + public final Queue list = new ConcurrentLinkedQueue(); + + 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 index 0000000..1708479 --- /dev/null +++ b/src/test/java/testutils/LogTester.java @@ -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 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 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 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); + } +} -- 2.39.5