dependency 'javax.el:javax.el-api:3.0.0'
dependency 'org.glassfish:jakarta.el:3.0.4'
dependency 'org.kohsuke:github-api:1.318'
+ dependency 'org.wiremock:wiremock-standalone:3.5.2'
// please keep this list alphabetically ordered
}
dependencies {
api project(':sonar-scanner-engine')
}
+
+jar {
+ manifest {
+ attributes(
+ 'Main-Class' : "org.sonar.scanner.bootstrap.ScannerMain"
+ )
+ }
+}
testImplementation 'org.hamcrest:hamcrest-core'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
- api 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
+ testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
testImplementation project(':plugins:sonar-xoo-plugin')
+ testImplementation 'org.wiremock:wiremock-standalone'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-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.sonar.scanner.mediumtest.bootstrap;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import java.io.ByteArrayInputStream;
+import java.nio.file.Path;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.scanner.bootstrap.ScannerMain;
+import org.sonarqube.ws.Ce;
+import org.sonarqube.ws.Qualityprofiles;
+import org.sonarqube.ws.Rules;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static testutils.TestUtils.protobufBody;
+
+class BootstrapMediumIT {
+
+ public static final String PROJECT_KEY = "my-project";
+ public static final String QPROFILE_KEY = "profile123";
+ @RegisterExtension
+ LogTesterJUnit5 logTester = new LogTesterJUnit5();
+
+ @RegisterExtension
+ static WireMockExtension sonarqube = WireMockExtension.newInstance()
+ .options(wireMockConfig().dynamicPort())
+ .build();
+
+ @BeforeEach
+ void mockBareMinimalServerEndpoints() {
+ sonarqube.stubFor(get("/api/plugins/installed")
+ .willReturn(okJson("{\n"
+ + " \"plugins\": []\n"
+ + "}")));
+
+ sonarqube.stubFor(get("/api/qualityprofiles/search.protobuf?project=" + PROJECT_KEY)
+ .willReturn(aResponse()
+ .withResponseBody(protobufBody(Qualityprofiles.SearchWsResponse.newBuilder()
+ .addProfiles(Qualityprofiles.SearchWsResponse.QualityProfile.newBuilder()
+ .setKey(QPROFILE_KEY)
+ .setName("My Profile")
+ .setRulesUpdatedAt("2021-01-01T00:00:00+0000")
+ .build())
+ .build()))));
+
+ sonarqube.stubFor(get("/api/rules/list.protobuf?qprofile=" + QPROFILE_KEY + "&ps=500&p=1")
+ .willReturn(aResponse()
+ .withResponseBody(protobufBody(Rules.ListResponse.newBuilder()
+ .build()))));
+
+ sonarqube.stubFor(get("/api/languages/list")
+ .willReturn(okJson("{\n"
+ + " \"languages\": []\n"
+ + "}")));
+
+ sonarqube.stubFor(get("/api/metrics/search?ps=500&p=1")
+ .willReturn(okJson("{\n"
+ + " \"metrics\": [],\n"
+ + " \"total\": 0,\n"
+ + " \"p\": 1,\n"
+ + " \"ps\": 100"
+ + "}")));
+
+ sonarqube.stubFor(post("/api/ce/submit?projectKey=" + PROJECT_KEY)
+ .willReturn(aResponse()
+ .withResponseBody(protobufBody(Ce.SubmitResponse.newBuilder()
+ .build()))));
+ }
+
+ @Test
+ void should_fail_if_invalid_json_input() {
+ var in = new ByteArrayInputStream("}".getBytes());
+ var e = assertThrows(IllegalArgumentException.class, () -> ScannerMain.run(in));
+ assertThat(e).hasMessage("Failed to parse JSON input");
+
+ assertThat(logTester.logs()).contains("Starting SonarScanner Engine...");
+ }
+
+ @Test
+ void should_warn_if_null_property_key() {
+ try {
+ ScannerMain.run(new ByteArrayInputStream("{\"scannerProperties\": [{\"value\": \"aValueWithoutKey\"}]}".getBytes()));
+ } catch (Exception ignored) {
+ }
+ assertThat(logTester.logs()).contains("Ignoring property with null key: 'aValueWithoutKey'");
+ }
+
+ @Test
+ void should_warn_if_duplicate_property_keys() {
+ try {
+ ScannerMain.run(new ByteArrayInputStream("{\"scannerProperties\": [{\"key\": \"aKey\"}, {\"key\": \"aKey\"}]}".getBytes()));
+ } catch (Exception ignored) {
+ }
+ assertThat(logTester.logs()).contains("Duplicated properties with key: 'aKey'");
+ }
+
+ @Test
+ void should_warn_if_null_property() {
+ try {
+ ScannerMain.run(new ByteArrayInputStream("{\"scannerProperties\": [{\"key\": \"aKey\", \"value\": \"aValue\"},]}".getBytes()));
+ } catch (Exception ignored) {
+ }
+ assertThat(logTester.logs()).contains("Ignoring null property");
+ }
+
+ /**
+ * For now this test is just checking that the scanner completes successfully, with no input files, and mocking server responses to the bare minimum.
+ */
+ @Test
+ void should_complete_successfully(@TempDir Path baseDir) {
+
+ ScannerMain.run(new ByteArrayInputStream(("{\"scannerProperties\": ["
+ + "{\"key\": \"sonar.host.url\", \"value\": \"" + sonarqube.baseUrl() + "\"},"
+ + "{\"key\": \"sonar.projectKey\", \"value\": \"" + PROJECT_KEY + "\"},"
+ + "{\"key\": \"sonar.projectBaseDir\", \"value\": \"" + baseDir + "\"}"
+ + "]}").getBytes()));
+
+ assertThat(logTester.logs()).contains("SonarScanner Engine completed successfully");
+ }
+
+ @Test
+ void should_enable_verbose(@TempDir Path baseDir) {
+
+ ScannerMain.run(new ByteArrayInputStream(("{\"scannerProperties\": ["
+ + "{\"key\": \"sonar.host.url\", \"value\": \"" + sonarqube.baseUrl() + "\"},"
+ + "{\"key\": \"sonar.projectKey\", \"value\": \"" + PROJECT_KEY + "\"},"
+ + "{\"key\": \"sonar.projectBaseDir\", \"value\": \"" + baseDir + "\"}"
+ + "]}").getBytes()));
+
+ assertThat(logTester.logs(Level.DEBUG)).isEmpty();
+
+ ScannerMain.run(new ByteArrayInputStream(("{\"scannerProperties\": ["
+ + "{\"key\": \"sonar.host.url\", \"value\": \"" + sonarqube.baseUrl() + "\"},"
+ + "{\"key\": \"sonar.projectKey\", \"value\": \"" + PROJECT_KEY + "\"},"
+ + "{\"key\": \"sonar.projectBaseDir\", \"value\": \"" + baseDir + "\"},"
+ + "{\"key\": \"sonar.verbose\", \"value\": \"true\"}"
+ + "]}").getBytes()));
+
+ assertThat(logTester.logs(Level.DEBUG)).isNotEmpty();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-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 com.github.tomakehurst.wiremock.http.Body;
+import com.github.tomakehurst.wiremock.http.ContentTypeHeader;
+import com.google.protobuf.Message;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class TestUtils {
+
+ public static Body protobufBody(Message message) {
+ var baos = new ByteArrayOutputStream();
+ try {
+ message.writeTo(baos);
+ return Body.ofBinaryOrText(baos.toByteArray(), ContentTypeHeader.absent());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
}
public LoggingConfiguration setVerbose(Map<String, String> props) {
+ verbose = isVerboseEnabled(props);
+ return setVerbose(verbose);
+ }
+
+ public static boolean isVerboseEnabled(Map<String, String> props) {
String logLevel = props.get("sonar.log.level");
String deprecatedProfilingLevel = props.get("sonar.log.profilingLevel");
- verbose = "true".equals(props.get("sonar.verbose")) ||
+ return "true".equals(props.get("sonar.verbose")) ||
"DEBUG".equals(logLevel) || "TRACE".equals(logLevel) ||
"BASIC".equals(deprecatedProfilingLevel) || "FULL".equals(deprecatedProfilingLevel);
-
- return setVerbose(verbose);
}
public LoggingConfiguration setRootLevel(String level) {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-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.sonar.scanner.bootstrap;
+
+import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import ch.qos.logback.core.CoreConstants;
+import ch.qos.logback.core.encoder.EncoderBase;
+import org.apache.commons.text.StringEscapeUtils;
+
+import static ch.qos.logback.core.CoreConstants.COMMA_CHAR;
+import static ch.qos.logback.core.CoreConstants.DOUBLE_QUOTE_CHAR;
+import static ch.qos.logback.core.CoreConstants.UTF_8_CHARSET;
+
+public class ScannerLogbackEncoder extends EncoderBase<ILoggingEvent> {
+
+ private static final byte[] EMPTY_BYTES = new byte[0];
+ private static final char OPEN_OBJ = '{';
+ private static final char CLOSE_OBJ = '}';
+ private static final char VALUE_SEPARATOR = COMMA_CHAR;
+ private static final char QUOTE = DOUBLE_QUOTE_CHAR;
+ private static final String QUOTE_COL = "\":";
+
+ private final ThrowableProxyConverter tpc = new ThrowableProxyConverter();
+
+ @Override
+ public byte[] headerBytes() {
+ return EMPTY_BYTES;
+ }
+
+ @Override
+ public byte[] encode(ILoggingEvent event) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(OPEN_OBJ);
+ var level = event.getLevel();
+ if (level != null) {
+ appenderMember(sb, "level", level.levelStr);
+ sb.append(VALUE_SEPARATOR);
+ }
+
+ appenderMember(sb, "message", StringEscapeUtils.escapeJson(event.getFormattedMessage()));
+
+ IThrowableProxy tp = event.getThrowableProxy();
+ String stackTrace = null;
+ if (tp != null) {
+ sb.append(VALUE_SEPARATOR);
+ stackTrace = tpc.convert(event);
+ appenderMember(sb, "stacktrace", StringEscapeUtils.escapeJson(stackTrace));
+ }
+
+ sb.append(CLOSE_OBJ);
+ sb.append(CoreConstants.JSON_LINE_SEPARATOR);
+ return sb.toString().getBytes(UTF_8_CHARSET);
+ }
+
+ private static void appenderMember(StringBuilder sb, String key, String value) {
+ sb.append(QUOTE).append(key).append(QUOTE_COL).append(QUOTE).append(value).append(QUOTE);
+ }
+
+ @Override
+ public byte[] footerBytes() {
+ return EMPTY_BYTES;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-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.sonar.scanner.bootstrap;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.LoggerFactory;
+import org.sonar.batch.bootstrapper.EnvironmentInformation;
+import org.sonar.batch.bootstrapper.LoggingConfiguration;
+
+import static org.sonar.batch.bootstrapper.LoggingConfiguration.LEVEL_ROOT_DEFAULT;
+import static org.sonar.batch.bootstrapper.LoggingConfiguration.LEVEL_ROOT_VERBOSE;
+
+public class ScannerMain {
+
+ private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ScannerMain.class);
+
+ private static final String SCANNER_APP_KEY = "sonar.scanner.app";
+ private static final String SCANNER_APP_VERSION_KEY = "sonar.scanner.appVersion";
+
+ public static void main(String... args) {
+ try {
+ run(System.in);
+ } catch (Exception e) {
+ LOG.error("Error during SonarScanner Engine execution", e);
+ System.exit(1);
+ }
+ }
+
+ public static void run(InputStream in) {
+ LOG.info("Starting SonarScanner Engine...");
+
+ var properties = parseInputProperties(in);
+
+ configureLogLevel(properties);
+
+ runScannerEngine(properties);
+
+ LOG.info("SonarScanner Engine completed successfully");
+ }
+
+ private static @NotNull Map<String, String> parseInputProperties(InputStream in) {
+ Map<String, String> properties = new HashMap<>();
+ var input = parseJsonInput(in);
+ if (input != null && input.scannerProperties != null) {
+ input.scannerProperties.forEach(prop -> {
+ if (prop == null) {
+ LOG.warn("Ignoring null property");
+ } else if (prop.key == null) {
+ LOG.warn("Ignoring property with null key: '{}'", prop.value);
+ } else {
+ if (properties.containsKey(prop.key)) {
+ LOG.warn("Duplicated properties with key: '{}'", prop.key);
+ }
+ properties.put(prop.key, prop.value);
+ }
+ });
+ }
+ return properties;
+ }
+
+ @CheckForNull
+ private static Input parseJsonInput(InputStream in) {
+ try (var reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
+ return new Gson().fromJson(reader, Input.class);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to parse JSON input", e);
+ }
+ }
+
+ private static void runScannerEngine(Map<String, String> properties) {
+ var scannerAppKey = properties.get(SCANNER_APP_KEY);
+ var scannerAppVersion = properties.get(SCANNER_APP_VERSION_KEY);
+ var env = new EnvironmentInformation(scannerAppKey, scannerAppVersion);
+ SpringGlobalContainer.create(properties, List.of(env)).execute();
+ }
+
+ private static void configureLogLevel(Map<String, String> properties) {
+ var verbose = LoggingConfiguration.isVerboseEnabled(properties);
+ var rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
+ rootLogger.setLevel(Level.toLevel(verbose ? LEVEL_ROOT_VERBOSE : LEVEL_ROOT_DEFAULT));
+ }
+
+ private static class Input {
+ @SerializedName("scannerProperties")
+ private List<ScannerProperty> scannerProperties;
+ }
+
+ private static class ScannerProperty {
+ @SerializedName("key")
+ private String key;
+
+ @SerializedName("value")
+ private String value;
+ }
+
+}
--- /dev/null
+<!-- This file is shared between the old bootstrapping entry point and new bootstrapping -->
+<included>
+ <!-- BeanUtils generate too many DEBUG logs when sonar.verbose is set -->
+ <logger name="org.apache.commons.beanutils.converters" level="WARN"/>
+
+ <!-- FileSnapshot generate too many DEBUG logs when sonar.verbose is set -->
+ <logger name="org.eclipse.jgit.internal.storage.file" level="INFO"/>
+
+ <!-- Spring generates too many DEBUG logs when sonar.verbose is set -->
+ <logger name="org.springframework" level="INFO"/>
+
+ <!-- AbstractApplicationContext generate too verbose warning if warn is Enabled -->
+ <logger name="org.springframework.context.annotation.AnnotationConfigApplicationContext" level="ERROR"/>
+
+ <logger name="org.sonar.core.platform.PriorityBeanFactory" level="INFO"/>
+</included>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration>
+<!-- This logback configuration is used when the scanner engine is bootstrapped using the SonarScannerCli class. -->
+
+<configuration>
+ <import class="ch.qos.logback.core.ConsoleAppender"/>
+ <include resource="logback-shared.xml"/>
+
+ <appender name="STDOUT" class="ConsoleAppender">
+ <encoder class="org.sonar.scanner.bootstrap.ScannerLogbackEncoder"/>
+ </appender>
+
+ <root level="info">
+ <appender-ref ref="STDOUT"/>
+ </root>
+</configuration>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" ?>
<configuration debug="false">
+ <include resource="logback-shared.xml"/>
<!--
</encoder>
</appender>
- <!-- BeanUtils generate too many DEBUG logs when sonar.verbose is set -->
- <logger name="org.apache.commons.beanutils.converters" level="WARN"/>
-
- <!-- FileSnapshot generate too many DEBUG logs when sonar.verbose is set -->
- <logger name="org.eclipse.jgit.internal.storage.file" level="INFO"/>
-
- <!-- Spring generates too many DEBUG logs when sonar.verbose is set -->
- <logger name="org.springframework" level="INFO"/>
-
- <!-- AbstractApplicationContext generate too verbose warning if warn is Enabled -->
- <logger name="org.springframework.context.annotation.AnnotationConfigApplicationContext" level="ERROR"/>
-
- <logger name="org.sonar.core.platform.PriorityBeanFactory" level="INFO"/>
-
- <!-- sonar.showSql -->
- <!-- see also org.sonar.db.MyBatis#configureLogback() -->
- <logger name="org.mybatis" level="${SQL_LOGGER_LEVEL:-WARN}"/>
- <logger name="org.apache.ibatis" level="${SQL_LOGGER_LEVEL:-WARN}"/>
- <logger name="java.sql" level="${SQL_LOGGER_LEVEL:-WARN}"/>
- <logger name="java.sql.ResultSet" level="WARN"/>
-
<root level="${ROOT_LOGGER_LEVEL}">
<!-- sonar.verbose -->
<appender-ref ref="STDOUT"/>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-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.sonar.scanner.bootstrap;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.ThrowableProxy;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class ScannerLogbackEncoderTest {
+
+ ScannerLogbackEncoder underTest = new ScannerLogbackEncoder();
+
+ @Test
+ void no_headers_and_footers() {
+ assertThat(underTest.headerBytes()).isEmpty();
+ assertThat(underTest.footerBytes()).isEmpty();
+ }
+
+ @Test
+ void should_encode_when_no_level_and_no_stacktrace() {
+ var logEvent = mock(ILoggingEvent.class);
+ when(logEvent.getLevel()).thenReturn(null);
+ when(logEvent.getFormattedMessage()).thenReturn("message");
+
+ var bytes = underTest.encode(logEvent);
+
+ assertThat(new String(bytes, StandardCharsets.UTF_8)).isEqualTo("{\"message\":\"message\"}\n");
+ }
+
+ @Test
+ void should_encode_when_no_stacktrace() {
+ var logEvent = mock(ILoggingEvent.class);
+ when(logEvent.getLevel()).thenReturn(Level.DEBUG);
+ when(logEvent.getFormattedMessage()).thenReturn("message");
+
+ var bytes = underTest.encode(logEvent);
+
+ assertThat(new String(bytes, StandardCharsets.UTF_8)).isEqualTo("{\"level\":\"DEBUG\",\"message\":\"message\"}\n");
+ }
+
+ @Test
+ void should_encode_with_stacktrace() {
+ var logEvent = mock(ILoggingEvent.class);
+ when(logEvent.getLevel()).thenReturn(Level.DEBUG);
+ when(logEvent.getFormattedMessage()).thenReturn("message");
+ when(logEvent.getThrowableProxy()).thenReturn(new ThrowableProxy(new IllegalArgumentException("foo")));
+
+ var bytes = underTest.encode(logEvent);
+
+ assertThat(new String(bytes, StandardCharsets.UTF_8)).isEqualTo("{\"level\":\"DEBUG\",\"message\":\"message\",\"stacktrace\":\"java.lang.IllegalArgumentException: foo\\n\"}\n");
+ }
+
+}
\ No newline at end of file