/*
 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/**
 * @test
 * @bug     8140364
 * @author  danielfuchs
 * @summary  JDK implementation specific unit test for JDK internal artifacts.
 *           This test tests all the public API methods defined in the {@link
 *           java.lang.System.Logger} interface, as well as all the JDK
 *           internal methods defined in the
 *           {@link sun.util.logging.PlatformLogger.Bridge}
 *           interface, with loggers returned by  {@link
 *           java.lang.System.LoggerFinder#getLogger(java.lang.String, java.lang.Class)}
 *           and {@link java.lang.System.LoggerFinder#getLocalizedLogger(java.lang.String,
 *           java.util.ResourceBundle, java.lang.Class)}
 *           (using both a null resource bundle and a non null resource bundle).
 *           It calls both the {@link java.lang.System} factory methods and
 *           {@link jdk.internal.logger.LazyLoggers} to obtains those loggers,
 *           and configure them with all possible known levels.
 * @modules java.base/java.lang:open
 *          java.base/sun.util.logging
 *          java.base/jdk.internal.logger
 *          java.logging/sun.util.logging.internal
 * @build LoggerFinderBackendTest SystemClassLoader
 * @run  main/othervm -Djava.system.class.loader=SystemClassLoader -Dtest.logger.hidesProvider=true LoggerFinderBackendTest
 * @run  main/othervm -Djava.system.class.loader=SystemClassLoader -Dtest.logger.hidesProvider=false LoggerFinderBackendTest
 */


import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;
import java.lang.System.LoggerFinder;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import sun.util.logging.PlatformLogger.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import sun.util.logging.internal.LoggingProviderImpl;

/**
 * @author danielfuchs
 */
public class LoggerFinderBackendTest {

    // whether the implementation of Logger try to do a best
    // effort for logp... If the provider is not hidden, then
    // the logp() implementation comes from LoggerWrapper - which does a
    // best effort. Otherwise, it comes from the default provider
    // which does support logp.
    static final boolean BEST_EFFORT_FOR_LOGP =
            !Boolean.getBoolean("test.logger.hidesProvider");
    static final boolean VERBOSE = false;

    static final Class<java.lang.System.Logger> spiLoggerClass =
            java.lang.System.Logger.class;
    static final Class<java.lang.System.Logger> jdkLoggerClass =
            java.lang.System.Logger.class;
    static final Class<sun.util.logging.PlatformLogger.Bridge> bridgeLoggerClass =
            sun.util.logging.PlatformLogger.Bridge.class;

    /** Use to retrieve the log records that were produced by the JUL backend */
    static class LoggerTesterHandler extends Handler {
        public final List<LogRecord> records =
                Collections.synchronizedList(new ArrayList<>());

        @Override
        public void publish(LogRecord record) {
            record.getSourceClassName(); record.getSourceMethodName();
            records.add(record);
        }

        @Override
        public void flush() {
        }

        @Override
        public void close() throws SecurityException {
            records.clear();
        }

        public void reset() {
            records.clear();
        }
    }

    /** The {@link LoggerTesterHandler} handler is added to the root logger. */
    static final LoggerTesterHandler handler = new LoggerTesterHandler();
    static {
        for (Handler h : Logger.getLogger("").getHandlers()) {
            if (h instanceof ConsoleHandler) {
                Logger.getLogger("").removeHandler(h);
            }
        }
        Logger.getLogger("").addHandler(handler);
    }

    /**
     * A resource handler parameter that will be used when calling out the
     * logrb-like methods - as well as when calling the level-specific
     * methods that take a ResourceBundle parameter.
     */
    public static class ResourceBundeParam extends ResourceBundle {
        Map<String, String> map = Collections.synchronizedMap(new LinkedHashMap<>());
        @Override
        protected Object handleGetObject(String key) {
            map.putIfAbsent(key, "${"+key+"}");
            return map.get(key);
        }

        @Override
        public Enumeration<String> getKeys() {
            return Collections.enumeration(new LinkedHashSet<>(map.keySet()));
        }

    }

    final static ResourceBundle bundleParam =
            ResourceBundle.getBundle(ResourceBundeParam.class.getName());

    /**
     * A resource handler parameter that will be used when creating localized
     * loggers by calling {@link
     * LoggerFinder#getLocalizedLogger(java.lang.String, java.util.ResourceBundle, java.lang.Class)}.
     */
    public static class ResourceBundeLocalized extends ResourceBundle {
        Map<String, String> map = Collections.synchronizedMap(new LinkedHashMap<>());
        @Override
        protected Object handleGetObject(String key) {
            map.putIfAbsent(key, "Localized:${"+key+"}");
            return map.get(key);
        }

        @Override
        public Enumeration<String> getKeys() {
            return Collections.enumeration(new LinkedHashSet<>(map.keySet()));
        }

    }

    /**
     * The Levels enum is used to call all the level-specific methods on
     * a logger instance. To minimize the amount of code it uses reflection
     * to do so.
     */
    static Lookup lookup = MethodHandles.lookup();
    public enum Levels {
        /** Used to call all forms of Logger.log?(SEVERE, ...) */
        SEVERE("severe", bridgeLoggerClass, Level.SEVERE, null, "error", false),
        /** Used to call all forms of Logger.log?(WARNING,...) */
        WARNING("warning", bridgeLoggerClass, Level.WARNING, "warning", "warning", false),
        /** Used to call all forms of Logger.log?(INFO,...) */
        INFO("info", bridgeLoggerClass, Level.INFO, "info", "info", false),
        /** Used to call all forms of Logger.log?(CONFIG,...) */
        CONFIG("config", bridgeLoggerClass, Level.CONFIG, null, "debug", false),
        /** Used to call all forms of Logger.log?(FINE,...) */
        FINE("fine", bridgeLoggerClass, Level.FINE, null, "debug", false),
        /** Used to call all forms of Logger.log?(FINER,...) */
        FINER("finer", bridgeLoggerClass, Level.FINER, null, "trace", false),
        /** Used to call all forms of Logger.log?(FINEST,...) */
        FINEST("finest", bridgeLoggerClass, Level.FINEST, null, "trace", false),
        ;
        public final String method;  // The name of the level-specific method to call
        public final Class<?> definingClass; // which interface j.u.logger.Logger or j.u.logging.spi.Logger defines it
        public final Level platformLevel; // The platform Level it will be mapped to in Jul when Jul is the backend
        public final String jdkExtensionToJUL; // The name of the method called on the JUL logger when JUL is the backend
        public final String julToJdkExtension; // The name of the method called in the jdk extension by the default impl in jdk.internal.logging.Logger
        public final String enableMethod; // The name of the isXxxxEnabled method
        public final boolean hasSpecificIsEnabled;
        Levels(String method, Class<?> definingClass, Level defaultMapping,
                String jdkExtensionToJUL, String julToJdkExtension,
                boolean hasSpecificIsEnabled) {
            this.method = method;
            this.definingClass = definingClass;
            this.platformLevel = defaultMapping;
            this.jdkExtensionToJUL = jdkExtensionToJUL;
            this.julToJdkExtension = julToJdkExtension;
            this.hasSpecificIsEnabled = hasSpecificIsEnabled;
            if (hasSpecificIsEnabled) {
                this.enableMethod = "is" + method.substring(0,1).toUpperCase()
                    + method.substring(1) + "Enabled";
            } else {
                this.enableMethod = "isLoggable";
            }
        }

        /*
         * calls this level specific method - e.g. if this==INFO: logger.info(msg);
         */
        public void level(Object logger, String msg) {
            MethodType mt = MethodType.methodType(void.class, Level.class, String.class);
            invoke("log", logger, mt, platformLevel, msg);
        }

        /*
         * calls this level specific method - e.g. if this==INFO: logger.info(msgSupplier);
         */
        public void level(Object logger, Supplier<String> msgSupplier) {
            MethodType mt = MethodType.methodType(void.class,  Level.class, Supplier.class);
            invoke("log", logger, mt, platformLevel, msgSupplier);
        }

        /*
         * calls this level specific method - e.g. if this==INFO: logger.info(msg, params);
         */
        public void level(Object logger, String msg, Object... params) {
            MethodType mt = MethodType.methodType(void.class,  Level.class, String.class,
                    Object[].class);
            invoke("log", logger, mt, platformLevel, msg, params);
        }

        /*
         * calls this level specific method - e.g. if this==INFO: logger.info(msg, thrown);
         */
        public void level(Object logger, String msg, Throwable thrown) {
            MethodType mt = MethodType.methodType(void.class,  Level.class, String.class,
                    Throwable.class);
            invoke("log", logger, mt, platformLevel, msg, thrown);
        }

        /*
         * calls this level specific method - e.g. if this==INFO: logger.info(msgSupplier, thrown);
         */
        public void level(Object logger, Supplier<String> msgSupplier, Throwable thrown) {
            MethodType mt = MethodType.methodType(void.class,  Level.class,
                     Throwable.class, Supplier.class);
            invoke("log", logger, mt, platformLevel, thrown, msgSupplier);
        }

