RollingFileAppenderTest.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.core.rolling;

import ch.qos.logback.core.util.Duration;
import ch.qos.logback.core.util.FileSize;
import ch.qos.logback.core.util.FileUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import ch.qos.logback.core.Appender;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.ContextBase;
import ch.qos.logback.core.appender.AbstractAppenderTest;
import ch.qos.logback.core.testUtil.DummyEncoder;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.testUtil.CoreTestConstants;
import ch.qos.logback.core.testUtil.RandomUtil;
import ch.qos.logback.core.status.testUtil.StatusChecker;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
//import ch.qos.logback.core.util.StatusPrinter;

public class RollingFileAppenderTest extends AbstractAppenderTest<Object> {

    RollingFileAppender<Object> rfa = new RollingFileAppender<Object>();
    Context context = new ContextBase();

    TimeBasedRollingPolicy<Object> tbrp = new TimeBasedRollingPolicy<Object>();
    int diff = RandomUtil.getPositiveInt();
    String randomOutputDir = CoreTestConstants.OUTPUT_DIR_PREFIX + diff + "/";
    DummyEncoder<Object> encoder;

    @BeforeEach
    public void setUp() throws Exception {
        // noStartTest fails if the context is set in setUp
        // rfa.setContext(context);
        encoder = new DummyEncoder<>();
        rfa.setEncoder(encoder);
        rfa.setName("test");
        tbrp.setContext(context);
        tbrp.setParent(rfa);
    }

    @AfterEach
    public void tearDown() throws Exception {
    }

    @Override
    protected Appender<Object> getAppender() {
        return rfa;
    }

    @Override
    protected Appender<Object> getConfiguredAppender() {
        rfa.setContext(context);
        tbrp.setFileNamePattern(CoreTestConstants.OUTPUT_DIR_PREFIX + "toto-%d.log");
        tbrp.start();
        rfa.setRollingPolicy(tbrp);

        rfa.start();
        return rfa;
    }

    @Test
    public void testPrudentModeLogicalImplications() {
        rfa.setContext(context);
        // prudent mode will force "file" property to be null
        rfa.setFile("some non null value");
        rfa.setAppend(false);
        rfa.setPrudent(true);

        tbrp.setFileNamePattern(CoreTestConstants.OUTPUT_DIR_PREFIX + "toto-%d.log");
        tbrp.start();
        rfa.setRollingPolicy(tbrp);

        rfa.start();

        Assertions.assertTrue(rfa.isAppend());
        Assertions.assertNull(rfa.rawFileProperty());
        Assertions.assertTrue(rfa.isStarted());
    }

    @Test
    public void testPrudentModeLogicalImplicationsOnCompression() {
        rfa.setContext(context);
        rfa.setAppend(false);
        rfa.setPrudent(true);

        tbrp.setFileNamePattern(CoreTestConstants.OUTPUT_DIR_PREFIX + "toto-%d.log.zip");
        tbrp.start();
        rfa.setRollingPolicy(tbrp);

        rfa.start();

        StatusChecker checker = new StatusChecker(context);
        Assertions.assertFalse(rfa.isStarted());
        Assertions.assertEquals(Status.ERROR, checker.getHighestLevel(0));
    }

    @Test
    public void testFilePropertyAfterRollingPolicy() {
        rfa.setContext(context);
        rfa.setRollingPolicy(tbrp);
        rfa.setFile("x");
        // StatusPrinter.print(context);
        StatusChecker statusChecker = new StatusChecker(context.getStatusManager());
        statusChecker.assertContainsMatch(Status.ERROR, "File property must be set before any triggeringPolicy ");
    }

    @Test
    public void testFilePropertyAfterTriggeringPolicy() {
        rfa.setContext(context);
        rfa.setTriggeringPolicy(new SizeBasedTriggeringPolicy<Object>());
        rfa.setFile("x");
        StatusChecker statusChecker = new StatusChecker(context.getStatusManager());
        statusChecker.assertContainsMatch(Status.ERROR, "File property must be set before any triggeringPolicy ");
    }

    @Test
    public void testFileNameWithParenthesis() {
        // if ')' is not escaped, the test throws
        // java.lang.IllegalStateException: FileNamePattern
        // [.../program(x86)/toto-%d.log] does not contain a valid
        // DateToken
        rfa.setContext(context);
        tbrp.setFileNamePattern(randomOutputDir + "program(x86)/toto-%d.log");
        tbrp.start();
        rfa.setRollingPolicy(tbrp);
        rfa.start();
        rfa.doAppend("hello");
    }

    @Test
    public void stopTimeBasedRollingPolicy() {
        rfa.setContext(context);

        tbrp.setFileNamePattern(CoreTestConstants.OUTPUT_DIR_PREFIX + "toto-%d.log.zip");
        tbrp.start();
        rfa.setRollingPolicy(tbrp);
        rfa.start();

        //StatusPrinter.print(context);
        Assertions.assertTrue(tbrp.isStarted());
        Assertions.assertTrue(rfa.isStarted());
        rfa.stop();
        Assertions.assertFalse(rfa.isStarted());
        Assertions.assertFalse(tbrp.isStarted());

    }

