//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util.log;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Properties;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;

/**
 * Tests for StdErrLog
 */
public class StdErrLogTest
{
    @Before
    public void before()
    {
        Thread.currentThread().setName("tname");
    }

    @Test
    public void testStdErrLogFormat() throws UnsupportedEncodingException
    {
        StdErrLog log = new StdErrLog(LogTest.class.getName(),new Properties());
        StdErrCapture output = new StdErrCapture(log);

        log.info("testing:{},{}","test","format1");
        log.info("testing:{}","test","format2");
        log.info("testing","test","format3");
        log.info("testing:{},{}","test",null);
        log.info("testing {} {}",null,null);
        log.info("testing:{}",null,null);
        log.info("testing",null,null);

        System.err.println(output);
        output.assertContains("INFO:oejul.LogTest:tname: testing:test,format1");
        output.assertContains("INFO:oejul.LogTest:tname: testing:test,format1");
        output.assertContains("INFO:oejul.LogTest:tname: testing:test format2");
        output.assertContains("INFO:oejul.LogTest:tname: testing test format3");
        output.assertContains("INFO:oejul.LogTest:tname: testing:test,null");
        output.assertContains("INFO:oejul.LogTest:tname: testing null null");
        output.assertContains("INFO:oejul.LogTest:tname: testing:null");
        output.assertContains("INFO:oejul.LogTest:tname: testing");
    }

    @Test
    public void testStdErrLogDebug()
    {
        StdErrLog log = new StdErrLog("xxx",new Properties());
        StdErrCapture output = new StdErrCapture(log);

        log.setLevel(StdErrLog.LEVEL_DEBUG);
        log.debug("testing {} {}","test","debug");
        log.info("testing {} {}","test","info");
        log.warn("testing {} {}","test","warn");
        log.setLevel(StdErrLog.LEVEL_INFO);
        log.debug("YOU SHOULD NOT SEE THIS!",null,null);

        // Test for backward compat with old (now deprecated) method
        Logger before = log.getLogger("before");
        log.setDebugEnabled(true);
        Logger after = log.getLogger("after");
        before.debug("testing {} {}","test","debug-before");
        log.debug("testing {} {}","test","debug-deprecated");
        after.debug("testing {} {}","test","debug-after");

        log.setDebugEnabled(false);
        before.debug("testing {} {}","test","debug-before-false");
        log.debug("testing {} {}","test","debug-deprecated-false");
        after.debug("testing {} {}","test","debug-after-false");

        output.assertContains("DBUG:xxx:tname: testing test debug");
        output.assertContains("INFO:xxx:tname: testing test info");
        output.assertContains("WARN:xxx:tname: testing test warn");
        output.assertNotContains("YOU SHOULD NOT SEE THIS!");
        output.assertContains("DBUG:x.before:tname: testing test debug-before");
        output.assertContains("DBUG:xxx:tname: testing test debug-deprecated");
        output.assertContains("DBUG:x.after:tname: testing test debug-after");
        output.assertNotContains("DBUG:x.before:tname: testing test debug-before-false");
        output.assertNotContains("DBUG:xxx:tname: testing test debug-deprecated-false");
        output.assertNotContains("DBUG:x.after:tname: testing test debug-after-false");
    }

    @Test
    public void testStdErrLogName()
    {
        StdErrLog log = new StdErrLog("test",new Properties());
        log.setPrintLongNames(true);
        StdErrCapture output = new StdErrCapture(log);

        Assert.assertThat("Log.name", log.getName(), is("test"));
        Logger next=log.getLogger("next");
        Assert.assertThat("Log.name(child)", next.getName(), is("test.next"));
        next.info("testing {} {}","next","info");

        output.assertContains(":test.next:tname: testing next info");
    }

