ConverterTest.java

/*
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v2.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.pattern;

import ch.qos.logback.classic.*;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.util.LogbackMDCAdapter;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.net.SyslogConstants;
import ch.qos.logback.core.pattern.DynamicConverter;
import ch.qos.logback.core.pattern.FormatInfo;
import ch.qos.logback.core.util.EnvUtil;
import ch.qos.logback.core.util.StatusPrinter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.MarkerFactory;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import static org.junit.jupiter.api.Assertions.*;

public class ConverterTest {

    LoggerContext loggerContext = new LoggerContext();
    LogbackMDCAdapter logbackMDCAdapter = new LogbackMDCAdapter();
    Logger logger = loggerContext.getLogger(ConverterTest.class);
    LoggingEvent le;
    //List<String> optionList = new ArrayList<String>();

    // The LoggingEvent is massaged with an FCQN of FormattingConverter. This
    // forces the returned caller information to match the caller stack for
    // this particular test.
    LoggingEvent makeLoggingEvent(Exception ex) {
        return new LoggingEvent(ch.qos.logback.core.pattern.FormattingConverter.class.getName(), logger, Level.INFO,
                "Some message", ex, null);
    }

    Exception getException(String msg, Exception cause) {
        return new Exception(msg, cause);
    }

    @BeforeEach
    public void setUp() throws Exception {
        loggerContext.setMDCAdapter(logbackMDCAdapter);
        Exception rootEx = getException("Innermost", null);
        Exception nestedEx = getException("Nested", rootEx);

        Exception ex = new Exception("Bogus exception", nestedEx);

        le = makeLoggingEvent(ex);
    }

    @Test
    public void testLineOfCaller() {
        {
            DynamicConverter<ILoggingEvent> converter = new LineOfCallerConverter();
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            // the number below should be the line number of the previous line
            assertEquals("73", buf.toString());
        }
    }

    @Test
    public void testLevel() {
        {
            DynamicConverter<ILoggingEvent> converter = new LevelConverter();
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            assertEquals("INFO", buf.toString());
        }
        {
            DynamicConverter<ILoggingEvent> converter = new LevelConverter();
            converter.setFormattingInfo(new FormatInfo(1, 1, true, false));
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            assertEquals("I", buf.toString());
        }
    }

    @Test
    public void testThread() {
        DynamicConverter<ILoggingEvent> converter = new ThreadConverter();
        StringBuilder buf = new StringBuilder();
        converter.write(buf, le);
        System.out.println(buf.toString());
        String regex = ClassicTestConstants.NAKED_MAIN_REGEX;
        assertTrue(buf.toString().matches(regex));
    }

    @Test
    public void testMessage() {
        DynamicConverter<ILoggingEvent> converter = new MessageConverter();
        StringBuilder buf = new StringBuilder();
        converter.write(buf, le);
        assertEquals("Some message", buf.toString());
    }

    @Test
    public void testLineSeparator() {
        DynamicConverter<ILoggingEvent> converter = new LineSeparatorConverter();
        StringBuilder buf = new StringBuilder();
        converter.write(buf, le);
        assertEquals(CoreConstants.LINE_SEPARATOR, buf.toString());
    }

    @Test
    public void testException() {
        {
            DynamicConverter<ILoggingEvent> converter = new ThrowableProxyConverter();
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
        }

        {
            DynamicConverter<ILoggingEvent> converter = new ThrowableProxyConverter();
            converter.setOptionList(List.of("3"));
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
        }
    }

    @Test
    public void testLogger() {
        {
            ClassicConverter converter = new LoggerConverter();
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            assertEquals(this.getClass().getName(), buf.toString());
        }

        {
            ClassicConverter converter = new LoggerConverter();
            converter.setOptionList(List.of("20"));
            converter.start();
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            assertEquals("c.q.l.c.p.ConverterTest", buf.toString());
        }

        {
            DynamicConverter<ILoggingEvent> converter = new LoggerConverter();
            converter.setOptionList(List.of("0"));
            converter.start();
            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            assertEquals("ConverterTest", buf.toString());
        }
    }

    @Test
    public void testVeryLongLoggerName() {
        ClassicConverter converter = new LoggerConverter();
        converter.setOptionList(List.of("5"));
        converter.start();
        StringBuilder buf = new StringBuilder();

        char c = 'a';
        int extraParts = 3;
        int totalParts = ClassicConstants.MAX_DOTS + extraParts;
        StringBuilder loggerNameBuf = new StringBuilder();
        StringBuilder witness = new StringBuilder();

        for (int i = 0; i < totalParts; i++) {
            loggerNameBuf.append(c).append(c).append(c);
            witness.append(c);
            loggerNameBuf.append('.');
            witness.append('.');
        }
        loggerNameBuf.append("zzzzzz");
        witness.append("zzzzzz");

        le.setLoggerName(loggerNameBuf.toString());
        converter.write(buf, le);
        assertEquals(witness.toString(), buf.toString());
    }

    @Test
    public void testClass() {
        DynamicConverter<ILoggingEvent> converter = new ClassOfCallerConverter();
        StringBuilder buf = new StringBuilder();
        converter.write(buf, le);
        assertEquals(this.getClass().getName(), buf.toString());
    }

    @Test
    public void testMethodOfCaller() {
        DynamicConverter<ILoggingEvent> converter = new MethodOfCallerConverter();
        StringBuilder buf = new StringBuilder();
        converter.write(buf, le);
        assertEquals("testMethodOfCaller", buf.toString());
    }

    @Test
    public void testFileOfCaller() {
        DynamicConverter<ILoggingEvent> converter = new FileOfCallerConverter();
        StringBuilder buf = new StringBuilder();
        converter.write(buf, le);
        assertEquals("ConverterTest.java", buf.toString());
    }

    @Test
    public void testCallerData() {
        {
            DynamicConverter<ILoggingEvent> converter = new CallerDataConverter();
            converter.start();

            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            if (buf.length() < 10) {
                fail("buf is too short");
            }
        }

        {
            DynamicConverter<ILoggingEvent> converter = new CallerDataConverter();
            converter.setOptionList(List.of("2", "XXX"));
            converter.start();

            StringBuilder buf = new StringBuilder();
            LoggingEvent event = makeLoggingEvent(null);
            event.addMarker(MarkerFactory.getMarker("XXX"));
            converter.write(buf, event);
            if (buf.length() < 10) {
                fail("buf is too short");
            }
        }

        {
            DynamicConverter<ILoggingEvent> converter = new CallerDataConverter();
            converter.setOptionList(List.of("2", "XXX", "*"));
            converter.start();

            StringBuilder buf = new StringBuilder();
            LoggingEvent event = makeLoggingEvent(null);
            event.addMarker(MarkerFactory.getMarker("YYY"));
            converter.write(buf, event);
            if (buf.length() < 10) {
                fail("buf is too short");
            }
        }
        {
            DynamicConverter<ILoggingEvent> converter = new CallerDataConverter();
            converter.setOptionList(List.of("2", "XXX", "*"));
            converter.start();

            StringBuilder buf = new StringBuilder();
            LoggingEvent event = makeLoggingEvent(null);
            event.addMarker(MarkerFactory.getMarker("YYY"));
            converter.write(buf, event);
            if (buf.length() < 10) {
                fail("buf is too short");
            }
        }

        {
            DynamicConverter<ILoggingEvent> converter = new CallerDataConverter();
            converter.setOptionList(List.of("2", "XXX", "*"));
            converter.start();

            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            if (buf.length() < 10) {
                fail("buf is too short");
            }
            // System.out.println(buf);
        }

        {
            DynamicConverter<ILoggingEvent> converter = new CallerDataConverter();

            boolean jdk18 = EnvUtil.isJDK18OrHigher();
            // jdk 18EA creates a different stack trace
            converter.setOptionList(jdk18 ? List.of("2..3") : List.of("4..5"));
            converter.start();

            StringBuilder buf = new StringBuilder();
            converter.write(buf, le);
            assertTrue( buf.length() >= 10, "buf is too short");

            String expectedRegex = "Caller\\+4";
            if(jdk18) {
                expectedRegex = "Caller\\+2";
            }
            expectedRegex+="\t at (java.base\\/)?java.lang.reflect.Method.invoke.*$";
            String actual = buf.toString();
            assertTrue( Pattern.compile(expectedRegex).matcher(actual).find(), "actual: " + actual);

        }
    }

    @Test
    public void testRelativeTime() throws Exception {
        DynamicConverter<ILoggingEvent> converter = new RelativeTimeConverter();
        StringBuilder buf0 = new StringBuilder();
        StringBuilder buf1 = new StringBuilder();
        long timestamp = System.currentTimeMillis();
        LoggingEvent e0 = makeLoggingEvent(null);
        e0.setTimeStamp(timestamp);
        LoggingEvent e1 = makeLoggingEvent(null);
        e1.setTimeStamp(timestamp);
        converter.write(buf0, e0);
        converter.write(buf1, e1);
        assertEquals(buf0.toString(), buf1.toString());
    }

    @Test
    public void testSyslogStart() throws Exception {
        DynamicConverter<ILoggingEvent> converter = new SyslogStartConverter();
        converter.setOptionList(List.of("MAIL"));
        converter.start();

        ILoggingEvent event = makeLoggingEvent(null);

        StringBuilder buf = new StringBuilder();
        converter.write(buf, event);

        String expected = "<" + (SyslogConstants.LOG_MAIL + SyslogConstants.INFO_SEVERITY) + ">";
        assertTrue(buf.toString().startsWith(expected));
    }

    @Test
    public void testMDCConverter() throws Exception {
        logbackMDCAdapter.clear();
        logbackMDCAdapter.put("someKey", "someValue");
        MDCConverter converter = new MDCConverter();
        converter.setOptionList(List.of("someKey"));
        converter.start();

        ILoggingEvent event = makeLoggingEvent(null);

        String result = converter.convert(event);
        assertEquals("someValue", result);
    }

    @Test
    public void contextNameConverter() {
        ClassicConverter converter = new ContextNameConverter();
        // see http://jira.qos.ch/browse/LBCLASSIC-149
        LoggerContext lcOther = new LoggerContext();
        lcOther.setName("another");
        converter.setContext(lcOther);

        loggerContext.setName("aValue");
        ILoggingEvent event = makeLoggingEvent(null);

        String result = converter.convert(event);
        assertEquals("aValue", result);
    }

    @Test
    public void contextProperty() {
        PropertyConverter converter = new PropertyConverter();
        converter.setContext(loggerContext);
        converter.setOptionList(List.of("k"));
        converter.start();
        loggerContext.setName("aValue");
        loggerContext.putProperty("k", "v");
        ILoggingEvent event = makeLoggingEvent(null);

        String result = converter.convert(event);
        assertEquals("v", result);
    }
    
    @Test
    public void testSequenceNumber() {
        //lc.setSequenceNumberGenerator(new BasicSequenceNumberGenerator());
        SequenceNumberConverter converter = new SequenceNumberConverter();
        converter.setContext(loggerContext);
        converter.start();

        assertTrue(converter.isStarted());
        LoggingEvent event = makeLoggingEvent(null);

        event.setSequenceNumber(123);
        assertEquals("123", converter.convert(event));
        StatusPrinter.print(loggerContext);
    }

    @Test
    void dateConverterTest() {
        // 2024-08-14T1Z:29:25,956 GMT
        long millis = 1_723_649_365_956L;
        dateConverterChecker(millis, List.of("STRICT", "GMT"), "2024-08-14T15:29:25,956");
        dateConverterChecker(millis, List.of("ISO8601", "GMT"), "2024-08-14 15:29:25,956");
        dateConverterChecker(millis, List.of("ISO8601", "UTC"), "2024-08-14 15:29:25,956");
        dateConverterChecker(millis, List.of("yyyy-MM-EE", "UTC", "fr-CH"), "2024-08-mer.");

    }

    void dateConverterChecker(long millis, List<String> options, String expected) {
        DateConverter dateConverter = new DateConverter();
        dateConverter.setOptionList(options) ;
        dateConverter.setContext(loggerContext);
        dateConverter.start();

        assertTrue(dateConverter.isStarted());
        LoggingEvent event = makeLoggingEvent(null);

        Instant now = Instant.ofEpochMilli(millis);
        event.setInstant(now);
        String result = dateConverter.convert(event);
        assertEquals(expected, result);
    }
}