    @Test
    public void stopFixedWindowRollingPolicy() {
        rfa.setContext(context);
        rfa.setFile(CoreTestConstants.OUTPUT_DIR_PREFIX + "toto-.log");

        FixedWindowRollingPolicy fwRollingPolicy = new FixedWindowRollingPolicy();
        fwRollingPolicy.setContext(context);
        fwRollingPolicy.setFileNamePattern(CoreTestConstants.OUTPUT_DIR_PREFIX + "toto-%i.log.zip");
        fwRollingPolicy.setParent(rfa);
        fwRollingPolicy.start();
        SizeBasedTriggeringPolicy<Object> sbTriggeringPolicy = new SizeBasedTriggeringPolicy<Object>();
        sbTriggeringPolicy.setContext(context);
        sbTriggeringPolicy.start();

        rfa.setRollingPolicy(fwRollingPolicy);
        rfa.setTriggeringPolicy(sbTriggeringPolicy);

        rfa.start();

        // StatusPrinter.print(context);
        Assertions.assertTrue(fwRollingPolicy.isStarted());
        Assertions.assertTrue(sbTriggeringPolicy.isStarted());
        Assertions.assertTrue(rfa.isStarted());
        rfa.stop();
        Assertions.assertFalse(rfa.isStarted());
        Assertions.assertFalse(fwRollingPolicy.isStarted());
        Assertions.assertFalse(sbTriggeringPolicy.isStarted());

    }

    /**
     * Test for http://jira.qos.ch/browse/LOGBACK-796
     */
    @Test
    public void testFileShouldNotMatchFileNamePattern() {
        rfa.setContext(context);
        rfa.setFile(CoreTestConstants.OUTPUT_DIR_PREFIX + "x-2013-04.log");
        tbrp.setFileNamePattern(CoreTestConstants.OUTPUT_DIR_PREFIX + "x-%d{yyyy-MM}.log");
        tbrp.start();

        rfa.setRollingPolicy(tbrp);
        rfa.start();
        StatusChecker statusChecker = new StatusChecker(context);
        final String msg = "File property collides with fileNamePattern. Aborting.";
        boolean containsMatch = statusChecker.containsMatch(Status.ERROR, msg);
        Assertions.assertTrue(containsMatch, "Missing error: " + msg);
    }

    @Test
    public void collidingTimeformat() {
        rfa.setContext(context);
        rfa.setAppend(false);
        rfa.setPrudent(true);

        tbrp.setFileNamePattern(CoreTestConstants.OUTPUT_DIR_PREFIX + "toto-%d{dd}.log.zip");
        tbrp.start();
        rfa.setRollingPolicy(tbrp);

        rfa.start();

        StatusChecker checker = new StatusChecker(context);
        Assertions.assertFalse(rfa.isStarted());
        Assertions.assertEquals(Status.ERROR, checker.getHighestLevel(0));
        // StatusPrinter.print(context);
        checker.assertContainsMatch("The date format in FileNamePattern will result");
    }

    @Test
    @DisplayName("Checks header and footer are written when the files are rolled")
    public void testHeaderFooterWritten() throws IOException, InterruptedException {

        String folderPrefix = CoreTestConstants.OUTPUT_DIR_PREFIX+diff+"/";
        String namePrefix = folderPrefix+"header-";
        File folderFile = new File(folderPrefix);
        FileUtil.createMissingParentDirectories(folderFile);


        encoder.setFileHeader("HEADER");
        encoder.setFileFooter("FOOTER");
        rfa.setContext(context);
        FixedWindowRollingPolicy fixedWindowRollingPolicy = new FixedWindowRollingPolicy();
        fixedWindowRollingPolicy.setContext(context);
        fixedWindowRollingPolicy.setParent(rfa);
        fixedWindowRollingPolicy.setMaxIndex(3);
        String fileNamePattern = namePrefix + "%i.log";
        fixedWindowRollingPolicy.setFileNamePattern(fileNamePattern);
        rfa.setRollingPolicy(fixedWindowRollingPolicy);
        rfa.setFile(namePrefix+"0.log");
        fixedWindowRollingPolicy.start();
        rfa.setImmediateFlush(true);
        SizeBasedTriggeringPolicy<Object> sbtp = new SizeBasedTriggeringPolicy<>();
        sbtp.setMaxFileSize(new FileSize(10));
        sbtp.setCheckIncrement(Duration.buildByMilliseconds(10));

        rfa.setTriggeringPolicy(sbtp);
        rfa.getTriggeringPolicy().start();
        rfa.start();

        for (int i = 0; i < 100; i++) {
            rfa.doAppend("data" + i);
            File file = new File(namePrefix + fixedWindowRollingPolicy.getMaxIndex() + ".log");
            if (file.exists()) {
                break;
            }
            Thread.sleep(5);
        }
        rfa.stop();

        for (int i = 0; i < fixedWindowRollingPolicy.getMaxIndex(); i++) {
            File file = new File(namePrefix + i + ".log");
            Assertions.assertTrue(file.exists());
            List<String> lines = Files.readAllLines(file.toPath());
            Assertions.assertTrue(lines.size() > 2, "At least 2 lines per file are expected in " + file);
            Assertions.assertEquals("HEADER", lines.get(0));
            Assertions.assertEquals("FOOTER", lines.get(lines.size() - 1));
            Assertions.assertEquals(1, Collections.frequency(lines, "HEADER"));
            Assertions.assertEquals(1, Collections.frequency(lines, "FOOTER"));
        }
    }

}