    @Test
    public void testStdErrThrowable()
    {
        // Common Throwable (for test)
        Throwable th = new Throwable("Message");

        // Capture raw string form
        StringWriter tout = new StringWriter();
        th.printStackTrace(new PrintWriter(tout));
        String ths = tout.toString();

        // Start test
        StdErrLog log = new StdErrLog("test",new Properties());
        StdErrCapture output = new StdErrCapture(log);

        log.warn("ex",th);
        output.assertContains(ths);

        th = new Throwable("Message with \033 escape");

        log.warn("ex",th);
        output.assertNotContains("Message with \033 escape");
        log.info(th.toString());
        output.assertNotContains("Message with \033 escape");

        log.warn("ex",th);
        output.assertContains("Message with ? escape");
        log.info(th.toString());
        output.assertContains("Message with ? escape");
    }

    /**
     * Test to make sure that using a Null parameter on parameterized messages does not result in a NPE
     */
    @Test
    public void testParameterizedMessage_NullValues() throws NullPointerException
    {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setLevel(StdErrLog.LEVEL_DEBUG);
        log.setHideStacks(true);

        log.info("Testing info(msg,null,null) - {} {}","arg0","arg1");
        log.info("Testing info(msg,null,null) - {} {}",null,null);
        log.info("Testing info(msg,null,null) - {}",null,null);
        log.info("Testing info(msg,null,null)",null,null);
        log.info(null,"Testing","info(null,arg0,arg1)");
        log.info(null,null,null);

        log.debug("Testing debug(msg,null,null) - {} {}","arg0","arg1");
        log.debug("Testing debug(msg,null,null) - {} {}",null,null);
        log.debug("Testing debug(msg,null,null) - {}",null,null);
        log.debug("Testing debug(msg,null,null)",null,null);
        log.debug(null,"Testing","debug(null,arg0,arg1)");
        log.debug(null,null,null);

        log.debug("Testing debug(msg,null)");
        log.debug(null,new Throwable("Testing debug(null,thrw)").fillInStackTrace());

        log.warn("Testing warn(msg,null,null) - {} {}","arg0","arg1");
        log.warn("Testing warn(msg,null,null) - {} {}",null,null);
        log.warn("Testing warn(msg,null,null) - {}",null,null);
        log.warn("Testing warn(msg,null,null)",null,null);
        log.warn(null,"Testing","warn(msg,arg0,arg1)");
        log.warn(null,null,null);

        log.warn("Testing warn(msg,null)");
        log.warn(null,new Throwable("Testing warn(msg,thrw)").fillInStackTrace());
    }