        /*
         * calls this level specific method - e.g. if this==INFO: logger.info(bundle, msg);
         */
        public void level(Object logger, String msg, ResourceBundle bundle) {
            MethodType mt = MethodType.methodType(void.class, Level.class,
                    ResourceBundle.class, String.class, Object[].class);
            invoke("logrb", logger, mt, platformLevel, bundle, msg, null);
        }

        public void level(Object logger, String msg, ResourceBundle bundle,
                Object... params) {
            MethodType mt = MethodType.methodType(void.class, Level.class,
                    ResourceBundle.class, String.class, Object[].class);
            invoke("logrb", logger, mt, platformLevel, bundle, msg, params);
        }

        public void level(Object logger, String msg, ResourceBundle bundle,
                Throwable thrown) {
            MethodType mt = MethodType.methodType(void.class, Level.class,
                    ResourceBundle.class, String.class, Throwable.class);
            invoke("logrb", logger, mt, platformLevel, bundle, msg, thrown);
        }

        public boolean isEnabled(Object logger) {
            try {
                if (hasSpecificIsEnabled) {
                    MethodType mt = MethodType.methodType(boolean.class);
                    final MethodHandle handle = lookup.findVirtual(definingClass,
                        enableMethod, mt).bindTo(logger);
                    return Boolean.class.cast(handle.invoke());
                } else {
                    MethodType mt = MethodType.methodType(boolean.class,
                        Level.class);
                    final MethodHandle handle = lookup.findVirtual(definingClass,
                        enableMethod, mt).bindTo(logger);
                    return Boolean.class.cast(handle.invoke(platformLevel));
                }
            } catch (Throwable ex) {
                throw new RuntimeException(ex);
            }
        }

        private void invoke(String method, Object logger, MethodType mt, Object... args) {
            try {
                final int last = mt.parameterCount()-1;
                boolean isVarargs = mt.parameterType(last).isArray();
                final MethodHandle handle = lookup.findVirtual(definingClass,
                        method, mt).bindTo(logger);

                final StringBuilder builder = new StringBuilder();
                builder.append(logger.getClass().getSimpleName()).append('.')
                        .append(method).append('(');
                String sep = "";
                int offset = 0;
                Object[] params = args;
                for (int i=0; (i-offset) < params.length; i++) {
                    if (isVarargs && i == last) {
                        offset = last;
                        params = (Object[])args[i];
                        if (params == null) break;
                    }
                    Object p = params[i - offset];
                    String quote = (p instanceof String) ? "\"" : "";
                    builder.append(sep).append(quote).append(p).append(quote);
                    sep = ", ";
                }
                builder.append(')');
                if (verbose) {
                    System.out.println(builder);
                }
                handle.invokeWithArguments(args);
            } catch (Throwable ex) {
                throw new RuntimeException(ex);
            }
        }

    };

    static interface Checker<LogResult, L> extends BiFunction<LogResult, L, Void> {}
    static interface JdkLogTester
            extends BiFunction<sun.util.logging.PlatformLogger.Bridge, Level, Void> {}
    static interface SpiLogTester
            extends BiFunction<java.lang.System.Logger, java.lang.System.Logger.Level, Void> {}

    static interface MethodInvoker<LOGGER, LEVEL> {
        public void logX(LOGGER logger, LEVEL level, Object... args);
    }

