#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

%require-types
%enable-all-warnings
%new-style
%strict-args

%requires ../../../../qlib/Util.qm
%requires ../../../../qlib/QUnit.qm
%requires ../../../../qlib/Logger.qm

%exec-class Test

class LoggerLayoutPattern2 inherits LoggerLayoutPattern {
    public list getParsedPattern() {
        return parsedPattern ?? ();
    }
}

class Test inherits QUnit::Test {
    private {
        Counter m_counter();
        const UNIQUE_NAME = "qore_logger_test_765489wqhkofl";
    }
    public {
    }

    constructor() : QUnit::Test("Logger", "1.0", \ARGV) {
        addTestCase("LoggerLevel", \testLoggerLevel());
        addTestCase("LoggerEvent", \testLoggerEvent());
        addTestCase("LoggerLayout", \testLoggerLayout());
        addTestCase("LoggerFilter", \testLoggerFilter());
        addTestCase("LoggerAppender", \testLoggerAppender());
        addTestCase("LoggerAppenderQueue", \testLoggerAppenderQueue());
        addTestCase("LoggerAppenderMultiThread", \testLoggerAppenderMultiThread());
        addTestCase("LoggerAppenderMultiThreadUnassigned", \testLoggerAppenderMultiThreadUnassigned());
        addTestCase("LoggerAppenderFile", \testLoggerAppenderFile());
        addTestCase("LoggerAppenderStdErr", \testLoggerAppenderStdErr());
        addTestCase("LoggerAppenderFileRotate", \testLoggerAppenderFileRotate());
        addTestCase("LoggerAppenderFileRing", \testLoggerAppenderFileRing());
        addTestCase("LoggerAppenderFileArchive", \testLoggerAppenderFileArchive());
        addTestCase("Logger", \testLogger());
        set_return_value(main());
    }

    testLoggerLevel() {
        hash lls = (
            'ALL':   LoggerLevel::ALL,
            'TRACE': LoggerLevel::TRACE,
            'DEBUG': LoggerLevel::DEBUG,
            'INFO':  LoggerLevel::INFO,
            'WARN':  LoggerLevel::WARN,
            'ERROR': LoggerLevel::ERROR,
            'FATAL': LoggerLevel::FATAL,
            'OFF':   LoggerLevel::OFF,
        );
        assertEq(LoggerLevel::getLevelAll().getStr(), 'ALL');
        assertEq(LoggerLevel::getLevelTrace().getStr(), 'TRACE');
        assertEq(LoggerLevel::getLevelDebug().getStr(), 'DEBUG');
        assertEq(LoggerLevel::getLevelInfo().getStr(), 'INFO');
        assertEq(LoggerLevel::getLevelWarn().getStr(), 'WARN');
        assertEq(LoggerLevel::getLevelError().getStr(), 'ERROR');
        assertEq(LoggerLevel::getLevelFatal().getStr(), 'FATAL');
        assertEq(LoggerLevel::getLevelOff().getStr(), 'OFF');
        LoggerLevel l1 = LoggerLevel::getLevelFatal();
        LoggerLevel l2 = LoggerLevel::getLevelError();
        assertTrue(l1.isGreaterOrEqual(l2));
        assertTrue(l1.isEqual(LoggerLevel::getLevelFatal()));
        assertTrue(l1.isEqual(l1));
        assertFalse(l1.isEqual(l2));

        foreach string s in (keys lls) {
            LoggerLevel ll1 = LoggerLevel::getLevel(s);
            assertEq(ll1.getValue(), lls{s});
            assertEq(ll1.getStr(), s);
            LoggerLevel ll2 = LoggerLevel::getLevel(lls{s});
            assertEq(ll2.getValue(), lls{s});
            assertEq(ll2.getStr(), s);
            assertTrue(ll1.isEqual(ll2));
            assertTrue(ll1.isGreaterOrEqual(ll2));
        }

    }

    testLoggerEvent() {
        LoggerEvent lle;
        date ts = LoggerEvent::getStartTime() + 1D;
        lle = new LoggerEvent('fqcn', 'ctg', LoggerLevel::getLevelError(), 'test: %d, %d', (1, 2), NOTHING, gettid(), ts, NOTHING);
        assertEq(lle.getFullQualifiedClassname(), 'fqcn');
        assertEq(lle.getCategoryName(), 'ctg');
        assertTrue(lle.getLevel().isEqual(LoggerLevel::getLevelError()));
        assertEq('test: 1, 2', lle.getMessage());
        assertEq(lle.getThreadId(), gettid());
        assertEq(lle.getTimeStamp(), ts);
        # get the duration to the same time on the next day; due to DST changes we cannot just add "1D"
        # as if we do, the test will fail twice a year, when the difference to the same time is 23 hours
        # or 25 hours
        date time_to_tomorrow = now();
        time_to_tomorrow = time_to_tomorrow + 1D - time_to_tomorrow;
        assertEq(lle.getRelativeTime(), time_to_tomorrow);

        Logger l('testLogger');
        lle = new LoggerEvent(l, LoggerLevel::getLevelError(), 'test: %d, %y, %d', (1, new LoggerEventParameter(\sprintf(), '%s:%s:%d:%y', 'A', 'B', 99, (3, 4)), 2));
        assertEq(lle.getFullQualifiedClassname(), get_class_name(l));
        assertEq(lle.getCategoryName(), 'testLogger');
        assertEq(lle.getLogger(), l);
        int id = lle.getUniqueId();
        assertTrue(id > 0);
        assertEq(id, lle.getUniqueId());
        assertEq('test: 1, "A:B:99:[3, 4]", 2', lle.getMessage());
    }