    @Test
    public void testGetLoggingLevel_Default()
    {
        Properties props = new Properties();

        // Default Levels
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,null));
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,""));
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty"));
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,StdErrLogTest.class.getName()));
    }

    @Test
    public void testGetLoggingLevel_Bad()
    {
        Properties props = new Properties();
        props.setProperty("log.LEVEL", "WARN");
        props.setProperty("org.eclipse.jetty.bad.LEVEL","EXPECTED_BAD_LEVEL");

        // Default Level (because of bad level value)
        Assert.assertEquals("Bad Logging Level",StdErrLog.LEVEL_WARN,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.bad"));
    }

    @Test
    public void testGetLoggingLevel_Lowercase()
    {
        Properties props = new Properties();
        props.setProperty("log.LEVEL", "warn");
        props.setProperty("org.eclipse.jetty.util.LEVEL","info");

        // Default Level
        Assert.assertEquals("Lowercase Level",StdErrLog.LEVEL_WARN,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty"));
        // Specific Level
        Assert.assertEquals("Lowercase Level",StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util"));
    }

    @Test
    public void testGetLoggingLevel_Root()
    {
        Properties props = new Properties();
        props.setProperty("log.LEVEL","DEBUG");

        // Default Levels
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,null));
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,""));
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty"));
        Assert.assertEquals("Default Logging Level",StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,StdErrLogTest.class.getName()));
    }

    @Test
    public void testGetLoggingLevel_FQCN()
    {
        String name = StdErrLogTest.class.getName();
        Properties props = new Properties();
        props.setProperty(name + ".LEVEL","ALL");

        // Default Levels
        Assert.assertEquals(StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,null));
        Assert.assertEquals(StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,""));
        Assert.assertEquals(StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty"));

        // Specified Level
        Assert.assertEquals(StdErrLog.LEVEL_ALL,StdErrLog.getLoggingLevel(props,name));
    }

    @Test
    public void testGetLoggingLevel_UtilLevel()
    {
        Properties props = new Properties();
        props.setProperty("org.eclipse.jetty.util.LEVEL","DEBUG");

        // Default Levels
        Assert.assertEquals(StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,null));
        Assert.assertEquals(StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,""));
        Assert.assertEquals(StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty"));
        Assert.assertEquals(StdErrLog.LEVEL_INFO,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.server.BogusObject"));

        // Configured Level
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,StdErrLogTest.class.getName()));
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util.Bogus"));
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util"));
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util.resource.FileResource"));
    }

    @Test
    public void testGetLoggingLevel_MixedLevels()
    {
        Properties props = new Properties();
        props.setProperty("log.LEVEL","DEBUG");
        props.setProperty("org.eclipse.jetty.util.LEVEL","WARN");
        props.setProperty("org.eclipse.jetty.util.ConcurrentHashMap.LEVEL","ALL");

        // Default Levels
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,null));
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,""));
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty"));
        Assert.assertEquals(StdErrLog.LEVEL_DEBUG,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.server.ServerObject"));

        // Configured Level
        Assert.assertEquals(StdErrLog.LEVEL_WARN,StdErrLog.getLoggingLevel(props,StdErrLogTest.class.getName()));
        Assert.assertEquals(StdErrLog.LEVEL_WARN,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util.MagicUtil"));
        Assert.assertEquals(StdErrLog.LEVEL_WARN,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util"));
        Assert.assertEquals(StdErrLog.LEVEL_WARN,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util.resource.FileResource"));
        Assert.assertEquals(StdErrLog.LEVEL_ALL,StdErrLog.getLoggingLevel(props,"org.eclipse.jetty.util.ConcurrentHashMap"));
    }

    /**
     * Tests StdErrLog.warn() methods with level filtering.
     * <p>
     * Should always see WARN level messages, regardless of set level.
     */
    @Test
    public void testWarnFiltering() throws UnsupportedEncodingException
    {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setHideStacks(false);

        StdErrCapture output = new StdErrCapture(log);

        // Start with default level
        log.warn("See Me");

        // Set to debug level
        log.setLevel(StdErrLog.LEVEL_DEBUG);
        log.warn("Hear Me");

        // Set to warn level
        log.setLevel(StdErrLog.LEVEL_WARN);
        log.warn("Cheer Me");

        log.warn("<zoom>", new Throwable("out of focus"));
        log.warn(new Throwable("scene lost"));

        // Validate Output
        // System.err.print(output);
        output.assertContains("See Me");
        output.assertContains("Hear Me");
        output.assertContains("Cheer Me");

        // Validate Stack Traces
        output.assertContains(".StdErrLogTest:tname: <zoom>");
        output.assertContains("java.lang.Throwable: out of focus");
        output.assertContains("java.lang.Throwable: scene lost");
    }

    /**
     * Tests StdErrLog.info() methods with level filtering.
     * <p>
     * Should only see INFO level messages when level is set to {@link StdErrLog#LEVEL_INFO} and below.
     */
    @Test
    public void testInfoFiltering() throws UnsupportedEncodingException
    {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setHideStacks(false);

        StdErrCapture output = new StdErrCapture(log);

        // Normal/Default behavior
        log.info("I will not buy");

        // Level Debug
        log.setLevel(StdErrLog.LEVEL_DEBUG);
        log.info("this record");

        // Level All
        log.setLevel(StdErrLog.LEVEL_ALL);
        log.info("it is scratched.");

        log.info("<zoom>", new Throwable("out of focus"));
        log.info(new Throwable("scene lost"));

        // Level Warn
        log.setLevel(StdErrLog.LEVEL_WARN);
        log.info("sorry?");
        log.info("<spoken line>", new Throwable("on editing room floor"));

        // Validate Output
        output.assertContains("I will not buy");
        output.assertContains("this record");
        output.assertContains("it is scratched.");
        output.assertNotContains("sorry?");

        // Validate Stack Traces
        output.assertNotContains("<spoken line>");
        output.assertNotContains("on editing room floor");

        output.assertContains(".StdErrLogTest:tname: <zoom>");
        output.assertContains("java.lang.Throwable: out of focus");
        output.assertContains("java.lang.Throwable: scene lost");
    }

    /**
     * Tests {@link StdErrLog#LEVEL_OFF} filtering.
     */
    @Test
    public void testOffFiltering() throws UnsupportedEncodingException
    {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setHideStacks(false);
        log.setLevel(StdErrLog.LEVEL_OFF);

        StdErrCapture output = new StdErrCapture(log);

        // Various logging events
        log.debug("Squelch");
        log.debug("Squelch", new RuntimeException("Squelch"));
        log.info("Squelch");
        log.info("Squelch", new IllegalStateException("Squelch"));
        log.warn("Squelch");
        log.warn("Squelch", new Exception("Squelch"));
        log.ignore(new Throwable("Squelch"));

        // Validate Output
        output.assertNotContains("Squelch");
    }

    /**
     * Tests StdErrLog.debug() methods with level filtering.
     * <p>
     * Should only see DEBUG level messages when level is set to {@link StdErrLog#LEVEL_DEBUG} and below.
     */
    @Test
    public void testDebugFiltering() throws UnsupportedEncodingException
    {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setHideStacks(true);

        StdErrCapture output = new StdErrCapture(log);

        // Normal/Default behavior
        log.debug("Tobacconist");
        log.debug("<spoken line>", new Throwable("on editing room floor"));

        // Level Debug
        log.setLevel(StdErrLog.LEVEL_DEBUG);
        log.debug("my hovercraft is");

        log.debug("<zoom>", new Throwable("out of focus"));
        log.debug(new Throwable("scene lost"));

        // Level All
        log.setLevel(StdErrLog.LEVEL_ALL);
        log.debug("full of eels.");

        // Level Warn
        log.setLevel(StdErrLog.LEVEL_WARN);
        log.debug("what?");

        // Validate Output
        // System.err.print(output);
        output.assertNotContains("Tobacconist");
        output.assertContains("my hovercraft is");
        output.assertContains("full of eels.");
        output.assertNotContains("what?");

        // Validate Stack Traces
        output.assertNotContains("<spoken line>");
        output.assertNotContains("on editing room floor");

        output.assertContains(".StdErrLogTest:tname: <zoom>");
        output.assertContains("java.lang.Throwable: out of focus");
        output.assertContains("java.lang.Throwable: scene lost");
    }

    /**
     * Tests StdErrLog with {@link Logger#ignore(Throwable)} use.
     * <p>
     * Should only see IGNORED level messages when level is set to {@link StdErrLog#LEVEL_ALL}.
     */
    @Test
    public void testIgnores() throws UnsupportedEncodingException
    {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setHideStacks(true);

        StdErrCapture output = new StdErrCapture(log);

        // Normal/Default behavior
        log.ignore(new Throwable("IGNORE ME"));

        // Show Ignored
        log.setLevel(StdErrLog.LEVEL_ALL);
        log.ignore(new Throwable("Don't ignore me"));

        // Set to Debug level
        log.setLevel(StdErrLog.LEVEL_DEBUG);
        log.ignore(new Throwable("Debug me"));

        // Validate Output
        // System.err.print(output);
        output.assertNotContains("IGNORE ME");
        output.assertContains("Don't ignore me");
        output.assertNotContains("Debug me");
    }

    @Test
    public void testIsDebugEnabled() {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setHideStacks(true);

        log.setLevel(StdErrLog.LEVEL_ALL);
        Assert.assertThat("log.level(all).isDebugEnabled", log.isDebugEnabled(), is(true));

        log.setLevel(StdErrLog.LEVEL_DEBUG);
        Assert.assertThat("log.level(debug).isDebugEnabled", log.isDebugEnabled(), is(true));

        log.setLevel(StdErrLog.LEVEL_INFO);
        Assert.assertThat("log.level(info).isDebugEnabled", log.isDebugEnabled(), is(false));

        log.setLevel(StdErrLog.LEVEL_WARN);
        Assert.assertThat("log.level(warn).isDebugEnabled", log.isDebugEnabled(), is(false));

        log.setLevel(StdErrLog.LEVEL_OFF);
        Assert.assertThat("log.level(off).isDebugEnabled", log.isDebugEnabled(), is(false));
    }

    @Test
    public void testSetGetLevel()
    {
        StdErrLog log = new StdErrLog(StdErrLogTest.class.getName(),new Properties());
        log.setHideStacks(true);

        log.setLevel(StdErrLog.LEVEL_ALL);
        Assert.assertThat("log.level(all).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_ALL));

        log.setLevel(StdErrLog.LEVEL_DEBUG);
        Assert.assertThat("log.level(debug).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_DEBUG));

        log.setLevel(StdErrLog.LEVEL_INFO);
        Assert.assertThat("log.level(info).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_INFO));

        log.setLevel(StdErrLog.LEVEL_WARN);
        Assert.assertThat("log.level(warn).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_WARN));

        log.setLevel(StdErrLog.LEVEL_OFF);
        Assert.assertThat("log.level(off).getLevel()", log.getLevel(), is(StdErrLog.LEVEL_OFF));
    }

    @Test
    public void testGetChildLogger_Simple()
    {
        String baseName = "jetty";
        StdErrLog log = new StdErrLog(baseName,new Properties());
        log.setHideStacks(true);

        Assert.assertThat("Logger.name", log.getName(), is("jetty"));

        Logger log2 = log.getLogger("child");
        Assert.assertThat("Logger.child.name", log2.getName(), is("jetty.child"));
    }

    @Test
    public void testGetChildLogger_Deep()
    {
        String baseName = "jetty";
        StdErrLog log = new StdErrLog(baseName,new Properties());
        log.setHideStacks(true);

        Assert.assertThat("Logger.name", log.getName(), is("jetty"));

        Logger log2 = log.getLogger("child.of.the.sixties");
        Assert.assertThat("Logger.child.name", log2.getName(), is("jetty.child.of.the.sixties"));
    }

    @Test
    public void testGetChildLogger_Null()
    {
        String baseName = "jetty";
        StdErrLog log = new StdErrLog(baseName,new Properties());
        log.setHideStacks(true);

        Assert.assertThat("Logger.name", log.getName(), is("jetty"));

        // Pass null as child reference, should return parent logger
        Logger log2 = log.getLogger((String)null);
        Assert.assertThat("Logger.child.name", log2.getName(), is("jetty"));
        Assert.assertSame("Should have returned same logger", log2, log);
    }

    @Test
    public void testGetChildLogger_EmptyName()
    {
        String baseName = "jetty";
        StdErrLog log = new StdErrLog(baseName,new Properties());
        log.setHideStacks(true);

        Assert.assertThat("Logger.name", log.getName(), is("jetty"));

        // Pass empty name as child reference, should return parent logger
        Logger log2 = log.getLogger("");
        Assert.assertThat("Logger.child.name", log2.getName(), is("jetty"));
        Assert.assertSame("Should have returned same logger", log2, log);
    }

    @Test
    public void testGetChildLogger_EmptyNameSpaces()
    {
        String baseName = "jetty";
        StdErrLog log = new StdErrLog(baseName,new Properties());
        log.setHideStacks(true);

        Assert.assertThat("Logger.name", log.getName(), is("jetty"));

        // Pass empty name as child reference, should return parent logger
        Logger log2 = log.getLogger("      ");
        Assert.assertThat("Logger.child.name", log2.getName(), is("jetty"));
        Assert.assertSame("Should have returned same logger", log2, log);
    }

    @Test
    public void testGetChildLogger_NullParent()
    {
        StdErrLog log = new StdErrLog(null,new Properties());

        Assert.assertThat("Logger.name", log.getName(), is(""));

        Logger log2 = log.getLogger("jetty");
        Assert.assertThat("Logger.child.name", log2.getName(), is("jetty"));
        Assert.assertNotSame("Should have returned same logger", log2, log);
    }

    @Test
    public void testToString()
    {
        StdErrLog log = new StdErrLog("jetty",new Properties());

        log.setLevel(StdErrLog.LEVEL_ALL);
        Assert.assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=ALL"));

        log.setLevel(StdErrLog.LEVEL_DEBUG);
        Assert.assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=DEBUG"));

        log.setLevel(StdErrLog.LEVEL_INFO);
        Assert.assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=INFO"));

        log.setLevel(StdErrLog.LEVEL_WARN);
        Assert.assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=WARN"));

        log.setLevel(99); // intentionally bogus level
        Assert.assertThat("Logger.toString", log.toString(), is("StdErrLog:jetty:LEVEL=?"));
    }

    @Test
    public void testPrintSource() throws UnsupportedEncodingException
    {
        Properties props=new Properties();
        props.put("test.SOURCE","true");
        StdErrLog log = new StdErrLog("test",props);
        log.setLevel(StdErrLog.LEVEL_DEBUG);

        ByteArrayOutputStream test = new ByteArrayOutputStream();
        PrintStream err = new PrintStream(test);
        log.setStdErrStream(err);

        log.debug("Show me the source!");

        String output = new String(test.toByteArray(), StandardCharsets.UTF_8);
        // System.err.print(output);

        Assert.assertThat(output, containsString(".StdErrLogTest#testPrintSource(StdErrLogTest.java:"));
        

        props.put("test.SOURCE","false");
        log=new StdErrLog("other",props);
    }

    @Test
    public void testConfiguredAndSetDebugEnabled()
    {
        Properties props = new Properties();
        props.setProperty("org.eclipse.jetty.util.LEVEL","WARN");
        props.setProperty("org.eclipse.jetty.io.LEVEL", "WARN");

        StdErrLog root = new StdErrLog("", props);
        assertLevel(root,StdErrLog.LEVEL_INFO); // default

        StdErrLog log = (StdErrLog)root.getLogger(StdErrLogTest.class.getName());
        Assert.assertThat("Log.isDebugEnabled()", log.isDebugEnabled(), is(false));
        assertLevel(log,StdErrLog.LEVEL_WARN); // as configured

        // Boot stomp it all to debug
        root.setDebugEnabled(true);
        Assert.assertThat("Log.isDebugEnabled()", log.isDebugEnabled(), is(true));
        assertLevel(log,StdErrLog.LEVEL_DEBUG); // as stomped

        // Restore configured
        root.setDebugEnabled(false);
        Assert.assertThat("Log.isDebugEnabled()", log.isDebugEnabled(), is(false));
        assertLevel(log,StdErrLog.LEVEL_WARN); // as configured
    }

    @Test
    public void testSuppressed()
    {
        StdErrLog log = new StdErrLog("xxx",new Properties());
        StdErrCapture output = new StdErrCapture(log);

        Exception inner = new Exception("inner");
        inner.addSuppressed( new IllegalStateException(){{addSuppressed(new Exception("branch0"));}});
        IOException outer = new IOException("outer",inner);
        
        outer.addSuppressed( new IllegalStateException(){{addSuppressed(new Exception("branch1"));}});
        outer.addSuppressed( new IllegalArgumentException(){{addSuppressed(new Exception("branch2"));}});
        
        log.warn("problem",outer);

        output.assertContains("\t|\t|java.lang.Exception: branch2");
        output.assertContains("\t|\t|java.lang.Exception: branch1");
        output.assertContains("\t|\t|java.lang.Exception: branch0");
    }
    
    private void assertLevel(StdErrLog log, int expectedLevel)
    {
        Assert.assertThat("Log[" + log.getName() + "].level",levelToString(log.getLevel()),is(levelToString(expectedLevel)));
    }

    private String levelToString(int level)
    {
        switch (level)
        {
            case StdErrLog.LEVEL_ALL:
                return "ALL";
            case StdErrLog.LEVEL_DEBUG:
                return "DEBUG";
            case StdErrLog.LEVEL_INFO:
                return "INFO";
            case StdErrLog.LEVEL_WARN:
                return "WARN";
            default:
                return Integer.toString(level);
        }
    }
}