    public enum JdkLogMethodInvoker
           implements MethodInvoker<sun.util.logging.PlatformLogger.Bridge, Level> {
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, String, Object...)};
         **/
        LOG_STRING_PARAMS("log", MethodType.methodType(void.class,
                Level.class, String.class, Object[].class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, String, Throwable)};
         **/
        LOG_STRING_THROWN("log", MethodType.methodType(void.class,
                Level.class, String.class, Throwable.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, Supplier<String>)};
         **/
        LOG_SUPPLIER("log", MethodType.methodType(void.class,
                Level.class, Supplier.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, Throwable, Supplier<String>)};
         **/
        LOG_SUPPLIER_THROWN("log", MethodType.methodType(void.class,
                Level.class, Throwable.class, Supplier.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logp(Level, String, String, String)};
         **/
        LOGP_STRING("logp", MethodType.methodType(void.class,
                Level.class, String.class, String.class, String.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logp(Level, String, String, String, Object...)};
         **/
        LOGP_STRING_PARAMS("logp", MethodType.methodType(void.class,
                Level.class, String.class, String.class, String.class, Object[].class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logp(Level, String, String, String, Throwable)};
         **/
        LOGP_STRING_THROWN("logp", MethodType.methodType(void.class,
                Level.class, String.class, String.class, String.class, Throwable.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logp(Level, String, String, Supplier<String>)};
         **/
        LOGP_SUPPLIER("logp", MethodType.methodType(void.class,
                Level.class, String.class, String.class, Supplier.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logp(Level, String, String, Throwable, Supplier<String>)};
         **/
        LOGP_SUPPLIER_THROWN("logp", MethodType.methodType(void.class,
                Level.class, String.class, String.class,
                Throwable.class, Supplier.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logrb(Level, ResourceBundle, String, Object...)};
         **/
        LOGRB_STRING_PARAMS("logrb", MethodType.methodType(void.class,
                Level.class, ResourceBundle.class, String.class, Object[].class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logrb(Level, ResourceBundle, String, Throwable)};
         **/
        LOGRB_STRING_THROWN("logrb", MethodType.methodType(void.class,
                Level.class, ResourceBundle.class, String.class, Throwable.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logrb(Level, String, String, ResourceBundle, String, Object...)};
         **/
        LOGRBP_STRING_PARAMS("logrb", MethodType.methodType(void.class,
                Level.class, String.class, String.class, ResourceBundle.class,
                String.class, Object[].class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logrb(Level, String, String, ResourceBundle, String, Throwable)};
         **/
        LOGRBP_STRING_THROWN("logrb", MethodType.methodType(void.class,
                Level.class, String.class, String.class, ResourceBundle.class,
                String.class, Throwable.class)),
        ;
        final MethodType mt;
        final String method;
        JdkLogMethodInvoker(String method, MethodType mt) {
            this.mt = mt;
            this.method = method;
        }
        Object[] makeArgs(Level level, Object... rest) {
            List<Object> list = new ArrayList<>(rest == null ? 1 : rest.length + 1);
            list.add(level);
            if (rest != null) {
                list.addAll(Arrays.asList(rest));
            }
            return list.toArray(new Object[list.size()]);
        }

        @Override
        public void logX(sun.util.logging.PlatformLogger.Bridge logger, Level level, Object... args) {
            try {
                MethodHandle handle = lookup.findVirtual(bridgeLoggerClass,
                        method, mt).bindTo(logger);
                final int last = mt.parameterCount()-1;
                boolean isVarargs = mt.parameterType(last).isArray();

                args = makeArgs(level, args);

                final StringBuilder builder = new StringBuilder();
                builder.append(logger.getClass().getSimpleName()).append('.')
                        .append(this.method).append('(');
                String sep = "";
                int offset = 0;
                Object[] params = args;
                for (int i=0; (i-offset) < params.length; i++) {
                    if (isVarargs && i == last) {
                        offset = last;
                        params = (Object[])args[i];
                        if (params == null) break;
                    }
                    Object p = params[i - offset];
                    String quote = (p instanceof String) ? "\"" : "";
                    p = p instanceof Level ? "Level."+p : p;
                    builder.append(sep).append(quote).append(p).append(quote);
                    sep = ", ";
                }
                builder.append(')');
                if (verbose) System.out.println(builder);
                handle.invokeWithArguments(args);
            } catch (Throwable ex) {
                throw new RuntimeException(ex);
            }
        }
    }


    public enum SpiLogMethodInvoker implements MethodInvoker<java.lang.System.Logger,
            java.lang.System.Logger.Level> {
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, String, Object...)};
         **/
        LOG_STRING_PARAMS("log", MethodType.methodType(void.class,
                java.lang.System.Logger.Level.class, String.class, Object[].class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, String, Throwable)};
         **/
        LOG_STRING_THROWN("log", MethodType.methodType(void.class,
                java.lang.System.Logger.Level.class, String.class, Throwable.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, Supplier<String>)};
         **/
        LOG_SUPPLIER("log", MethodType.methodType(void.class,
                java.lang.System.Logger.Level.class, Supplier.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, Throwable, Supplier<String>)};
         **/
        LOG_SUPPLIER_THROWN("log", MethodType.methodType(void.class,
                java.lang.System.Logger.Level.class, Supplier.class, Throwable.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#log(Level, Supplier<String>)};
         **/
        LOG_OBJECT("log", MethodType.methodType(void.class,
                java.lang.System.Logger.Level.class, Object.class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logrb(Level, ResourceBundle, String, Object...)};
         **/
        LOGRB_STRING_PARAMS("log", MethodType.methodType(void.class,
                java.lang.System.Logger.Level.class, ResourceBundle.class,
                String.class, Object[].class)),
        /**
         * Tests {@link
         * jdk.internal.logging.Logger#logrb(Level, ResourceBundle, String, Throwable)};
         **/
        LOGRB_STRING_THROWN("log", MethodType.methodType(void.class,
                java.lang.System.Logger.Level.class, ResourceBundle.class,
                String.class, Throwable.class)),
        ;
        final MethodType mt;
        final String method;
        SpiLogMethodInvoker(String method, MethodType mt) {
            this.mt = mt;
            this.method = method;
        }
        Object[] makeArgs(java.lang.System.Logger.Level level, Object... rest) {
            List<Object> list = new ArrayList<>(rest == null ? 1 : rest.length + 1);
            list.add(level);
            if (rest != null) {
                list.addAll(Arrays.asList(rest));
            }
            return list.toArray(new Object[list.size()]);
        }

        @Override
        public void logX(java.lang.System.Logger logger,
                java.lang.System.Logger.Level level, Object... args) {
            try {
                MethodHandle handle = lookup.findVirtual(spiLoggerClass,
                        method, mt).bindTo(logger);
                final int last = mt.parameterCount()-1;
                boolean isVarargs = mt.parameterType(last).isArray();

                args = makeArgs(level, args);

                final StringBuilder builder = new StringBuilder();
                builder.append(logger.getClass().getSimpleName()).append('.')
                        .append(this.method).append('(');
                String sep = "";
                int offset = 0;
                Object[] params = args;
                for (int i=0; (i-offset) < params.length; i++) {
                    if (isVarargs && i == last) {
                        offset = last;
                        params = (Object[])args[i];
                        if (params == null) break;
                    }
                    Object p = params[i - offset];
                    String quote = (p instanceof String) ? "\"" : "";
                    p = p instanceof Level ? "Level."+p : p;
                    builder.append(sep).append(quote).append(p).append(quote);
                    sep = ", ";
                }
                builder.append(')');
                if (verbose) System.out.println(builder);
                handle.invokeWithArguments(args);
            } catch (Throwable ex) {
                throw new RuntimeException(ex);
            }
        }
    }


    public abstract static class BackendTester<BackendRecord> {
        static final Level[] levelMap = {Level.ALL, Level.FINER, Level.FINE,
            Level.INFO, Level.WARNING, Level.SEVERE, Level.OFF};

        abstract class BackendAdaptor {
            public abstract String getLoggerName(BackendRecord res);
            public abstract Object getLevel(BackendRecord res);
            public abstract String getMessage(BackendRecord res);
            public abstract String getSourceClassName(BackendRecord res);
            public abstract String getSourceMethodName(BackendRecord res);
            public abstract Throwable getThrown(BackendRecord res);
            public abstract ResourceBundle getResourceBundle(BackendRecord res);
            public abstract void setLevel(java.lang.System.Logger logger,
                    Level level);
            public abstract void setLevel(java.lang.System.Logger logger,
                    java.lang.System.Logger.Level level);
            public abstract List<BackendRecord> getBackendRecords();
            public abstract void resetBackendRecords();
            public boolean shouldBeLoggable(Levels level, Level loggerLevel) {
                final Level logLevel = level.platformLevel;
                return shouldBeLoggable(logLevel, loggerLevel);
            }
            public boolean shouldBeLoggable(Level logLevel, Level loggerLevel) {
                return loggerLevel.intValue() != Level.OFF.intValue()
                        && logLevel.intValue() >= loggerLevel.intValue();
            }
            public boolean shouldBeLoggable(java.lang.System.Logger.Level logLevel,
                    java.lang.System.Logger.Level loggerLevel) {
                return loggerLevel != java.lang.System.Logger.Level.OFF
                        && logLevel.ordinal() >= loggerLevel.ordinal();
            }
            public boolean isLoggable(java.lang.System.Logger logger, Level l) {
                return bridgeLoggerClass.cast(logger).isLoggable(l);
            }
            public String getCallerClassName(Levels level, String clazz) {
                return clazz != null ? clazz : Levels.class.getName();
            }
            public String getCallerClassName(MethodInvoker<?,?> logMethod,
                   String clazz) {
                return clazz != null ? clazz : logMethod.getClass().getName();
            }
            public String getCallerMethodName(Levels level, String method) {
                return method != null ? method : "invoke";
            }
            public String getCallerMethodName(MethodInvoker<?,?> logMethod,
                    String method) {
                return method != null ? method : "logX";
            }
            public Object getMappedLevel(Object level) {
                return level;
            }

            public Level toJUL(java.lang.System.Logger.Level level) {
                return levelMap[level.ordinal()];
            }
        }

        public final boolean isSystem;
        public final Class<? extends java.lang.System.Logger> restrictedTo;
        public final ResourceBundle localized;
        public BackendTester(boolean isSystem) {
            this(isSystem,null,null);
        }
        public BackendTester(boolean isSystem, ResourceBundle localized) {
            this(isSystem,null,localized);
        }
        public BackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo) {
            this(isSystem, restrictedTo, null);
        }
        public BackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo,
                ResourceBundle localized) {
            this.isSystem = isSystem;
            this.restrictedTo = restrictedTo;
            this.localized = localized;
        }

        public java.lang.System.Logger convert(java.lang.System.Logger logger) {
            return logger;
        }

        public static Level[] LEVELS = {
            Level.OFF,
            Level.SEVERE, Level.WARNING, Level.INFO, Level.CONFIG,
            Level.FINE, Level.FINER, Level.FINEST,
            Level.ALL
        };

        abstract BackendAdaptor adaptor();

        protected void checkRecord(Levels test, BackendRecord res, String loggerName,
                Level level, String msg, String className, String methodName,
                Throwable thrown, ResourceBundle bundle, Object... params) {
            checkRecord(test, res, loggerName, level, ()->msg, className,
                    methodName, thrown, bundle, params);

        }
        protected void checkRecord(Levels test, BackendRecord res, String loggerName,
                Level level, Supplier<String> msg, String className, String methodName,
                Throwable thrown, ResourceBundle bundle, Object... params) {
            checkRecord(test.method, res, loggerName, level, msg,
                    className, methodName, thrown, bundle, params);
        }
        protected <L> void checkRecord(String logMethod, BackendRecord res, String loggerName,
                L level, Supplier<String> msg, String className, String methodName,
                Throwable thrown, ResourceBundle bundle, Object... params) {
            final BackendAdaptor analyzer = adaptor();
            if (! Objects.equals(analyzer.getLoggerName(res), loggerName)) {
                throw new RuntimeException(logMethod+": expected logger name "
                        + loggerName + " got " + analyzer.getLoggerName(res));
            }
            if (!Objects.equals(analyzer.getLevel(res), analyzer.getMappedLevel(level))) {
                throw new RuntimeException(logMethod+": expected level "
                        + analyzer.getMappedLevel(level) + " got " + analyzer.getLevel(res));
            }
            if (!Objects.equals(analyzer.getMessage(res), msg.get())) {
                throw new RuntimeException(logMethod+": expected message \""
                        + msg.get() + "\" got \"" + analyzer.getMessage(res) +"\"");
            }
            if (!Objects.equals(analyzer.getSourceClassName(res), className)) {
                throw new RuntimeException(logMethod
                        + ": expected class name \"" + className
                        + "\" got \"" + analyzer.getSourceClassName(res) +"\"");
            }
            if (!Objects.equals(analyzer.getSourceMethodName(res), methodName)) {
                throw new RuntimeException(logMethod
                        + ": expected method name \"" + methodName
                        + "\" got \"" + analyzer.getSourceMethodName(res) +"\"");
            }
            final Throwable thrownRes = analyzer.getThrown(res);
            if (!Objects.equals(thrownRes, thrown)) {
                throw new RuntimeException(logMethod
                        + ": expected throwable \"" + thrown
                        + "\" got \"" + thrownRes + "\"");
            }
            if (!Objects.equals(analyzer.getResourceBundle(res), bundle)) {
                throw new RuntimeException(logMethod
                        + ": expected bundle \"" + bundle
                        + "\" got \"" + analyzer.getResourceBundle(res) +"\"");
            }
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                String msg) {
            Runnable test = () -> level.level(logger, msg);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            null, localized);
                return null;
            };
            test("msg", level, logger, test, check);
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                String msg, Object... params) {
            Runnable test = () -> level.level(logger, msg, (Object[])params);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            null, localized, (Object[])params);
                return null;
            };
            test("msg, params", level, logger, test, check);
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                String msg, Throwable thrown) {
            Runnable test = () -> level.level(logger, msg, thrown);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            thrown, localized);
                return null;
            };
            test("msg, thrown", level, logger, test, check);
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                Supplier<String> msg) {
            Runnable test = () -> level.level(logger, msg);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            null, null);
                return null;
            };
            test("msgSupplier", level, logger, test, check);
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                Supplier<String> msg, Throwable thrown) {
            Runnable test = () -> level.level(logger, msg, thrown);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            thrown, null);
                return null;
            };
            test("throw, msgSupplier", level, logger, test, check);
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                String msg, ResourceBundle bundle) {
            Runnable test = () -> level.level(logger, msg, bundle);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            null, bundle);
                return null;
            };
            test("bundle, msg", level, logger, test, check);
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                String msg, ResourceBundle bundle, Object... params) {
            Runnable test = () -> level.level(logger, msg, bundle, (Object[])params);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            null, bundle, (Object[])params);
                return null;
            };
            test("bundle, msg, params", level, logger, test, check);
        }

        public void testLevel(Levels level, java.lang.System.Logger logger,
                String msg, ResourceBundle bundle, Throwable thrown) {
            Runnable test = () -> level.level(logger, msg, bundle, thrown);
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord(level, res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(level, Levels.class.getName()),
                            adaptor().getCallerMethodName(level, "invoke"),
                            thrown, bundle);
                return null;
            };
            test("bundle, msg, throwable", level, logger, test, check);
        }

        // System.Logger
        public void testSpiLog(java.lang.System.Logger logger, String msg) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOG_STRING_PARAMS,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOG_STRING_PARAMS,
                                    "logX"), null, localized);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOG_STRING_PARAMS.logX(x, level, msg, (Object[])null);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "log(Level." + l + ", \"" + msg + "\")";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger,
                ResourceBundle bundle, String msg) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    "logX"), null, bundle);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOGRB_STRING_PARAMS.logX(x, level, bundle, msg, (Object[])null);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "log(Level." + l
                    + ", bundle, \"" + msg + "\")";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger, String msg, Object... params) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOG_STRING_PARAMS,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOG_STRING_PARAMS,
                                    "logX"), null, localized, params);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOG_STRING_PARAMS.logX(x, level, msg, params);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "log(Level." + l + ", \"" + msg + "\", params...)";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger,
                ResourceBundle bundle, String msg, Object... params) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    "logX"), null, bundle, params);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOGRB_STRING_PARAMS.logX(x, level, bundle, msg, params);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "log(Level." + l
                    + ", bundle, \"" + msg + "\", params...)";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger, String msg, Throwable thrown) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                           adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOG_STRING_THROWN,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOG_STRING_THROWN,
                                    "logX"), thrown, localized);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOG_STRING_THROWN.logX(x, level, msg, thrown);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", \"" + msg + "\", thrown)";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger,
                ResourceBundle bundle, String msg, Throwable thrown) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOGRB_STRING_THROWN,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOGRB_STRING_THROWN,
                                    "logX"), thrown, bundle);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOGRB_STRING_THROWN.logX(x, level, bundle, msg, thrown);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", bundle, \"" + msg + "\", thrown)";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger, Supplier<String> msg) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOG_SUPPLIER,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOG_SUPPLIER,
                                    "logX"), null, null);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOG_SUPPLIER.logX(x, level, msg);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", () -> \"" + msg.get() + "\")";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger, Object obj) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> obj.toString(),
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOG_OBJECT,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOG_OBJECT,
                                    "logX"), null, null);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOG_OBJECT.logX(x, level, obj);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", new "+obj.getClass().getSimpleName()+"(\""
                            + obj.toString() + "\"))";
            testSpiLog(logger, tester, check, nameProducer);
        }

        public void testSpiLog(java.lang.System.Logger logger, Throwable thrown, Supplier<String> msg) {
            Checker<BackendRecord, java.lang.System.Logger.Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(
                                    SpiLogMethodInvoker.LOG_SUPPLIER_THROWN,
                                    SpiLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    SpiLogMethodInvoker.LOG_SUPPLIER_THROWN,
                                    "logX"), thrown, null);
                return null;
            };
            SpiLogTester tester = (x, level) -> {
                SpiLogMethodInvoker.LOG_SUPPLIER_THROWN.logX(x, level, msg, thrown);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", () -> \"" + msg.get() + "\", thrown)";
            testSpiLog(logger, tester, check, nameProducer);
        }


        // JDK

        public void testLog(java.lang.System.Logger logger, String msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOG_STRING_PARAMS,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOG_STRING_PARAMS,
                                    "logX"), null, localized);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOG_STRING_PARAMS.logX(x, level, msg, (Object[])null);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "log(Level." + l + ", \"" + msg + "\")";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogrb(java.lang.System.Logger logger,
                ResourceBundle bundle, String msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    "logX"), null, bundle);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGRB_STRING_PARAMS.logX(x, level, bundle, msg, (Object[])null);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "logrb(Level." + l
                    + ", bundle, \"" + msg + "\")";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLog(java.lang.System.Logger logger, String msg, Object... params) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOG_STRING_PARAMS,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOG_STRING_PARAMS,
                                    "logX"), null, localized, params);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOG_STRING_PARAMS.logX(x, level, msg, params);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "log(Level." + l + ", \"" + msg + "\", params...)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogrb(java.lang.System.Logger logger,
                ResourceBundle bundle, String msg, Object... params) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOGRB_STRING_PARAMS,
                                    "logX"), null, bundle, params);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGRB_STRING_PARAMS.logX(x, level, bundle, msg, params);
                return null;
            };
            Function<String, String> nameProducer = (l) -> "log(Level." + l
                    + ", bundle, \"" + msg + "\", params...)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLog(java.lang.System.Logger logger, String msg, Throwable thrown) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                           adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOG_STRING_THROWN,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOG_STRING_THROWN,
                                    "logX"), thrown, localized);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOG_STRING_THROWN.logX(x, level, msg, thrown);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", \"" + msg + "\", thrown)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogrb(java.lang.System.Logger logger,
                ResourceBundle bundle, String msg, Throwable thrown) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRB_STRING_THROWN,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOGRB_STRING_THROWN,
                                    "logX"), thrown, bundle);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGRB_STRING_THROWN.logX(x, level, bundle, msg, thrown);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", bundle, \"" + msg + "\", thrown)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLog(java.lang.System.Logger logger, Supplier<String> msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOG_SUPPLIER,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOG_SUPPLIER,
                                    "logX"), null, null);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOG_SUPPLIER.logX(x, level, msg);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", () -> \"" + msg.get() + "\")";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLog(java.lang.System.Logger logger, Throwable thrown, Supplier<String> msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOG_SUPPLIER_THROWN,
                                    JdkLogMethodInvoker.class.getName()),
                            adaptor().getCallerMethodName(
                                    JdkLogMethodInvoker.LOG_SUPPLIER_THROWN,
                                    "logX"), thrown, null);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOG_SUPPLIER_THROWN.logX(x, level, thrown, msg);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", () -> \"" + msg.get() + "\", thrown)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        static Supplier<String> logpMessage(ResourceBundle bundle,
                String className, String methodName, Supplier<String> msg) {
            if (BEST_EFFORT_FOR_LOGP && bundle == null
                    && (className != null || methodName != null)) {
                final String cName = className == null ? "" :  className;
                final String mName = methodName == null ? "" : methodName;
                return () -> {
                    String m = msg.get();
                    return String.format("[%s %s] %s", cName, mName, m == null ? "" : m);
                };
            } else {
                return msg;
            }
        }

        public void testLogp(java.lang.System.Logger logger, String className,
                String methodName, String msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("logp", res, logger.getName(), l,
                            logpMessage(localized, className, methodName, () -> msg),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_STRING, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_STRING, methodName),
                            null, localized);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGP_STRING.logX(x, level,
                        className, methodName, msg);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "logp(Level." + l + ", class, method, \"" + msg + "\")";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogrb(java.lang.System.Logger logger, String className,
                String methodName, ResourceBundle bundle, String msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("logp", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRBP_STRING_PARAMS, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRBP_STRING_PARAMS, methodName),
                            null, bundle);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGRBP_STRING_PARAMS.logX(x, level,
                        className, methodName, bundle, msg, (Object[])null);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "logp(Level." + l + ", class, method, bundle, \"" + msg + "\")";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogp(java.lang.System.Logger logger, String className,
                String methodName, String msg, Object... params) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("logp", res, logger.getName(), l,
                            logpMessage(localized, className, methodName, () -> msg),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_STRING_PARAMS, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_STRING_PARAMS, methodName),
                            null, localized, params);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGP_STRING_PARAMS.logX(x, level,
                        className, methodName, msg, params);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", class, method, \"" + msg + "\", params...)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogrb(java.lang.System.Logger logger, String className,
                String methodName, ResourceBundle bundle, String msg,
                Object... params) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("logp", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRBP_STRING_PARAMS, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRBP_STRING_PARAMS, methodName),
                            null, bundle, params);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGRBP_STRING_PARAMS.logX(x, level,
                        className, methodName, bundle, msg, params);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", class, method, bundle, \""
                            + msg + "\", params...)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogp(java.lang.System.Logger logger, String className,
                String methodName, String msg, Throwable thrown) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l,
                            logpMessage(localized, className, methodName, () -> msg),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_STRING_THROWN, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_STRING_THROWN, methodName),
                            thrown, localized);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGP_STRING_THROWN.logX(x, level,
                        className, methodName, msg, thrown);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", class, method, \"" + msg + "\", thrown)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogrb(java.lang.System.Logger logger, String className,
                String methodName, ResourceBundle bundle,
                String msg, Throwable thrown) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l, () -> msg,
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRBP_STRING_THROWN, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGRBP_STRING_THROWN, methodName),
                            thrown, bundle);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGRBP_STRING_THROWN.logX(x, level,
                        className, methodName, bundle, msg, thrown);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", class, method, bundle, \"" + msg + "\", thrown)";
            testJdkLog(logger, tester, check, nameProducer);

        }

        public void testLogp(java.lang.System.Logger logger, String className,
                String methodName, Supplier<String> msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l,
                            logpMessage(null, className, methodName, msg),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_SUPPLIER, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_SUPPLIER, methodName),
                            null, null);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGP_SUPPLIER.logX(x, level,
                        className, methodName, msg);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", class, method, () -> \"" + msg.get() + "\")";
            testJdkLog(logger, tester, check, nameProducer);
        }

        public void testLogp(java.lang.System.Logger logger, String className,
                String methodName, Throwable thrown, Supplier<String> msg) {
            Checker<BackendRecord, Level> check = (res, l) -> {
                checkRecord("log", res, logger.getName(), l,
                            logpMessage(null, className, methodName, msg),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_SUPPLIER_THROWN, className),
                            adaptor().getCallerClassName(
                                    JdkLogMethodInvoker.LOGP_SUPPLIER_THROWN, methodName),
                            thrown, null);
                return null;
            };
            JdkLogTester tester = (x, level) -> {
                JdkLogMethodInvoker.LOGP_SUPPLIER_THROWN.logX(x, level,
                        className, methodName, thrown, msg);
                return null;
            };
            Function<String, String> nameProducer = (l) ->
                    "log(Level." + l + ", class, method, () -> \"" + msg.get() + "\", thrown)";
            testJdkLog(logger, tester, check, nameProducer);
        }

        private void testJdkLog(java.lang.System.Logger logger,
                JdkLogTester log, Checker<BackendRecord,Level> check,
                Function<String, String> nameProducer) {
            if (restrictedTo != null) {
                if (!bridgeLoggerClass.isAssignableFrom(restrictedTo)) {
                    if (VERBOSE) {
                        System.out.println("Skipping method from "
                            + bridgeLoggerClass);
                    }
                    return;
                }
            }
            System.out.println("Testing Logger." + nameProducer.apply("*")
                     + " on " + logger);
            final BackendAdaptor adaptor = adaptor();
            for (Level loggerLevel : LEVELS) {
                adaptor.setLevel(logger, loggerLevel);
                for (Level l : LEVELS) {
                    check(logger, () -> log.apply(bridgeLoggerClass.cast(logger), l),
                          check, () -> adaptor.isLoggable(logger, l),
                          () -> adaptor.shouldBeLoggable(l, loggerLevel),
                          l, loggerLevel, nameProducer.apply(l.toString()));
                }
            }
        }

        private void testSpiLog(java.lang.System.Logger logger,
                SpiLogTester log, Checker<BackendRecord, java.lang.System.Logger.Level> check,
                Function<String, String> nameProducer) {
            System.out.println("Testing System.Logger." + nameProducer.apply("*")
                     + " on " + logger);
            final BackendAdaptor adaptor = adaptor();
            for (java.lang.System.Logger.Level loggerLevel : java.lang.System.Logger.Level.values()) {

                adaptor.setLevel(logger, loggerLevel);
                for (java.lang.System.Logger.Level l : java.lang.System.Logger.Level.values()) {
                    check(logger, () -> log.apply(logger, l),
                          check, () -> logger.isLoggable(l),
                          () -> adaptor.shouldBeLoggable(l, loggerLevel),
                          l, loggerLevel, nameProducer.apply(l.toString()));
                }
            }
        }

        private void test(String args, Levels level, java.lang.System.Logger logger,
                Runnable test, Checker<BackendRecord, Level> check) {
            if (restrictedTo != null) {
                if (!level.definingClass.isAssignableFrom(restrictedTo)) {
                    if (VERBOSE) {
                        System.out.println("Skipping method from "
                            + level.definingClass);
                    }
                    return;
                }
            }
            String method = args.contains("bundle") ? "logrb" : "log";
            System.out.println("Testing Logger."
                    + method + "(Level." + level.platformLevel
                    + ", "+ args + ")" + " on " + logger);
            final BackendAdaptor adaptor = adaptor();
            for (Level loggerLevel : LEVELS) {
                adaptor.setLevel(logger, loggerLevel);
                check(logger, test, check,
                        () -> level.isEnabled(logger),
                        () -> adaptor.shouldBeLoggable(level, loggerLevel),
                        level.platformLevel, loggerLevel, level.method);
            }
        }

        private <L> void check(java.lang.System.Logger logger,
                Runnable test, Checker<BackendRecord,L> check,
                BooleanSupplier checkLevelEnabled,
                BooleanSupplier shouldBeLoggable,
                L logLevel, L loggerLevel, String logMethod) {
            final BackendAdaptor adaptor = adaptor();
            adaptor.resetBackendRecords();
            test.run();
            final List<BackendRecord> records = adaptor.getBackendRecords();
            if (shouldBeLoggable.getAsBoolean()) {
                if (!checkLevelEnabled.getAsBoolean()) {
                    throw new RuntimeException("Logger is not enabled for "
                            + logMethod
                            + " although logger level is " + loggerLevel);
                }
                if (records.size() != 1) {
                    throw new RuntimeException(loggerLevel + " [" +
                            logLevel + "] : Unexpected record sizes: "
                        + records.toString());
                }
                BackendRecord res = records.get(0);
                check.apply(res, logLevel);
            } else {
                if (checkLevelEnabled.getAsBoolean()) {
                    throw new RuntimeException("Logger is enabled for "
                            + logMethod
                            + " although logger level is " + loggerLevel);
                }
                if (!records.isEmpty()) {
                    throw new RuntimeException(loggerLevel + " [" +
                            logLevel + "] : Unexpected record sizes: "
                        + records.toString());
                }
            }
        }
    }

    public static class JULBackendTester extends BackendTester<LogRecord>{

        public JULBackendTester(boolean isSystem) {
            this(isSystem,null,null);
        }
        public JULBackendTester(boolean isSystem, ResourceBundle localized) {
            this(isSystem,null,localized);
        }
        public JULBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo) {
            this(isSystem, restrictedTo, null);
        }
        public JULBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo,
                ResourceBundle localized) {
            super(isSystem, restrictedTo, localized);
        }

        Logger getBackendLogger(String name) {
            if (isSystem) {
                return LoggingProviderImpl.getLogManagerAccess().demandLoggerFor(
                        LogManager.getLogManager(), name, Thread.class.getModule());
            } else {
                return Logger.getLogger(name);
            }
        }

        class JULBackendAdaptor extends BackendAdaptor {
            @Override
            public String getLoggerName(LogRecord res) {
                return res.getLoggerName();
            }
            @Override
            public Level getLevel(LogRecord res) {
                return Level.valueOf(res.getLevel().getName());
            }
            @Override
            public String getMessage(LogRecord res) {
                return res.getMessage();
            }
            @Override
            public String getSourceClassName(LogRecord res) {
                return res.getSourceClassName();
            }
            @Override
            public String getSourceMethodName(LogRecord res) {
                return res.getSourceMethodName();
            }
            @Override
            public Throwable getThrown(LogRecord res) {
                return res.getThrown();
            }
            @Override
            public ResourceBundle getResourceBundle(LogRecord res) {
                return res.getResourceBundle();
            }
            @Override
            public void setLevel(java.lang.System.Logger logger, Level level) {
                Logger backend = getBackendLogger(logger.getName());
                backend.setLevel(java.util.logging.Level.parse(level.name()));
            }
            @Override
            public void setLevel(java.lang.System.Logger logger, java.lang.System.Logger.Level level) {
                setLevel(logger, toJUL(level));
            }
            @Override
            public List<LogRecord> getBackendRecords() {
                return handler.records;
            }
            @Override
            public void resetBackendRecords() {
                handler.reset();
            }
            @Override
            public Level getMappedLevel(Object level) {
                if (level instanceof java.lang.System.Logger.Level) {
                    return toJUL((java.lang.System.Logger.Level)level);
                }
                return (Level)level;
            }
        }

        final JULBackendAdaptor julAdaptor = new JULBackendAdaptor();

        @Override
        BackendAdaptor adaptor() {
            return julAdaptor;
        }

    }

    public abstract static class BackendTesterFactory {
        public abstract BackendTester createBackendTester(boolean isSystem);
        public abstract  BackendTester createBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo);
        public abstract  BackendTester createBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo,
                ResourceBundle bundle);
        public abstract  BackendTester createBackendTester(boolean isSystem,
                ResourceBundle bundle);
    }

    public static class JULBackendTesterFactory extends BackendTesterFactory {

        @Override
        public BackendTester createBackendTester(boolean isSystem) {
            return new JULBackendTester(isSystem);
        }

        @Override
        public BackendTester createBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo) {
            return new JULBackendTester(isSystem, restrictedTo);
        }

        @Override
        public BackendTester createBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo,
                ResourceBundle bundle) {
            return new JULBackendTester(isSystem, restrictedTo, bundle);
        }

        @Override
        public BackendTester createBackendTester(boolean isSystem,
                ResourceBundle bundle) {
            return new JULBackendTester(isSystem, bundle);
        }
    }

    public static class CustomLoggerFinder extends LoggerFinder {

        static enum CustomLevel { OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL };
        static CustomLevel[] customLevelMap = { CustomLevel.ALL,
            CustomLevel.TRACE, CustomLevel.DEBUG, CustomLevel.INFO,
            CustomLevel.WARN, CustomLevel.ERROR, CustomLevel.OFF
        };
        static class CustomLogRecord {
            public final CustomLevel logLevel;
            public final java.lang.System.Logger logger;
            public final String msg;
            public final Object[] params;
            public final Throwable thrown;
            public final ResourceBundle bundle;

            CustomLogRecord(java.lang.System.Logger producer,
                    CustomLevel level, String msg) {
                this(producer, level, msg, (ResourceBundle)null, (Throwable)null, (Object[])null);
            }

            CustomLogRecord(java.lang.System.Logger producer,
                    CustomLevel level, String msg, ResourceBundle bundle,
                    Throwable thrown, Object... params) {
                this.logger   = producer;
                this.logLevel = level;
                this.msg = msg;
                this.params = params;
                this.thrown = thrown;
                this.bundle = bundle;
            }
        }

        static final List<CustomLogRecord>  records =
                Collections.synchronizedList(new ArrayList<>());

        static class CustomLogger implements java.lang.System.Logger {

            final String name;
            volatile CustomLevel level;
            CustomLogger(String name) {
                this.name = name;
                this.level = CustomLevel.INFO;
            }

            @Override
            public String getName() {
                return name;
            }

            public void setLevel(CustomLevel level) {
                this.level = level;
            }


            @Override
            public boolean isLoggable(java.lang.System.Logger.Level level) {

                return this.level != CustomLevel.OFF && this.level.ordinal()
                        >= customLevelMap[level.ordinal()].ordinal();
            }

            @Override
            public void log(java.lang.System.Logger.Level level, ResourceBundle bundle, String key, Throwable thrown) {
                if (isLoggable(level)) {
                    records.add(new CustomLogRecord(this, customLevelMap[level.ordinal()],
                              key, bundle, thrown));
                }
            }

            @Override
            public void log(java.lang.System.Logger.Level level, ResourceBundle bundle, String format, Object... params) {
                if (isLoggable(level)) {
                    records.add(new CustomLogRecord(this, customLevelMap[level.ordinal()],
                              format, bundle, null, params));
                }
            }

        }

        final Map<String, java.lang.System.Logger> applicationLoggers =
                Collections.synchronizedMap(new HashMap<>());
        final Map<String, java.lang.System.Logger> systemLoggers =
                Collections.synchronizedMap(new HashMap<>());

        @Override
        public java.lang.System.Logger getLogger(String name, Module caller) {
            ClassLoader callerLoader = caller.getClassLoader();
            if (callerLoader == null) {
                systemLoggers.putIfAbsent(name, new CustomLogger(name));
                return systemLoggers.get(name);
            } else {
                applicationLoggers.putIfAbsent(name, new CustomLogger(name));
                return applicationLoggers.get(name);
            }
        }

        CustomLevel fromJul(Level level) {
            if (level.intValue() == Level.OFF.intValue()) {
                return CustomLevel.OFF;
            } else if (level.intValue() > Level.SEVERE.intValue()) {
                return CustomLevel.ERROR;
            } else if (level.intValue() > Level.WARNING.intValue()) {
                return CustomLevel.ERROR;
            } else if (level.intValue() > Level.INFO.intValue()) {
                return CustomLevel.WARN;
            } else if (level.intValue() > Level.CONFIG.intValue()) {
                return CustomLevel.INFO;
            } else if (level.intValue() > Level.FINER.intValue()) {
                return CustomLevel.DEBUG;
            } else if (level.intValue() > Level.FINEST.intValue()) {
                return CustomLevel.TRACE;
            } else if (level.intValue() == Level.ALL.intValue()) {
                return CustomLevel.ALL;
            } else {
                return CustomLevel.TRACE;
            }
        }

        Level toJul(CustomLevel level) {
            switch(level) {
                case OFF: return Level.OFF;
                case FATAL: return Level.SEVERE;
                case ERROR: return Level.SEVERE;
                case WARN: return Level.WARNING;
                case INFO: return Level.INFO;
                case DEBUG: return Level.FINE;
                case TRACE: return Level.FINER;
                case ALL: return Level.ALL;
                default: throw new InternalError("No such level: "+level);
            }
        }

    }

    public static class CustomBackendTester extends
            BackendTester<CustomLoggerFinder.CustomLogRecord> {

        public final CustomLoggerFinder provider;

        public CustomBackendTester(boolean isSystem) {
            this(isSystem, null, null);
        }

        public CustomBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo) {
            this(isSystem, restrictedTo, null);
        }

        public CustomBackendTester(boolean isSystem,
                ResourceBundle localized) {
            this(isSystem, null, localized);
        }

        public CustomBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo,
                ResourceBundle localized) {
            super(isSystem, restrictedTo, localized);
            provider = (CustomLoggerFinder)java.lang.System.LoggerFinder.getLoggerFinder();
        }

        @Override
        public java.lang.System.Logger convert(java.lang.System.Logger logger) {
            if (restrictedTo != null && restrictedTo.isInstance(logger)) {
                return logger;
            } else if (restrictedTo == jdkLoggerClass) {
                return logger;
            } else {
                return java.lang.System.Logger.class.cast(
                        sun.util.logging.PlatformLogger.Bridge.convert(logger));
            }
        }

        class CustomBackendAdaptor extends BackendAdaptor {

            @Override
            public String getLoggerName(CustomLoggerFinder.CustomLogRecord res) {
                return res.logger.getName();
            }

            @Override
            public CustomLoggerFinder.CustomLevel getLevel(CustomLoggerFinder.CustomLogRecord res) {
                return res.logLevel;
            }

            @Override
            public String getMessage(CustomLoggerFinder.CustomLogRecord res) {
                return res.msg;
            }

            @Override // we don't support source class name in our custom provider implementation
            public String getSourceClassName(CustomLoggerFinder.CustomLogRecord res) {
                return null;
            }

            @Override // we don't support source method name in our custom provider implementation
            public String getSourceMethodName(CustomLoggerFinder.CustomLogRecord res) {
                return null;
            }

            @Override
            public Throwable getThrown(CustomLoggerFinder.CustomLogRecord res) {
                return res.thrown;
            }

            @Override
            public ResourceBundle getResourceBundle(CustomLoggerFinder.CustomLogRecord res) {
                return res.bundle;
            }

            @Override
            public void setLevel(java.lang.System.Logger logger, Level level) {
                final CustomLoggerFinder.CustomLogger l =
                        (CustomLoggerFinder.CustomLogger)
                        (isSystem ? provider.getLogger(logger.getName(), Thread.class.getModule()) :
                        provider.getLogger(logger.getName(), LoggerFinderBackendTest.class.getModule()));
                l.setLevel(provider.fromJul(level));
            }
            @Override
            public void setLevel(java.lang.System.Logger logger,
                    java.lang.System.Logger.Level level) {
                setLevel(logger, toJUL(level));
            }

            CustomLoggerFinder.CustomLevel getLevel(java.lang.System.Logger logger) {
                final CustomLoggerFinder.CustomLogger l =
                        (CustomLoggerFinder.CustomLogger)
                        (isSystem ? provider.getLogger(logger.getName(), Thread.class.getModule()) :
                        provider.getLogger(logger.getName(), LoggerFinderBackendTest.class.getModule()));
                return l.level;
            }

            @Override
            public List<CustomLoggerFinder.CustomLogRecord> getBackendRecords() {
                return CustomLoggerFinder.records;
            }

            @Override
            public void resetBackendRecords() {
                CustomLoggerFinder.records.clear();
            }

            @Override
            public boolean shouldBeLoggable(Levels level, Level loggerLevel) {
                return loggerLevel != Level.OFF &&
                       fromLevels(level).ordinal() <= provider.fromJul(loggerLevel).ordinal();
            }

            @Override
            public boolean isLoggable(java.lang.System.Logger logger, Level l) {
                return super.isLoggable(logger, l);
            }

            @Override
            public boolean shouldBeLoggable(Level logLevel, Level loggerLevel) {
                return loggerLevel != Level.OFF &&
                        provider.fromJul(logLevel).ordinal() <= provider.fromJul(loggerLevel).ordinal();
            }

            @Override // we don't support source class name in our custom provider implementation
            public String getCallerClassName(Levels level, String clazz) {
                return null;
            }

            @Override // we don't support source method name in our custom provider implementation
            public String getCallerMethodName(Levels level, String method) {
                return null;
            }

            @Override // we don't support source class name in our custom provider implementation
            public String getCallerClassName(MethodInvoker<?,?> logMethod, String clazz) {
                return null;
            }

            @Override // we don't support source method name in our custom provider implementation
            public String getCallerMethodName(MethodInvoker<?,?> logMethod, String method) {
                return null;
            }

            @Override
            public CustomLoggerFinder.CustomLevel getMappedLevel(Object level) {
                if (level instanceof java.lang.System.Logger.Level) {
                    final int index = ((java.lang.System.Logger.Level)level).ordinal();
                    return CustomLoggerFinder.customLevelMap[index];
                } else if (level instanceof Level) {
                    return provider.fromJul((Level)level);
                }
                return (CustomLoggerFinder.CustomLevel) level;
            }

            CustomLoggerFinder.CustomLevel fromLevels(Levels level) {
                switch(level) {
                    case SEVERE:
                        return CustomLoggerFinder.CustomLevel.ERROR;
                    case WARNING:
                        return CustomLoggerFinder.CustomLevel.WARN;
                    case INFO:
                        return CustomLoggerFinder.CustomLevel.INFO;
                    case CONFIG: case FINE:
                        return CustomLoggerFinder.CustomLevel.DEBUG;
                    case FINER:  case FINEST:
                        return CustomLoggerFinder.CustomLevel.TRACE;
                }
                throw new InternalError("No such level "+level);
            }

        }

        @Override
        BackendAdaptor adaptor() {
            return new CustomBackendAdaptor();
        }

    }

    public static class CustomBackendTesterFactory extends BackendTesterFactory {

        @Override
        public BackendTester createBackendTester(boolean isSystem) {
            return new CustomBackendTester(isSystem);
        }

        @Override
        public BackendTester createBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo) {
            return new CustomBackendTester(isSystem, restrictedTo);
        }

        @Override
        public BackendTester createBackendTester(boolean isSystem,
                Class<? extends java.lang.System.Logger> restrictedTo,
                ResourceBundle bundle) {
            return new CustomBackendTester(isSystem, restrictedTo, bundle);
        }

        @Override
        public BackendTester createBackendTester(boolean isSystem,
                ResourceBundle bundle) {
            return new CustomBackendTester(isSystem, bundle);
        }
    }

    static final Method getLazyLogger;
    static final Method accessLoggerFinder;
    static {
        // jdk.internal.logger.LazyLoggers.getLazyLogger(name, caller);
        try {
            Class<?> lazyLoggers = jdk.internal.logger.LazyLoggers.class;
            getLazyLogger = lazyLoggers.getMethod("getLazyLogger",
                    String.class, Module.class);
            getLazyLogger.setAccessible(true);
            Class<?> loggerFinderLoader =
                    Class.forName("java.lang.System$LoggerFinder");
            accessLoggerFinder = loggerFinderLoader.getDeclaredMethod("accessProvider");
            accessLoggerFinder.setAccessible(true);
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }

    static java.lang.System.Logger getSystemLogger(String name, Module caller) throws Exception {
        try {
            return java.lang.System.Logger.class.cast(getLazyLogger.invoke(null, name, caller));
        } catch (InvocationTargetException x) {
            Throwable t = x.getTargetException();
            if (t instanceof Exception) {
                throw (Exception)t;
            } else {
                throw (Error)t;
            }
        }
    }
    static java.lang.System.Logger getSystemLogger(String name,
            ResourceBundle bundle, Module caller) throws Exception {
        try {
            LoggerFinder provider = LoggerFinder.class.cast(accessLoggerFinder.invoke(null));
            return provider.getLocalizedLogger(name, bundle, caller);
        } catch (InvocationTargetException x) {
            Throwable t = x.getTargetException();
            if (t instanceof Exception) {
                throw (Exception)t;
            } else {
                throw (Error)t;
            }
        }
    }

    // Change this to 'true' to get more traces...
    public static boolean verbose = false;

    public static void main(String[] argv) throws Exception {

        final AtomicInteger nb = new AtomicInteger(0);
        final boolean hidesProvider = Boolean.getBoolean("test.logger.hidesProvider");
        System.out.println(ClassLoader.getSystemClassLoader());
        final BackendTesterFactory factory;
        if (java.lang.System.LoggerFinder.getLoggerFinder() instanceof CustomLoggerFinder) {
            if (hidesProvider) {
                System.err.println("Custom backend "
                        + java.lang.System.LoggerFinder.getLoggerFinder()
                        + " should have been hidden!");
                throw new RuntimeException(
                        "Custom backend should have been hidden: "
                        + "check value of java.system.class.loader property");
            }
            System.out.println("Using custom backend");
            factory = new CustomBackendTesterFactory();
        } else {
            if (!hidesProvider) {
                System.err.println("Default JUL backend "
                        + java.lang.System.LoggerFinder.getLoggerFinder()
                        + " should have been hidden!");
                throw new RuntimeException(
                        "Default JUL backend should have been hidden: "
                        + "check value of java.system.class.loader property");
            }
            System.out.println("Using JUL backend");
            factory = new JULBackendTesterFactory();
        }

        testBackend(nb, factory);
    }

    public static void testBackend(AtomicInteger nb, BackendTesterFactory factory) throws Exception {

        // Tests all level specifics methods with loggers configured with
        // all possible levels and loggers obtained with all possible
        // entry points from LoggerFactory and JdkLoggerFactory, with
        // JUL as backend.

        // Test a simple application logger with JUL backend
        final BackendTester tester = factory.createBackendTester(false);
        final java.lang.System.Logger logger =
                java.lang.System.LoggerFinder.getLoggerFinder()
                        .getLogger("foo", LoggerFinderBackendTest.class.getModule());

        testLogger(tester, logger, nb);

        // Test a simple system logger with JUL backend
        final java.lang.System.Logger system =
                java.lang.System.LoggerFinder.getLoggerFinder()
                        .getLogger("bar", Thread.class.getModule());
        final BackendTester systemTester = factory.createBackendTester(true);
        testLogger(systemTester, system, nb);

        // Test a localized application logger with null resource bundle and
        // JUL backend
        final java.lang.System.Logger noBundleLogger =
                java.lang.System.LoggerFinder.getLoggerFinder()
                        .getLocalizedLogger("baz", null, LoggerFinderBackendTest.class.getModule());
        final BackendTester noBundleTester =
                factory.createBackendTester(false, spiLoggerClass);
        testLogger(noBundleTester, noBundleLogger, nb);

        // Test a localized system logger with null resource bundle and JUL
        // backend
        final java.lang.System.Logger noBundleSysLogger =
                java.lang.System.LoggerFinder.getLoggerFinder()
                        .getLocalizedLogger("oof", null, Thread.class.getModule());
        final BackendTester noBundleSysTester =
                factory.createBackendTester(true, spiLoggerClass);
        testLogger(noBundleSysTester, noBundleSysLogger, nb);

        // Test a localized application logger with null resource bundle and
        // JUL backend
        try {
            System.getLogger("baz", null);
            throw new RuntimeException("Expected NullPointerException not thrown");
        } catch (NullPointerException x) {
            System.out.println("System.Loggers.getLogger(\"baz\", null): got expected " + x);
        }
        final java.lang.System.Logger noBundleExtensionLogger =
                getSystemLogger("baz", null, LoggerFinderBackendTest.class.getModule());
        final BackendTester noBundleExtensionTester =
                factory.createBackendTester(false, jdkLoggerClass);
        testLogger(noBundleExtensionTester, noBundleExtensionLogger, nb);

        // Test a simple system logger with JUL backend
        final java.lang.System.Logger sysExtensionLogger =
                getSystemLogger("oof", Thread.class.getModule());
        final BackendTester sysExtensionTester =
                factory.createBackendTester(true, jdkLoggerClass);
        testLogger(sysExtensionTester, sysExtensionLogger, nb);

        // Test a localized system logger with null resource bundle and JUL
        // backend
        final java.lang.System.Logger noBundleSysExtensionLogger =
                getSystemLogger("oof", null, Thread.class.getModule());
        final BackendTester noBundleSysExtensionTester =
                factory.createBackendTester(true, jdkLoggerClass);
        testLogger(noBundleSysExtensionTester, noBundleSysExtensionLogger, nb);

        // Test a localized application logger converted to JDK with null
        // resource bundle and JUL backend
        final java.lang.System.Logger noBundleConvertedLogger =
                (java.lang.System.Logger)
                sun.util.logging.PlatformLogger.Bridge.convert(noBundleLogger);
        final BackendTester noBundleJdkTester = factory.createBackendTester(false);
        testLogger(noBundleJdkTester, noBundleConvertedLogger, nb);

        // Test a localized system logger converted to JDK with null resource
        // bundle and JUL backend
        final java.lang.System.Logger noBundleConvertedSysLogger =
                (java.lang.System.Logger)
                sun.util.logging.PlatformLogger.Bridge.convert(noBundleSysLogger);
        final BackendTester noBundleJdkSysTester = factory.createBackendTester(true);
        testLogger(noBundleJdkSysTester, noBundleConvertedSysLogger, nb);

        // Test a localized application logger with resource bundle and JUL
        // backend
        final ResourceBundle bundle =
                ResourceBundle.getBundle(ResourceBundeLocalized.class.getName());
        final java.lang.System.Logger bundleLogger =
                java.lang.System.LoggerFinder.getLoggerFinder()
                        .getLocalizedLogger("toto", bundle, LoggerFinderBackendTest.class.getModule());
        final BackendTester bundleTester =
                factory.createBackendTester(false, spiLoggerClass, bundle);
        testLogger(bundleTester, bundleLogger, nb);

        // Test a localized system logger with resource bundle and JUL backend
        final java.lang.System.Logger bundleSysLogger =
                java.lang.System.LoggerFinder.getLoggerFinder()
                        .getLocalizedLogger("titi", bundle, Thread.class.getModule());
        final BackendTester bundleSysTester =
                factory.createBackendTester(true, spiLoggerClass, bundle);
        testLogger(bundleSysTester, bundleSysLogger, nb);

        // Test a localized Jdk application logger with resource bundle and JUL
        // backend
        final java.lang.System.Logger bundleExtensionLogger =
                System.getLogger("tita", bundle);
        final BackendTester bundleExtensionTester =
                factory.createBackendTester(false, jdkLoggerClass, bundle);
        testLogger(bundleExtensionTester, bundleExtensionLogger, nb);

        // Test a localized Jdk system logger with resource bundle and JUL
        // backend
        final java.lang.System.Logger bundleExtensionSysLogger =
                getSystemLogger("titu", bundle, Thread.class.getModule());
        final BackendTester bundleExtensionSysTester =
                factory.createBackendTester(true, jdkLoggerClass, bundle);
        testLogger(bundleExtensionSysTester, bundleExtensionSysLogger, nb);

        // Test a localized application logger converted to JDK with resource
        // bundle and JUL backend
        final BackendTester bundleJdkTester =
                factory.createBackendTester(false, bundle);
        final java.lang.System.Logger bundleConvertedLogger =
                (java.lang.System.Logger)
                sun.util.logging.PlatformLogger.Bridge.convert(bundleLogger);
        testLogger(bundleJdkTester, bundleConvertedLogger, nb);

        // Test a localized Jdk system logger converted to JDK with resource
        // bundle and JUL backend
        final BackendTester bundleJdkSysTester =
                factory.createBackendTester(true, bundle);
        final java.lang.System.Logger bundleConvertedSysLogger =
                (java.lang.System.Logger)
                sun.util.logging.PlatformLogger.Bridge.convert(bundleSysLogger);
        testLogger(bundleJdkSysTester, bundleConvertedSysLogger, nb);

        // Now need to add tests for all the log/logp/logrb methods...

    }

    private static class FooObj {
        final String s;
        FooObj(String s) {
            this.s = s;
        }

        @Override
        public String toString() {
            return super.toString() +": "+s;
        }

    }

    public static void testLogger(BackendTester tester,
            java.lang.System.Logger spiLogger, AtomicInteger nb) {

        // Test all level-specific method forms:
        // fatal(...) error(...) severe(...) etc...
        java.lang.System.Logger jdkLogger = tester.convert(spiLogger);
        for (Levels l : Levels.values()) {
            java.lang.System.Logger logger =
                    l.definingClass.equals(spiLoggerClass) ? spiLogger : jdkLogger;
            tester.testLevel(l, logger, l.method + "[" + logger.getName()+ "]-"
                    + nb.incrementAndGet());
            tester.testLevel(l, logger, l.method + "[" + logger.getName()+ "]-"
                    + nb.incrementAndGet(),
                    bundleParam);
            final int nbb = nb.incrementAndGet();
            tester.testLevel(l, logger, () -> l.method + "[" + logger.getName()
                    + "]-" + nbb);
        }
        for (Levels l : Levels.values()) {
            java.lang.System.Logger logger =
                    l.definingClass.equals(spiLoggerClass) ? spiLogger : jdkLogger;
            tester.testLevel(l, logger,
                    l.method + "[" + logger.getName()+ "]({0},{1})-"
                    + nb.incrementAndGet(),
                    "One", "Two");
            tester.testLevel(l, logger,
                    l.method + "[" + logger.getName()+ "]({0},{1})-"
                    + nb.incrementAndGet(),
                    bundleParam, "One", "Two");
        }
        final Throwable thrown = new RuntimeException("Test");
        for (Levels l : Levels.values()) {
            java.lang.System.Logger logger =
                    l.definingClass.equals(spiLoggerClass) ? spiLogger : jdkLogger;
            tester.testLevel(l, logger, l.method + "[" + logger.getName()+ "]-"
                    + nb.incrementAndGet(),
                    thrown);
            tester.testLevel(l, logger, l.method + "[" + logger.getName()+ "]-"
                    + nb.incrementAndGet(),
                    bundleParam, thrown);
            final int nbb = nb.incrementAndGet();
            tester.testLevel(l, logger, ()->l.method + "[" + logger.getName()+ "]-"
                    + nbb, thrown);
        }

        java.lang.System.Logger logger = jdkLogger;

         // test System.Logger methods
       tester.testSpiLog(logger, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet());
        tester.testSpiLog(logger, bundleParam, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet());
        tester.testSpiLog(logger, "[" + logger.getName()+ "]-({0},{1})"
                + nb.incrementAndGet(), "One", "Two");
        tester.testSpiLog(logger, bundleParam, "[" + logger.getName()+ "]-({0},{1})"
                + nb.incrementAndGet(), "One", "Two");
        tester.testSpiLog(logger, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet(), thrown);
        tester.testSpiLog(logger, bundleParam, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet(), thrown);
        final int nbb01 = nb.incrementAndGet();
        tester.testSpiLog(logger, () -> "[" + logger.getName()+ "]-" + nbb01);
        final int nbb02 = nb.incrementAndGet();
        tester.testSpiLog(logger, thrown, () -> "[" + logger.getName()+ "]-" + nbb02);
        final int nbb03 = nb.incrementAndGet();
        tester.testSpiLog(logger, new FooObj("[" + logger.getName()+ "]-" + nbb03));

        // Test all log method forms:
        // jdk.internal.logging.Logger.log(...)
        tester.testLog(logger, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet());
        tester.testLogrb(logger, bundleParam, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet());
        tester.testLog(logger, "[" + logger.getName()+ "]-({0},{1})"
                + nb.incrementAndGet(), "One", "Two");
        tester.testLogrb(logger, bundleParam, "[" + logger.getName()+ "]-({0},{1})"
                + nb.incrementAndGet(), "One", "Two");
        tester.testLog(logger, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet(), thrown);
        tester.testLogrb(logger, bundleParam, "[" + logger.getName()+ "]-"
                + nb.incrementAndGet(), thrown);
        final int nbb1 = nb.incrementAndGet();
        tester.testLog(logger, () -> "[" + logger.getName()+ "]-" + nbb1);
        final int nbb2 = nb.incrementAndGet();
        tester.testLog(logger, thrown, () -> "[" + logger.getName()+ "]-" + nbb2);

        // Test all logp method forms
        // jdk.internal.logging.Logger.logp(...)
        tester.testLogp(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(),
                "[" + logger.getName()+ "]-"
                + nb.incrementAndGet());
        tester.testLogrb(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(), bundleParam,
                "[" + logger.getName()+ "]-"
                + nb.incrementAndGet());
        tester.testLogp(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(),
                "[" + logger.getName()+ "]-({0},{1})"
                + nb.incrementAndGet(), "One", "Two");
        tester.testLogrb(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(), bundleParam,
                "[" + logger.getName()+ "]-({0},{1})"
                + nb.incrementAndGet(), "One", "Two");
        tester.testLogp(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(),
                "[" + logger.getName()+ "]-"
                + nb.incrementAndGet(), thrown);
        tester.testLogrb(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(), bundleParam,
                "[" + logger.getName()+ "]-"
                + nb.incrementAndGet(), thrown);
        final int nbb3 = nb.incrementAndGet();
        tester.testLogp(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(),
                () -> "[" + logger.getName()+ "]-" + nbb3);
        final int nbb4 = nb.incrementAndGet();
        tester.testLogp(logger, "clazz" + nb.incrementAndGet(),
                "method" + nb.incrementAndGet(),
                thrown, () -> "[" + logger.getName()+ "]-" + nbb4);
    }

}