    testLoggerLayout() {
        LoggerLayoutPattern2 llp("");
        hash<CallStackInfo> csi();
        csi.file = "testfile";
        csi.source = "testsource";
        csi.line = 987;
        csi.function = "myfunction";
        hash<ExceptionInfo> ei();
        try {
            throw "TEST-ERROR", "Test exception";
        } catch (hash<ExceptionInfo> ex) {
            ei = ex;
        }
        LoggerEvent::setStartTime(2000-01-01T00:00:00);
        date ts = LoggerEvent::getStartTime() + 1D;
        LoggerEvent lle = new LoggerEvent('Logger', 'mylogger', LoggerLevel::getLevelError(), 'ABCDEF', (), csi, 123, ts, ei);
        hash patterns = (
            "": (
                "parsed": (),
                "output": "",
            ),
            "ABC": (
                "parsed": ("ABC",),
                "output": "ABC",
            ),
            "%%": (
                "parsed": ("%",),
                "output": "%",
            ),
            "%%%%%%": (
                "parsed": ("%%%",),
                "output": "%%%",
            ),
            "ABC%%": (
                "parsed": ("ABC%",),
                "output": "ABC%",
            ),
            "%%ABC": (
                "parsed": ("%ABC",),
                "output": "%ABC",
            ),
            "ABC%%D": (
                "parsed": ("ABC%D",),
                "output": "ABC%D",
            ),
            "%m": (
                "parsed": (("key": "m"),),
                "output": "ABCDEF",
            ),
            "%mGH": (
                "parsed": (("key": "mGH"),),
                "output": "ABCDEFGH",
            ),
            "%mGH{}": (
                "parsed": ((
                    "key": "mGH",
                    "option": "",
                ),),
                "error": "Unknown pattern token \"mGH\"",
            ),
            "%ZGH": (
                "parsed": (("key": "ZGH"),),
                "error": "Unknown pattern token \"ZGH\"",
            ),
            "%mmmm%m": (
                "parsed": (
                    ("key": "mmmm"),
                    ("key": "m"),
                ),
                "output": "ABCDEFmmmABCDEF",
            ),
            "%Z": (
                "parsed": (("key": "Z"),),
                "error": "Unknown pattern token \"Z\"",
            ),
            "%m{%DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "option": "%DUMMY",
                ),),
                "output": "ABCDEF",
            ),
            "%5m{DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "minWidth": 5,
                    "leftJustify": False,
                    "option": "DUMMY",
                ),),
                "output": "ABCDEF",
            ),
            "%10m{DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "minWidth": 10,
                    "leftJustify": False,
                    "option": "DUMMY",
                ),),
                "output": "    ABCDEF",
            ),
            "%-10m{DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "minWidth": 10,
                    "leftJustify": True,
                    "option": "DUMMY",
                ),),
                "output": "ABCDEF    ",
            ),
            "%.3m{DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "maxWidth": 3,
                    "option": "DUMMY",
                ),),
                "output": "ABC",
            ),
            "%.10m{DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "maxWidth": 10,
                    "option": "DUMMY",
                ),),
                "output": "ABCDEF",
            ),
            "%10.10m{DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "minWidth": 10,
                    "maxWidth": 10,
                    "leftJustify": False,
                    "option": "DUMMY",
                ),),
                "output": "    ABCDEF",
            ),
            "%-10.10m{DUMMY}": (
                "parsed": ((
                    "key": "m",
                    "minWidth": 10,
                    "maxWidth": 10,
                    "leftJustify": True,
                    "option": "DUMMY",
                ),),
                "output": "ABCDEF    ",
            ),
            "%%%m%%": (
                "parsed": (
                    "%",
                    (
                        "key": "m",
                    ),
                    "%"
                ),
                "output": "%ABCDEF%",
            ),
            "%d": (
                "parsed": (("key": "d"),),
                "output": format_date(LoggerLayoutPattern::DEFAULT_DATE_FORMAT, lle.getTimeStamp()),
            ),
            "%F:%L %M": (
                "parsed": (
                    ("key": "F"),
                    ":",
                    ("key": "L"),
                    " ",
                    ("key": "M"),
                ),
                "output": csi.file+":"+string(csi.line)+" "+csi.function,
            ),
            "%d{DD.MM.YYYY HH:mm:SS}": (
                "parsed": (("key": "d", "option": "DD.MM.YYYY HH:mm:SS"),),
                "output": format_date("DD.MM.YYYY HH:mm:SS", lle.getTimeStamp()),
            ),

            "%c %c{XXX} %C%n": (
                "parsed": (
                    ("key": "c"),
                    " ",
                    ("key": "c", "option": "XXX"),
                    " ",
                    ("key": "C"),
                    ("key": "n"),
                ),
                "output": "mylogger mylogger Logger" + LoggerLayoutPattern::getLineDelimiter(),
            ),

            "%E{"+ENV.firstKey()+"}|%E{"+ENV.lastKey()+"}": (
                "parsed": (
                    ("key": "E", "option": ENV.firstKey()),
                    "|",
                    ("key": "E", "option": ENV.lastKey()),
                ),
                "output": ENV.firstValue()+"|"+ENV.lastValue(),
            ),

            "%r{%010d} [%t] %p %c - %m%n": (
                "parsed": (
                    ("key": "r", "option": "%010d"),
                    " [",
                    ("key": "t"),
                    "] ",
                    ("key": "p"),
                    " ",
                    ("key": "c"),
                    " - ",
                    ("key": "m"),
                    ("key": "n"),
                ),
                "output": "0086400000 [123] ERROR mylogger - ABCDEF"+LoggerLayoutPattern::getLineDelimiter(),
            ),
        );
        foreach string patt in (keys patterns) {
            llp.setPattern(patt);
            assertEq(patt, llp.getPattern());
            assertEq(patterns{patt}.parsed, llp.getParsedPattern());
            if (exists patterns{patt}.output) {
                assertEq(patterns{patt}.output, llp.format(lle));
            }
            if (exists patterns{patt}.error) {
                assertThrows("LOGGER-ERROR", patterns{patt}.error, \llp.format(), (lle));
            }
        }

        llp.setPattern("%x");
        assertTrue(exists lle.getThrowableInfo());
        assertRegex("TEST-ERROR: Test exception", llp.format(lle));

        llp.setPattern("%u");
        assertTrue(int(llp.format(lle)) > 0);

        hash patternsErr = (
            "%/AAA": "Wrong pattern starting %/AAA",
            "%0-9c": "Wrong pattern option starting %0-9c",
            "%%%": "Wrong pattern starting %",
        );
        foreach string patt in (keys patternsErr) {
            assertThrows("LOGGER-ERROR", patternsErr{patt}, \llp.setPattern(), (patt));
        }
    }

    testLoggerFilter() {
        hash<CallStackInfo> csi();
        csi.file = "testfile";
        csi.source = "testsource";
        csi.line = 987;
        csi.function = "myfunction";
        LoggerEvent lle = new LoggerEvent('Logger', 'mylogger', LoggerLevel::getLevelError(), 'ABCDEF', (), csi, 123);
        LoggerFilterLevel lfl(); # INFO-OFF
        # max > min
        assertEq(LoggerLevel::getLevelInfo(), lfl.getMinLevel());
        assertEq(LoggerLevel::getLevelOff(), lfl.getMaxLevel());
        assertEq(LoggerFilter::NEUTRAL, lfl.eval(lle));
        lfl.setMinLevel(LoggerLevel::getLevelFatal());
        assertEq(LoggerFilter::DENY, lfl.eval(lle));
        lfl.setMinLevel(LoggerLevel::ERROR);
        assertEq(LoggerLevel::getLevelError(), lfl.getMinLevel());
        assertEq(LoggerFilter::NEUTRAL, lfl.eval(lle));
        lfl.setMaxLevel("ERROR");
        assertEq(LoggerLevel::getLevelError(), lfl.getMaxLevel());
        assertEq(LoggerFilter::NEUTRAL, lfl.eval(lle));
        lfl.setMinLevel("DEBUG");
        assertEq(LoggerLevel::getLevelDebug(), lfl.getMinLevel());
        assertEq(LoggerFilter::NEUTRAL, lfl.eval(lle));
        lfl.setMaxLevel(LoggerLevel::INFO);
        assertEq(LoggerLevel::getLevelInfo(), lfl.getMaxLevel());
        assertEq(LoggerFilter::DENY, lfl.eval(lle));
        # max < min
        lfl.setMaxLevel("DEBUG");
        lfl.setMinLevel("ERROR");
        assertEq(LoggerFilter::DENY, lfl.eval(lle));
        lfl.setMinLevel("INFO");
        assertEq(LoggerFilter::NEUTRAL, lfl.eval(lle));
        lfl.setMinLevel("OFF");
        lfl.setMaxLevel("ERROR");
        assertEq(LoggerFilter::DENY, lfl.eval(lle));
        lfl.setMaxLevel("FATAL");
        assertEq(LoggerFilter::NEUTRAL, lfl.eval(lle));

        LoggerFilterRegex lfr("^A", True);
        assertEq("^A", lfr.getRegex());
        assertTrue(lfr.getRegexResult());
        assertEq(LoggerFilter::NEUTRAL, lfr.eval(lle));
        lfr.setRegex("^A", False);
        assertFalse(lfr.getRegexResult());
        assertEq(LoggerFilter::DENY, lfr.eval(lle));
        lfr.setRegex("^B", False);
        assertEq("^B", lfr.getRegex());
        assertFalse(lfr.getRegexResult());
        assertEq(LoggerFilter::NEUTRAL, lfr.eval(lle));
        lfr.setRegex("^B", True);
        assertEq(LoggerFilter::DENY, lfr.eval(lle));

    }

    testLoggerAppender() {
        LoggerLayoutPattern llp("");
        llp.setPattern("%m");
        StringOutputStream sos();
        StreamWriter sw(sos);
        LoggerAppenderStream la("TEST", llp, sw);
        LoggerFilterLevel lfl(LoggerLevel::ERROR);
        LoggerFilterRegex lfr("^A");
        LoggerFilterRegex lfr2("DUMMY");
        LoggerEvent lle = new LoggerEvent('Logger', 'mylogger', LoggerLevel::getLevelError(), 'ABCDEF');
        assertEq("TEST", la.getName());
        assertFalse(la.isOpen());
        assertEq(llp, la.getLayout());
        assertEq(NOTHING, la.getQueue());
        la.open();
        assertTrue(la.isOpen());
        la.post(lle);
        assertEq("ABCDEF", sos.getData());
        # filter list test
        assertEq((), la.getFilters());
        la.addFilter(lfl);
        assertEq((lfl, ), la.getFilters());
        la.removeFilter(lfl);
        assertEq((), la.getFilters());
        la.addFilter(lfl);
        la.addFilter(lfr);
        assertEq((lfl, lfr), la.getFilters());
        assertThrows("LOGGER-ERROR", "Filter already exists in list", \la.addFilter(), (lfr));
        la.addFilter(lfr2, True);
        assertEq((lfr2, lfl, lfr), la.getFilters());
        la.removeFilter(lfl);
        assertEq((lfr2, lfr), la.getFilters());
        la.removeAllFilters();
        assertEq((), la.getFilters());
        la.addFilter(lfl);
        la.addFilter(lfr);
        la.post(lle);
        assertEq("ABCDEF", sos.getData());
        lfr.setRegex("^B");
        la.post(lle);
        assertEq("", sos.getData());
        la.removeFilter(lfr);
        assertEq((lfl, ), la.getFilters());
        la.post(lle);
        assertEq("ABCDEF", sos.getData());
    }

    testLoggerAppenderQueue() {
        StringOutputStream sos();
        StreamWriter sw(sos);
        LoggerAppenderStream la("TEST", new LoggerLayoutPattern("%m"), sw);
        LoggerEvent lle = new LoggerEvent('Logger', 'mylogger', LoggerLevel::getLevelError(), 'ABCDEF');
        assertFalse(la.isOpen());
        assertEq(NOTHING, la.getQueue());
        LoggerAppenderQueue laq = new LoggerAppenderQueue();
        la.setQueue(laq);
        assertEq(laq, la.getQueue());
        la.setQueue(NOTHING);
        assertEq(NOTHING, la.getQueue());
        la.setQueue(laq);
        la.open();
        assertTrue(la.isOpen());
        assertThrows("LOGGER-ERROR", "Appender is opened", \la.setQueue(), (laq,));
        la.post(lle);
        laq.process();
        assertEq("ABCDEF", sos.getData());
        la.close();
        laq.process(); # async close
        assertFalse(la.isOpen());
        la.setQueue(NOTHING);
        assertEq(NOTHING, la.getQueue());
        assertThrows("LOGGER-ERROR", "Cannot reopen stream", \la.open());
    }

    startPostThread(LoggerAppender la) {
        on_exit m_counter.dec();
        try {
            assertTrue(la.post(new LoggerEvent('Logger', 'e', LoggerLevel::getLevelError(), 'ABCDEF')));
        } catch (hash ex) {
            printf("Ex: %y\n", ex);
        }
    }

    startProcessThread(LoggerAppenderStream la) {
        StringOutputStream sos = la.getStreamWriter().getOutputStream();
        try {
            int n = la.getQueue().size();
            sos.reassignThread();
            la.getQueue().process();
            assertEq(n, sos.getData().split(",").size());
        } catch (hash<ExceptionInfo> ex) {
            printf("Ex: %y\n", ex);
        }
        sos.unassignThread();
        m_counter.dec();
    }

    testLoggerAppenderMultiThread() {
        LoggerLayoutPattern llp("");
        llp.setPattern("%m[%t]");
        StringOutputStream sos();
        StreamWriter sw(sos);
        LoggerAppenderStream la("TEST", llp, sw);
        la.setQueue(new LoggerAppenderQueue());
        la.open();
        assertFalse(la.hasAssignThread());

        m_counter.inc();
        int tid = background startPostThread(la);
        m_counter.waitForZero();
        la.getQueue().process();
        assertEq(sprintf("ABCDEF[%d]", tid), sos.getData());

        llp.setPattern("%t,");
        int n = 200;
        list tids = ();
        while (n > 0) {
            m_counter.inc();
            push tids, background startPostThread(la);
            n--;
        }
        while (m_counter.getCount() > 0 || !sos.hasData()) {
            la.getQueue().process(100ms);
        }
        m_counter.waitForZero();
        list l2 = ();
        string ls = sos.getData();

        {
            on_error {
                printf("sos: %s\n", sos);
                printf("l2: %y\n", l2);
            }
            map (push l2, int($1)), (select ls.split(","), $1 != "");
            tids = sort(tids, int sub(int l, int h) {return l<=>h;});
            l2 = sort(l2, int sub(int l, int h) {return l<=>h;});
            assertEq(tids, l2);
        }

        llp.setPattern("%m,");
        n = 200;
        while (n > 0) {
            m_counter.inc();
            push tids, background startPostThread(la);
            n--;
        }
        m_counter.waitForZero();
        m_counter.inc();
        # "external" stream thread manipulation
        sos.unassignThread();
        background startProcessThread(la);
        m_counter.waitForZero();
        sos.reassignThread();  # for close()
    }

    testLoggerAppenderMultiThreadUnassigned() {
        StringOutputStream sos();
        sos.unassignThread();
        assertEq(-1, sos.getThreadId());
        StreamWriter sw(sos);
        LoggerAppenderStream la("TEST", new LoggerLayoutPattern("%m[%t]"), sw);
        la.setQueue(new LoggerAppenderQueue());
        assertTrue(la.hasAssignThread());
        la.open();

        m_counter.inc();
        int tid = background startPostThread(la);
        m_counter.waitForZero();
        la.getQueue().process();

        sos.reassignThread(); # just to get value
        assertEq(sprintf("ABCDEF[%d]", tid), sos.getData());
        sos.unassignThread();
        assertEq(-1, sos.getThreadId());
        la.close();
    }

    testLoggerAppenderFile() {
        string fn = tmp_location() + DirSep + UNIQUE_NAME + ".log";
        unlink(fn);
        LoggerAppenderFile la1("TEST1", new LoggerLayoutPattern("%m"), fn);
        LoggerAppenderFile la("TEST", new LoggerLayoutPattern("%m"), fn);
        LoggerEvent lle1 = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "X");
        LoggerEvent lle = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "TEST");
        assertFalse(la.getFile().isOpen());
        la1.open();
        la.open();
        assertTrue(la.getFile().isOpen());
        assertEq(fn, la.getFile().getFileName());
        la.post(lle);
        la1.post(lle1);
        la.close();
        la1.close();
        assertFalse(la.getFile().isOpen());
        string s = ReadOnlyFile::readTextFile(fn);
        assertEq("TESTX", s);
        la.open();
        assertTrue(la.getFile().isOpen());
        la.post(lle);
        la.close();
        assertFalse(la.getFile().isOpen());
        s = ReadOnlyFile::readTextFile(fn);
        assertEq("TESTXTEST", s);
        unlink(fn);
    }

    testLoggerAppenderStdErr() {
        string fn = tmp_location() + DirSep + UNIQUE_NAME + ".log";
        File f();
        f.open2(fn, O_CREAT|O_WRONLY|O_TRUNC);
        on_exit unlink(fn);

        LoggerAppenderStdErr laso("STDERR", new LoggerLayoutPattern("%m"));
        LoggerEvent lle = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "STDTEST");
        stderr.redirect(f);
        stderr.setPos(0);
        laso.open();
        laso.post(lle);
        laso.close();
        stderr.redirect(stdout);  # TODO: redir back to real stderr, seems it is not possible so redir to stdout to see anything
        f.close();
        assertEq("STDTEST", ReadOnlyFile::readTextFile(fn));
    }

    testLoggerAppenderFileRotate() {
        string fn = DirSep + "%c-%C.log";
        LoggerAppenderFileRotate la("TEST", new LoggerLayoutPattern("%m"), fn, 3);
        assertEq(DirSep + "LoggerAppenderFileRotate-TEST.log", la.getFileName());
        assertEq(3, la.getCount());
        for (my int i = 1; i<=la.getCount(); i++) {
            assertEq(sprintf("%s.%d", la.getFileName(), i), la.getArchiveFileName(i));
        }
        la = new LoggerAppenderFileRotate("TEST", new LoggerLayoutPattern("%m"), fn, 3, "%p%i"+DirSep+"%f.%i.%n");
        for (my int i = 1; i<=la.getCount(); i++) {
            assertEq(sprintf("%s%d%sLoggerAppenderFileRotate-TEST.log.%d.%d", DirSep, i, DirSep, i, la.getCount()), la.getArchiveFileName(i));
        }
        code do_clean = sub (bool del_dir) {
            for (int i=1; i <= la.getCount(); i++) {
                string s = la.getArchiveFileName(i);
                unlink(s);
                if (del_dir && is_dir(dirname(s))) {
                    rmdir(dirname(s));
                }
            }
            unlink(la.getFileName());
        };
        fn = tmp_location() + DirSep + UNIQUE_NAME + ".log";
        la = new LoggerAppenderFileRotate("TEST", new LoggerLayoutPattern("%u"), fn, 3);
        do_clean(False);
        la.open();
        list prev = ();
        for (int i=0; i < 3*la.getCount(); i++) {
            LoggerEvent lle = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "TEST");
            la.post(lle);
            unshift prev, string(lle.getUniqueId());
            la.rotate();
            assertTrue(la.getFile().isOpen());
            for (int j=1; j <= la.getCount(); j++) {
                assertEq(j <= prev.size(), is_file(la.getArchiveFileName(j)));
                if (j <= prev.size()) {
                    string s2 = ReadOnlyFile::readTextFile(la.getArchiveFileName(j));
                    assertEq(prev[j-1], s2);
                }
            }
        }
        do_clean(False);

        # test rotation in subdirectory
        la = new LoggerAppenderFileRotate("TEST", new LoggerLayoutPattern("%u"), fn, 3, "%p"+UNIQUE_NAME+"-%i"+DirSep+"%f");
        do_clean(True);
        la.open();
        prev = ();
        for (int i=0; i < 3*la.getCount(); i++) {
            LoggerEvent lle = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "TEST");
            la.post(lle);
            unshift prev, string(lle.getUniqueId());
            la.rotate();
            assertTrue(la.getFile().isOpen());
            for (int j=1; j <= la.getCount(); j++) {
                string s = la.getArchiveFileName(j);
                assertEq(j <= prev.size(), is_dir(dirname(s)));
                assertEq(j <= prev.size(), is_file(s));
                if (j <= prev.size()) {
                    string s2 = ReadOnlyFile::readTextFile(s);
                    assertEq(prev[j-1], s2);
                }
            }
        }
        do_clean(True);
    }

    testLoggerAppenderFileRing() {
        string fn = "%c-%C-%i{%02d}-%n{%02d}.log";
        LoggerAppenderFileRing la("TEST", new LoggerLayoutPattern("%m"), fn, 50);
        assertEq(50, la.getCount());
        assertEq("LoggerAppenderFileRing-TEST-00-50.log", la.getFileName());

        int ringSize = 3;
        fn = tmp_location() + DirSep + UNIQUE_NAME + ".log.%i";
        string fn2 = fn;
        fn2 =~ s/%i/%d/;
        for (int i = 0; i < ringSize; i++) {
            unlink(sprintf(fn2, i));
        }
        la = new LoggerAppenderFileRing("TEST", new LoggerLayoutPattern("%u"), fn, ringSize);
        assertFalse(la.getFile().isOpen());
        assertEq(sprintf(fn2, 0), la.getFileName());
        la.open();
        assertTrue(la.getFile().isOpen());

        for (int rot = 0; rot < 3 * ringSize; rot++) {
            assertEq(sprintf(fn2, rot % ringSize), la.getFileName());
            string prevFilename = la.getFileName();
            LoggerEvent lle = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "TEST");
            la.post(lle);
            assertEq(rot % ringSize, la.getCurrentIndex());
            la.rotate();
            assertEq((rot+1)% ringSize, la.getCurrentIndex());
            assertEq(sprintf(fn2, (rot+1) % ringSize), la.getFileName());
            assertFalse(prevFilename == la.getFileName());
            assertTrue(la.getFile().isOpen());
            string s = ReadOnlyFile::readTextFile(prevFilename);
            assertEq(string(lle.getUniqueId()), s);
        }
        la.rotate();
        assertEq(sprintf(fn2, 1), la.getFileName());
        sleep(1200ms); # we need unique mtime
        la.post(new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "TEST"));
        la.close();
        assertFalse(la.isOpen());
        assertFalse(la.getFile().isOpen());
        la.open();
        assertEq(sprintf(fn2, 1), la.getFileName()); # check the latest file is found
        la.close();
        for (int i = 0; i < ringSize; i++) {
            unlink(sprintf(fn2, i));
        }
    }

    testLoggerAppenderFileArchive() {
        LoggerAppenderFileArchive la("TEST", new LoggerLayoutPattern("%m"), "%c%s%C.log");
        assertEq("LoggerAppenderFileArchive"+DirSep+"TEST.log", la.getFileName());

        string fn = tmp_location() + DirSep + UNIQUE_NAME + ".log";
        unlink(fn);
        la = new LoggerAppenderFileArchive("TEST", new LoggerLayoutPattern("%m"), fn);
        assertEq(bindex(la.getArchiveFileName()+DirSep, fn), 0);
        assertFalse(la.getFile().isOpen());
        assertEq(fn, la.getFileName());
        la.open();
        assertTrue(la.getFile().isOpen());
        LoggerEvent lle = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "ARCHIVE");
        la.post(lle);
        string afn = la.archive();
        assertTrue(la.getFile().isOpen());
        assertTrue(is_file(afn));
        assertThrows("LOGGER-ERROR", "Target file already exists", \la.archive());
        lle = new LoggerEvent("Logger", "a", LoggerLevel::getLevelError(), "LOG");
        la.post(lle);
        la.close();
        string s = ReadOnlyFile::readTextFile(la.getFileName());
        assertEq("LOG", s);
        s = ReadOnlyFile::readTextFile(afn);
        assertEq("ARCHIVE", s);
        unlink(afn);
        afn = la.archive(); # archive closed appender
        assertFalse(la.getFile().isOpen());
        assertFalse(la.isOpen());
        s = ReadOnlyFile::readTextFile(afn);
        assertEq("LOG", s);
        assertFalse(is_file(la.getFileName()));
        unlink(afn);

        # multi-level subdir
        afn = tmp_location() + DirSep + UNIQUE_NAME + DirSep + UNIQUE_NAME + DirSep + UNIQUE_NAME + ".log";
        unlink(afn);
        rmdir(dirname(afn));
        rmdir(dirname(dirname(afn)));
        assertFalse(is_dir(dirname(dirname(afn))));
        la = new LoggerAppenderFileArchive("TEST", new LoggerLayoutPattern("%m"), fn, "%p"+UNIQUE_NAME+"%s"+UNIQUE_NAME+"%s%f");
        afn = la.getArchiveFileName();
        assertEq(afn, la.getArchiveFileName());
        assertFalse(la.getFile().isOpen());
        la.open();
        la.post(lle);
        la.close();
        afn = la.archive();
        assertFalse(la.getFile().isOpen());
        assertTrue(is_file(afn));
        s = ReadOnlyFile::readTextFile(afn);
        assertEq("LOG", s);
        assertFalse(is_file(la.getFileName()));
        unlink(afn);
        rmdir(dirname(afn));
        rmdir(dirname(dirname(afn)));

        hash errs = (
            "": "Empty target filename",
            "%P": "Source and target path are equal",
            "%pdummy"+DirSep+".."+DirSep+"%f": "Source and target path are equal",
            "%p": "Target is directory",
        );
        foreach string patt in (keys errs) {
            la = new LoggerAppenderFileArchive("TEST", new LoggerLayoutPattern("%m"), fn, patt);
            assertThrows("LOGGER-ERROR", errs{patt}, \la.archive());
        }
    }

    testLogger() {
        Logger l1("TEST");
        LoggerRoot l2(LoggerLevel::getLevelError());
        assertEq("TEST", l1.getName());
        # parent test
        assertEq(NOTHING, l1.getParent());
        l1.setParent(l2);
        assertEq(l2, l1.getParent());
        assertEq(NOTHING, l2.getParent());
        l1.setParent(NOTHING);
        assertEq(NOTHING, l1.getParent());
        l1.setParent(l2);
        assertEq(l2, l1.getParent());

        assertThrows("LOGGER-ERROR", "Circular logger chain", \l1.setParent(), (l1));
        assertEq(l2, l1.getParent());
        assertThrows("LOGGER-ERROR", "Cannot set parent for root", \l2.setParent(), (l1));
        assertThrows("LOGGER-ERROR", "Cannot set null level for root", \l2.setLevel());

        # level test
        assertEq(NOTHING, l1.getLevel(False));
        assertEq(LoggerLevel::getLevelError(), l2.getLevel(False));
        assertEq(LoggerLevel::getLevelError(), l1.getLevel());
        assertEq(True, l1.isEnabledFor(LoggerLevel::ERROR));
        assertEq(True, l1.isErrorEnabled());
        assertEq(False, l1.isEnabledFor(LoggerLevel::getLevelDebug()));
        assertEq(False, l1.isDebugEnabled());
        assertEq(True, l1.isEnabledFor("FATAL"));
        assertEq(True, l1.isFatalEnabled());
        l1.setLevel("FATAL");
        assertEq(LoggerLevel::getLevelFatal(), l1.getLevel());
        l1.setLevel(LoggerLevel::ALL);
        assertEq(LoggerLevel::getLevelAll(), l1.getLevel());
        l1.setLevel(NOTHING);
        assertEq(LoggerLevel::getLevelError(), l1.getLevel());

        l1.setParent(NOTHING);
        assertThrows("LOGGER-ERROR", "Cannot get effective level", \l1.getLevel());
        l1.setParent(l2);

        # additivity test
        assertEq(True, l1.getAdditivity());
        l1.setAdditivity(False);
        assertEq(False, l1.getAdditivity());
        l1.setAdditivity(True);
        assertEq(True, l1.getAdditivity());

        # arrange appenders
        LoggerLayoutPattern llp("%c/%m");
        StringOutputStream sos1();
        StreamWriter sw1(sos1);
        LoggerAppenderStream la1("TEST", llp, sw1);

        StringOutputStream sos2();
        StreamWriter sw2(sos2);
        LoggerAppenderStream la2("ALERT", llp, sw2);
        LoggerFilterLevel lfl(LoggerLevel::FATAL);
        la2.addFilter(lfl);

        StringOutputStream sos3();
        StreamWriter sw3(sos3);
        LoggerAppenderStream la3("ROOT", llp, sw3);

        # appender list test
        assertEq((), l1.getAppenders());
        l1.addAppender(la1);
        assertEq((la1, ), l1.getAppenders());
        l1.removeAppender(la1);
        assertEq((), l1.getAppenders());
        l1.addAppender(la1);
        l1.addAppender(la2);
        assertEq((la1, la2), l1.getAppenders());
        assertThrows("LOGGER-ERROR", "Appender already exists in list", \l1.addAppender(), (la1));
        l1.addAppender(la3);
        assertEq((la1, la2, la3), l1.getAppenders());
        l1.removeAppender(la2);
        assertEq((la1, la3), l1.getAppenders());
        l1.removeAllAppenders();
        assertEq((), l1.getAppenders());

        l1.setAdditivity(True);
        l1.setLevel("DEBUG");
        l1.addAppender(la1);
        l1.addAppender(la2);
        l2.addAppender(la3);
        la1.open();
        la2.open();
        la3.open();
        # sos1 ... DEBUG
        # sos2 ... FATAL
        # sos3 ... ERROR

        l1.error("test");
        assertEq("TEST/test", sos1.getData());
        assertEq("", sos2.getData());
        assertEq("/test", sos3.getData());
        l1.setAdditivity(False);
        l1.log("ERROR", "test");
        assertEq("TEST/test", sos1.getData());
        assertEq("", sos2.getData());
        assertEq("", sos3.getData());
        l1.setAdditivity(True);
        l2.log(LoggerLevel::ERROR, "test");
        assertEq("", sos1.getData());
        assertEq("", sos2.getData());
        assertEq("/test", sos3.getData());

        l1.trace("test");
        assertEq("", sos1.getData());
        assertEq("", sos2.getData());
        assertEq("", sos3.getData());

        l1.log(LoggerLevel::getLevelDebug(), "test(%s, %s)", "my", new LoggerEventParameter(\sprintf(), "%y", (1, 2)));
        assertEq("TEST/test(my, [1, 2])", sos1.getData());
        assertEq("", sos2.getData());
        assertEq("", sos3.getData());

        l1.log(LoggerLevel::FATAL, "test");
        assertEq("TEST/test", sos1.getData());
        assertEq("TEST/test", sos2.getData());
        assertEq("/test", sos3.getData());

        l1.logArgs(LoggerLevel::FATAL, "test-%d", 1);
        assertEq("TEST/test-1", sos1.getData());
        assertEq("TEST/test-1", sos2.getData());
        assertEq("/test-1", sos3.getData());

        l1.logArgs(LoggerLevel::FATAL, "test-%d", ("2", ));
        assertEq("TEST/test-2", sos1.getData());
        assertEq("TEST/test-2", sos2.getData());
        assertEq("/test-2", sos3.getData());

        l1.assertLog(False, "test: %d,%s", 1, "a");
        assertEq("TEST/test: 1,a", sos1.getData());
        assertEq("", sos2.getData());
        assertEq("/test: 1,a", sos3.getData());

        int i = 124;
        l1.debugVar("i", i);
        assertEq("TEST/i: 124", sos1.getData());

        llp.setPattern("%F:%L");
        l1.error("ERROR", "test");
        hash<CallStackInfo> loc = get_thread_call_stack()[0];

        assertEq(sprintf("%s:%d", loc.file, loc.line-1), sos1.getData());

        # inc, dec middle level
        l1.setLevel(Logger::LoggerLevel::DEBUG);
        assertEq(Logger::LoggerLevel::getLevel("DEBUG"), l1.getLevel());
        l1.decLevel();
        assertEq(Logger::LoggerLevel::getLevel("TRACE"), l1.getLevel());
        l1.incLevel();
        assertEq(Logger::LoggerLevel::getLevel("DEBUG"), l1.getLevel());

        # dec minimum level
        l1.setLevel(Logger::LoggerLevel::ALL);
        assertEq(Logger::LoggerLevel::getLevel("ALL"), l1.getLevel());
        l1.decLevel();
        assertEq(Logger::LoggerLevel::getLevel("ALL"), l1.getLevel());
        l1.incLevel();
        assertEq(Logger::LoggerLevel::getLevel("TRACE"), l1.getLevel());

        # inc maximum level
        l1.setLevel(Logger::LoggerLevel::OFF);
        assertEq(Logger::LoggerLevel::getLevel("OFF"), l1.getLevel());
        l1.incLevel();
        assertEq(Logger::LoggerLevel::getLevel("OFF"), l1.getLevel());
        l1.decLevel();
        assertEq(Logger::LoggerLevel::getLevel("FATAL"), l1.getLevel());
    }

}
