diff --git a/build.gradle b/build.gradle index 5b33f3f..e0271a2 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ dependencies { testCompileOnly("org.jetbrains:annotations:${jetbrains_ann_version}") testImplementation("org.junit.jupiter:junit-jupiter-api:${junit_version}") testImplementation("org.junit.jupiter:junit-jupiter-engine:${junit_version}") + testImplementation("org.junit.jupiter:junit-jupiter-params:${junit_version}") testImplementation("org.powermock:powermock-core:${powermock_version}") testImplementation("org.powermock:powermock-reflect:${powermock_version}") testRuntimeOnly("org.apiguardian:apiguardian-api:${apiguardian_version}") @@ -92,7 +93,7 @@ test { useJUnitPlatform() systemProperty 'testJars.location', testsJar.archiveFile.get().asFile - + jvmArgs( '--module-path', classpath.asPath, '--add-modules', 'ALL-MODULE-PATH', diff --git a/src/main/java/cpw/mods/modlauncher/ArgumentHandler.java b/src/main/java/cpw/mods/modlauncher/ArgumentHandler.java index b8496be..98872b2 100644 --- a/src/main/java/cpw/mods/modlauncher/ArgumentHandler.java +++ b/src/main/java/cpw/mods/modlauncher/ArgumentHandler.java @@ -19,10 +19,13 @@ package cpw.mods.modlauncher; import cpw.mods.modlauncher.api.*; +import cpw.mods.modlauncher.util.UriConverter; import joptsimple.*; import joptsimple.util.*; import org.jetbrains.annotations.NotNull; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.*; import java.util.*; import java.util.function.*; @@ -37,17 +40,19 @@ public class ArgumentHandler { private OptionSpec nonOption; private OptionSpec launchTarget; private OptionSpec uuidOption; + private OptionSpec loggingConfigOption; - record DiscoveryData(Path gameDir, String launchTarget, String[] arguments) {} + record DiscoveryData(Path gameDir, String launchTarget, List loggingConfigs, String[] arguments) {} DiscoveryData setArgs(String[] args) { this.args = args; final OptionParser parser = new OptionParser(); final var gameDir = parser.accepts("gameDir", "Alternative game directory").withRequiredArg().withValuesConvertedBy(new PathConverter(PathProperties.DIRECTORY_EXISTING)).defaultsTo(Path.of(".")); final var launchTarget = parser.accepts("launchTarget", "LauncherService target to launch").withRequiredArg(); + final var loggingConfig = parser.accepts("loggingConfig", "Log4j configuration files to composite together").withOptionalArg().withValuesConvertedBy(new UriConverter()).withValuesSeparatedBy(','); parser.allowsUnrecognizedOptions(); final OptionSet optionSet = parser.parse(args); - return new DiscoveryData(optionSet.valueOf(gameDir), optionSet.valueOf(launchTarget), args); + return new DiscoveryData(optionSet.valueOf(gameDir), optionSet.valueOf(launchTarget), optionSet.valuesOf(loggingConfig), args); } void processArguments(Environment env, Consumer parserConsumer, BiConsumer> resultConsumer) { @@ -59,6 +64,7 @@ void processArguments(Environment env, Consumer parserConsumer, Bi minecraftJarOption = parser.accepts("minecraftJar", "Path to minecraft jar").withRequiredArg().withValuesConvertedBy(new PathConverter(PathProperties.READABLE)).withValuesSeparatedBy(','); uuidOption = parser.accepts("uuid", "The UUID of the logging in player").withRequiredArg(); launchTarget = parser.accepts("launchTarget", "LauncherService target to launch").withRequiredArg(); + loggingConfigOption = parser.accepts("loggingConfig", "Log4j configuration files to composite together").withOptionalArg().withValuesConvertedBy(new UriConverter()).withValuesSeparatedBy(','); parserConsumer.accept(parser); nonOption = parser.nonOptions(); diff --git a/src/main/java/cpw/mods/modlauncher/Launcher.java b/src/main/java/cpw/mods/modlauncher/Launcher.java index 464cde3..42c23b5 100644 --- a/src/main/java/cpw/mods/modlauncher/Launcher.java +++ b/src/main/java/cpw/mods/modlauncher/Launcher.java @@ -20,9 +20,19 @@ import cpw.mods.jarhandling.SecureJar; import cpw.mods.modlauncher.api.*; +import cpw.mods.modlauncher.log.MarkerLogLevelFilter; +import cpw.mods.modlauncher.util.LoggingUtils; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.*; +import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory; +import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; +import org.apache.logging.log4j.core.filter.MarkerFilter; +import java.net.URI; import java.nio.file.Path; import java.util.*; import java.util.function.BiFunction; @@ -58,6 +68,7 @@ private Launcher() { environment.computePropertyIfAbsent(IEnvironment.Keys.MLIMPL_VERSION.get(), s->IEnvironment.class.getPackage().getImplementationVersion()); environment.computePropertyIfAbsent(IEnvironment.Keys.MODLIST.get(), s->new ArrayList<>()); environment.computePropertyIfAbsent(IEnvironment.Keys.SECURED_JARS_ENABLED.get(), k-> ProtectionDomainHelper.canHandleSecuredJars()); + environment.computePropertyIfAbsent(IEnvironment.Keys.LOGGING_CONFIG.get(), k -> new ArrayList<>(LoggingUtils.getConfigurationSources(this.moduleLayerHandler.getLayer(IModuleLayerManager.Layer.BOOT).orElseThrow()))); this.transformStore = new TransformStore(); this.transformationServicesHandler = new TransformationServicesHandler(this.transformStore, this.moduleLayerHandler); this.argumentHandler = new ArgumentHandler(); @@ -74,6 +85,7 @@ public static void main(String... args) { JVM information: %s %s %s """, props.getProperty("java.vm.vendor"), props.getProperty("java.vm.name"), props.getProperty("java.vm.version")); } + LogManager.getLogger().info(MODLAUNCHER,"ModLauncher running: args {}", () -> LaunchServiceHandler.hideAccessToken(args)); LogManager.getLogger().info(MODLAUNCHER, "JVM identified as {} {} {}", props.getProperty("java.vm.vendor"), props.getProperty("java.vm.name"), props.getProperty("java.vm.version")); new Launcher().run(args); @@ -85,14 +97,19 @@ public final TypesafeMap blackboard() { private void run(String... args) { final ArgumentHandler.DiscoveryData discoveryData = this.argumentHandler.setArgs(args); + reconfigureLogger(discoveryData.loggingConfigs()); this.transformationServicesHandler.discoverServices(discoveryData); + reconfigureLogger(discoveryData.loggingConfigs()); final var scanResults = this.transformationServicesHandler.initializeTransformationServices(this.argumentHandler, this.environment, this.nameMappingServiceHandler) .stream().collect(Collectors.groupingBy(ITransformationService.Resource::target)); scanResults.getOrDefault(IModuleLayerManager.Layer.PLUGIN, List.of()) .stream() .mapMulti((resource, action) -> resource.resources().forEach(action)) .forEach(np->this.moduleLayerHandler.addToLayer(IModuleLayerManager.Layer.PLUGIN, np)); - this.moduleLayerHandler.buildLayer(IModuleLayerManager.Layer.PLUGIN); + final var pluginLayer = this.moduleLayerHandler.buildLayer(IModuleLayerManager.Layer.PLUGIN); + var sources = LoggingUtils.getConfigurationSources(pluginLayer.layer()); + this.environment.getProperty(IEnvironment.Keys.LOGGING_CONFIG.get()).ifPresent(lc -> lc.addAll(sources)); + reconfigureLogger(discoveryData.loggingConfigs()); final var gameResults = this.transformationServicesHandler.triggerScanCompletion(this.moduleLayerHandler) .stream().collect(Collectors.groupingBy(ITransformationService.Resource::target)); final var gameContents = Stream.of(scanResults, gameResults) @@ -105,10 +122,68 @@ private void run(String... args) { this.launchService.validateLaunchTarget(this.argumentHandler); final TransformingClassLoaderBuilder classLoaderBuilder = this.launchService.identifyTransformationTargets(this.argumentHandler); this.classLoader = this.transformationServicesHandler.buildTransformingClassLoader(this.launchPlugins, classLoaderBuilder, this.environment, this.moduleLayerHandler); + reconfigureLogger(discoveryData.loggingConfigs()); Thread.currentThread().setContextClassLoader(this.classLoader); this.launchService.launch(this.argumentHandler, this.moduleLayerHandler.getLayer(IModuleLayerManager.Layer.GAME).orElseThrow(), this.classLoader, this.launchPlugins); } + private void reconfigureLogger(List additionalConfigurationFiles) { + final var configurations = this.environment.getProperty(IEnvironment.Keys.LOGGING_CONFIG.get()).orElseThrow().stream() + .map(ConfigurationSource::fromUri) + .map(source -> ConfigurationFactory.getInstance().getConfiguration(LoggerContext.getContext(), source)) + .map(AbstractConfiguration.class::cast) + .collect(Collectors.toList()); + + additionalConfigurationFiles.stream() + .map(ConfigurationSource::fromUri) + .map(source -> ConfigurationFactory.getInstance().getConfiguration(LoggerContext.getContext(), source)) + .map(AbstractConfiguration.class::cast) + .forEach(configurations::add); + + final var levelConfigBuilder = ConfigurationBuilderFactory.newConfigurationBuilder() + .setConfigurationName("MODLAUNCHER-LOGLEVELS"); + System.getProperties().entrySet().stream() + .map(entry -> (Map.Entry)(Map.Entry)entry) + .filter(entry -> entry.getKey().startsWith("logging.loglevel.")) + .forEach(entry -> { + final var loggerName = entry.getKey().substring("logging.loglevel.".length()); + final var level = Level.getLevel(entry.getValue()); + if (loggerName.equals("default")) { + levelConfigBuilder.add(levelConfigBuilder.newRootLogger(level)); + } else { + levelConfigBuilder.add(levelConfigBuilder.newLogger(loggerName, level)); + } + }); + + final var markerConfigBuilder = ConfigurationBuilderFactory.newConfigurationBuilder() + .setConfigurationName("MODLAUNCHER-MARKERS"); + System.getProperties().entrySet().stream() + .map(entry -> (Map.Entry)(Map.Entry)entry) + .filter(entry -> entry.getKey().startsWith("logging.marker.")) + .forEach(entry -> { + final var markerName = entry.getKey().substring("logging.marker.".length()); + final var minimumLevel = Level.getLevel(entry.getValue()); + markerConfigBuilder.add(markerConfigBuilder.newFilter("MarkerLogLevelFilter", Filter.Result.ACCEPT, Filter.Result.DENY) + .addAttribute(MarkerLogLevelFilter.ATTR_MARKER, markerName) + .addAttribute(MarkerLogLevelFilter.ATTR_MINIMUM_LEVEL, minimumLevel)); + }); + + // These are the default markers; they have to be specified at the very end so that they have the lowest priority. + markerConfigBuilder.add(markerConfigBuilder.newFilter("MarkerFilter", Filter.Result.DENY, Filter.Result.NEUTRAL) + .addAttribute(MarkerFilter.ATTR_MARKER, "NETWORK_PACKETS")) + .add(markerConfigBuilder.newFilter("MarkerFilter", Filter.Result.DENY, Filter.Result.NEUTRAL) + .addAttribute(MarkerFilter.ATTR_MARKER, "CLASSLOADING")) + .add(markerConfigBuilder.newFilter("MarkerFilter", Filter.Result.DENY, Filter.Result.NEUTRAL) + .addAttribute(MarkerFilter.ATTR_MARKER, "LAUNCHPLUGIN")) + .add(markerConfigBuilder.newFilter("MarkerFilter", Filter.Result.DENY, Filter.Result.NEUTRAL) + .addAttribute(MarkerFilter.ATTR_MARKER, "CLASSDUMP")); + + configurations.add(levelConfigBuilder.build()); + configurations.add(markerConfigBuilder.build()); + Configurator.reconfigure(new CompositeConfiguration(configurations)); + LogManager.getLogger().trace(MODLAUNCHER, "Logger reconfigured from {} sources", configurations.size()); + } + public Environment environment() { return this.environment; } diff --git a/src/main/java/cpw/mods/modlauncher/TransformationServicesHandler.java b/src/main/java/cpw/mods/modlauncher/TransformationServicesHandler.java index 15694e4..59876de 100644 --- a/src/main/java/cpw/mods/modlauncher/TransformationServicesHandler.java +++ b/src/main/java/cpw/mods/modlauncher/TransformationServicesHandler.java @@ -20,6 +20,7 @@ import cpw.mods.modlauncher.api.*; import cpw.mods.modlauncher.serviceapi.ITransformerDiscoveryService; +import cpw.mods.modlauncher.util.LoggingUtils; import cpw.mods.modlauncher.util.ServiceLoaderUtils; import joptsimple.*; import org.apache.logging.log4j.LogManager; @@ -58,6 +59,8 @@ List initializeTransformationServices(ArgumentH TransformingClassLoader buildTransformingClassLoader(final LaunchPluginHandler pluginHandler, final TransformingClassLoaderBuilder builder, final Environment environment, final ModuleLayerHandler layerHandler) { final List>> classLocatorList = serviceLookup.values().stream().map(TransformationServiceDecorator::getClassLoader).filter(Objects::nonNull).collect(Collectors.toList()); final var layerInfo = layerHandler.buildLayer(IModuleLayerManager.Layer.GAME, (cf, parents)->new TransformingClassLoader(transformStore, pluginHandler, builder, environment, cf, parents)); + var sources = LoggingUtils.getConfigurationSources(layerInfo.layer()); + Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.LOGGING_CONFIG.get()).ifPresent(lc -> lc.addAll(sources)); layerHandler.updateLayer(IModuleLayerManager.Layer.PLUGIN, li->li.cl().setFallbackClassLoader(layerInfo.cl())); return (TransformingClassLoader) layerInfo.cl(); } @@ -128,6 +131,8 @@ void discoverServices(final ArgumentHandler.DiscoveryData discoveryData) { LOGGER.debug(MODLAUNCHER, "Found additional transformation services from discovery services: {}", ()->additionalPaths.stream().map(ap->Arrays.toString(ap.paths())).collect(Collectors.joining())); additionalPaths.forEach(np->layerHandler.addToLayer(IModuleLayerManager.Layer.SERVICE, np)); var serviceLayer = layerHandler.buildLayer(IModuleLayerManager.Layer.SERVICE); + var sources = LoggingUtils.getConfigurationSources(serviceLayer.layer()); + Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.LOGGING_CONFIG.get()).ifPresent(lc -> lc.addAll(sources)); earlyDiscoveryServices.forEach(s->s.earlyInitialization(discoveryData.launchTarget(), discoveryData.arguments())); serviceLookup = ServiceLoaderUtils.streamServiceLoader(()->ServiceLoader.load(serviceLayer.layer(), ITransformationService.class), sce -> LOGGER.fatal(MODLAUNCHER, "Encountered serious error loading transformation service, expect problems", sce)) .collect(Collectors.toMap(ITransformationService::name, TransformationServiceDecorator::new)); diff --git a/src/main/java/cpw/mods/modlauncher/api/IEnvironment.java b/src/main/java/cpw/mods/modlauncher/api/IEnvironment.java index bbdfe39..e9cf360 100644 --- a/src/main/java/cpw/mods/modlauncher/api/IEnvironment.java +++ b/src/main/java/cpw/mods/modlauncher/api/IEnvironment.java @@ -20,6 +20,7 @@ import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import java.net.URI; import java.nio.file.*; import java.util.*; import java.util.function.*; @@ -117,6 +118,11 @@ final class Keys { * True if we can compute secured JAR state. JVMs < 8.0.61 do not have this feature because reasons */ public static final Supplier> SECURED_JARS_ENABLED = buildKey("securedJarsEnabled", Boolean.class); + + /** + * Logging configurations to be composited together when configuring logging. + */ + public static final Supplier>> LOGGING_CONFIG = buildKey("loggingConfig", List.class); } diff --git a/src/main/java/cpw/mods/modlauncher/log/MarkerLogLevelFilter.java b/src/main/java/cpw/mods/modlauncher/log/MarkerLogLevelFilter.java new file mode 100644 index 0000000..b1ab5f5 --- /dev/null +++ b/src/main/java/cpw/mods/modlauncher/log/MarkerLogLevelFilter.java @@ -0,0 +1,136 @@ +package cpw.mods.modlauncher.log; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.util.PerformanceSensitive; + +@Plugin(name = "MarkerLogLevelFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true) +@PerformanceSensitive("allocation") +public class MarkerLogLevelFilter extends AbstractFilter { + public static final String ATTR_MARKER = "marker"; + public static final String ATTR_MINIMUM_LEVEL = "minimumLevel"; + + private final String marker; + private final String minimumLevel; + + protected MarkerLogLevelFilter(String marker, String minimumLevel, Result onMatch, Result onMismatch) { + super(onMatch, onMismatch); + this.marker = marker; + this.minimumLevel = minimumLevel; + } + + private Result filter(Marker marker, Level level) { + if (marker == null || level == null || !marker.isInstanceOf(this.marker)) + return Result.NEUTRAL; + + final var comparedLevel = Level.getLevel(this.minimumLevel); + if (comparedLevel.isLessSpecificThan(level)) + return onMatch; + + return onMismatch; + } + + @Override + public Result filter(LogEvent event) { + return filter(event.getMarker(), event.getLevel()); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object... params) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2, Object p3) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2, Object p3, Object p4) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8) { + return filter(marker, level); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9) { + return filter(marker, level); + } + + @Override + public String toString() { + return "MarkerLogLevelFilter{" + "marker='" + marker + '\'' + ", minimumLevel=" + minimumLevel + '}'; + } + + @PluginFactory + public static MarkerLogLevelFilter createFilter( + @PluginAttribute(ATTR_MARKER) final String marker, + @PluginAttribute(ATTR_MINIMUM_LEVEL) final String minLevel, + @PluginAttribute(AbstractFilterBuilder.ATTR_ON_MATCH) final Result match, + @PluginAttribute(AbstractFilterBuilder.ATTR_ON_MISMATCH) final Result mismatch) { + + if (marker == null) { + LOGGER.error("A marker must be provided for MarkerLogLevelFilter"); + return null; + } + + if (minLevel == null) { + LOGGER.error("A minimum level must be provided for MarkerLogLevelFilter"); + return null; + } + + return new MarkerLogLevelFilter(marker, minLevel, match, mismatch); + } +} diff --git a/src/main/java/cpw/mods/modlauncher/util/LoggingUtils.java b/src/main/java/cpw/mods/modlauncher/util/LoggingUtils.java new file mode 100644 index 0000000..0df7f00 --- /dev/null +++ b/src/main/java/cpw/mods/modlauncher/util/LoggingUtils.java @@ -0,0 +1,55 @@ +package cpw.mods.modlauncher.util; + +import org.apache.logging.log4j.core.config.ConfigurationSource; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public final class LoggingUtils { + // We're duplicating these here because log4j doesn't make them easily accessible + private static final String[] defaultConfigurationFiles = new String[] { + "log4j2-test.properties", + "log4j2-test.yaml", "log4j2-test.yml", + "log4j2-test.json", "log4j2-test.jsn", + "log4j2-test.xml", + "log4j2.properties", + "log4j2.yaml", "log4j2.yml", + "log4j2.json", "log4j2.jsn", + "log4j2.xml", + }; + + /** + * Gets a list of {@link URI}s from the given {@link ModuleLayer} + * following the same logic as log4j's automatic configuration. + * @param layer The module layer to load the configuration source from. + * @return The found configuration source, or null if one could not be + * found. + */ + public static List getConfigurationSources(ModuleLayer layer) { + try { + return Arrays.stream(defaultConfigurationFiles) + .flatMap(file -> layer.modules().stream() + .flatMap(module -> module.getClassLoader().resources(file))) + .map(LoggingUtils::toUri) + .toList(); + } catch (RuntimeException e) { + // TODO: do something with this + } + + return List.of(); + } + + private static URI toUri(URL url) { + try { + return url.toURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/cpw/mods/modlauncher/util/UriConverter.java b/src/main/java/cpw/mods/modlauncher/util/UriConverter.java new file mode 100644 index 0000000..d4011a1 --- /dev/null +++ b/src/main/java/cpw/mods/modlauncher/util/UriConverter.java @@ -0,0 +1,22 @@ +package cpw.mods.modlauncher.util; + +import joptsimple.ValueConverter; + +import java.net.URI; + +public class UriConverter implements ValueConverter { + @Override + public URI convert(String value) { + return URI.create(value); + } + + @Override + public Class valueType() { + return URI.class; + } + + @Override + public String valuePattern() { + return "[scheme:]scheme-specific-part[#fragment]"; + } +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 78aa13c..c2d0ced 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,16 +1,11 @@ - - - - - - + @@ -18,6 +13,7 @@ + @@ -27,12 +23,12 @@ - - - - - - + + + + + + diff --git a/src/test/java/cpw/mods/modlauncher/test/ClassLoaderAPITest.java b/src/test/java/cpw/mods/modlauncher/test/ClassLoaderAPITest.java index cad9f91..e891864 100644 --- a/src/test/java/cpw/mods/modlauncher/test/ClassLoaderAPITest.java +++ b/src/test/java/cpw/mods/modlauncher/test/ClassLoaderAPITest.java @@ -27,7 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -class ClassLoaderAPITest { +class ClassLoaderAPITest extends LoggingStateTestBase { @Test void testGetResources() throws ClassNotFoundException { Launcher.main("--version", "1.0", "--launchTarget", "mockLaunch", "--test.mods", "A,B,C,cpw.mods.modlauncher.testjar.TestClass", "--accessToken", "SUPERSECRET!"); diff --git a/src/test/java/cpw/mods/modlauncher/test/ClassTransformerTests.java b/src/test/java/cpw/mods/modlauncher/test/ClassTransformerTests.java index 77989f8..b1de8bc 100644 --- a/src/test/java/cpw/mods/modlauncher/test/ClassTransformerTests.java +++ b/src/test/java/cpw/mods/modlauncher/test/ClassTransformerTests.java @@ -44,7 +44,7 @@ /** * Test core transformer functionality */ -class ClassTransformerTests { +class ClassTransformerTests extends LoggingStateTestBase { @Test void testClassTransformer() throws Exception { MarkerManager.getMarker("CLASSDUMP"); diff --git a/src/test/java/cpw/mods/modlauncher/test/LauncherTests.java b/src/test/java/cpw/mods/modlauncher/test/LauncherTests.java index 1c440c5..7f59b73 100644 --- a/src/test/java/cpw/mods/modlauncher/test/LauncherTests.java +++ b/src/test/java/cpw/mods/modlauncher/test/LauncherTests.java @@ -46,7 +46,7 @@ /** * Test overall launcher */ -class LauncherTests { +class LauncherTests extends LoggingStateTestBase { @Test void testLauncher() throws Exception { Launcher.main("--version", "1.0", "--launchTarget", "mockLaunch", "--test.mods", "A,B,C,cpw.mods.modlauncher.testjar.TestClass", "--accessToken", "SUPERSECRET!"); diff --git a/src/test/java/cpw/mods/modlauncher/test/LoggingStateTestBase.java b/src/test/java/cpw/mods/modlauncher/test/LoggingStateTestBase.java new file mode 100644 index 0000000..322bfa1 --- /dev/null +++ b/src/test/java/cpw/mods/modlauncher/test/LoggingStateTestBase.java @@ -0,0 +1,25 @@ +package cpw.mods.modlauncher.test; + +import cpw.mods.modlauncher.Launcher; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.Configurator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.util.Map; + +public abstract class LoggingStateTestBase { + @BeforeEach + @AfterEach + void EnsureCleanLoggingState() { + final var keysToRemove = System.getProperties().keySet().stream() + .map(entry -> (String)entry) + .filter(entry -> entry.startsWith("logging.")) + .toList(); + + keysToRemove.forEach(System::clearProperty); + Configurator.reconfigure(ConfigurationFactory.getInstance().getConfiguration(LoggerContext.getContext(), ConfigurationSource.fromResource("log4j2.xml", Launcher.class.getClassLoader()))); + } +} diff --git a/src/test/java/cpw/mods/modlauncher/test/LoggingTests.java b/src/test/java/cpw/mods/modlauncher/test/LoggingTests.java new file mode 100644 index 0000000..10f8107 --- /dev/null +++ b/src/test/java/cpw/mods/modlauncher/test/LoggingTests.java @@ -0,0 +1,127 @@ +package cpw.mods.modlauncher.test; + +import cpw.mods.modlauncher.Launcher; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configurator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class LoggingTests extends LoggingStateTestBase { + private static Stream testLogLevelPropertyArgs() { + return Stream.of( + Arguments.of(Level.ALL, + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of()), + Arguments.of(Level.TRACE, + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of()), + Arguments.of(Level.DEBUG, + List.of(Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE)), + Arguments.of(Level.INFO, + List.of(Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG)), + Arguments.of(Level.WARN, + List.of(Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO)), + Arguments.of(Level.ERROR, + List.of(Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN)), + Arguments.of(Level.FATAL, + List.of(Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR)), + Arguments.of(Level.OFF, + List.of(), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL))); + } + + @ParameterizedTest + @MethodSource("testLogLevelPropertyArgs") + void testLogLevelProperty(Level defaultLogLevel, List enabledLevels, List disabledLevels) { + System.setProperty("logging.loglevel.default", defaultLogLevel.name()); + Launcher.main("--version", "1.0", "--launchTarget", "mockLaunch", "--test.mods", "A,B,C,cpw.mods.modlauncher.testjar.TestClass", "--accessToken", "SUPERSECRET!", "--testLogLevel", defaultLogLevel.name()); + for (final var level : enabledLevels) { + assertTrue(LogManager.getRootLogger().isEnabled(level)); + } + for (final var level : disabledLevels) { + assertFalse(LogManager.getRootLogger().isEnabled(level)); + } + } + + private static Stream testLogMarkerPropertyArgs() { + return Stream.of( + Arguments.of("MODLAUNCHER", Level.ALL, + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of()), + Arguments.of("MODLAUNCHER", Level.TRACE, + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of()), + Arguments.of("MODLAUNCHER", Level.DEBUG, + List.of(Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE)), + Arguments.of("MODLAUNCHER", Level.INFO, + List.of(Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG)), + Arguments.of("MODLAUNCHER", Level.WARN, + List.of(Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO)), + Arguments.of("MODLAUNCHER", Level.ERROR, + List.of(Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN)), + Arguments.of("MODLAUNCHER", Level.FATAL, + List.of(Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR)), + Arguments.of("MODLAUNCHER", Level.OFF, + List.of(), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL)), + + Arguments.of("LAUNCHPLUGIN", Level.ALL, + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of()), + Arguments.of("LAUNCHPLUGIN", Level.TRACE, + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of()), + Arguments.of("LAUNCHPLUGIN", Level.DEBUG, + List.of(Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE)), + Arguments.of("LAUNCHPLUGIN", Level.INFO, + List.of(Level.INFO, Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG)), + Arguments.of("LAUNCHPLUGIN", Level.WARN, + List.of(Level.WARN, Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO)), + Arguments.of("LAUNCHPLUGIN", Level.ERROR, + List.of(Level.ERROR, Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN)), + Arguments.of("LAUNCHPLUGIN", Level.FATAL, + List.of(Level.FATAL), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR)), + Arguments.of("LAUNCHPLUGIN", Level.OFF, + List.of(), + List.of(Level.TRACE, Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR, Level.FATAL))); + } + + @ParameterizedTest + @MethodSource("testLogMarkerPropertyArgs") + void testLogMarkerProperty(String markerName, Level defaultLogLevel, List enabledLevels, List disabledLevels) { + System.setProperty("logging.marker." + markerName, defaultLogLevel.name()); + Launcher.main("--version", "1.0", "--launchTarget", "mockLaunch", "--test.mods", "A,B,C,cpw.mods.modlauncher.testjar.TestClass", "--accessToken", "SUPERSECRET!", "--testMarker", markerName, "--testMarkerLevel", defaultLogLevel.name()); + var marker = MarkerManager.getMarker(markerName); + for (final var level : enabledLevels) { + assertTrue(LogManager.getRootLogger().isEnabled(level, marker)); + } + for (final var level : disabledLevels) { + assertFalse(LogManager.getRootLogger().isEnabled(level, marker)); + } + } +} diff --git a/src/test/java/cpw/mods/modlauncher/test/MockLaunchPluginService.java b/src/test/java/cpw/mods/modlauncher/test/MockLaunchPluginService.java new file mode 100644 index 0000000..166bd15 --- /dev/null +++ b/src/test/java/cpw/mods/modlauncher/test/MockLaunchPluginService.java @@ -0,0 +1,19 @@ +package cpw.mods.modlauncher.test; + +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import org.objectweb.asm.Type; + +import java.util.EnumSet; + +public class MockLaunchPluginService implements ILaunchPluginService { + + @Override + public String name() { + return "test"; + } + + @Override + public EnumSet handlesClass(Type classType, boolean isEmpty) { + return EnumSet.noneOf(Phase.class); + } +} diff --git a/src/test/java/cpw/mods/modlauncher/test/TestingLHTests.java b/src/test/java/cpw/mods/modlauncher/test/TestingLHTests.java index fcb16eb..7247dfb 100644 --- a/src/test/java/cpw/mods/modlauncher/test/TestingLHTests.java +++ b/src/test/java/cpw/mods/modlauncher/test/TestingLHTests.java @@ -27,7 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -class TestingLHTests { +class TestingLHTests extends LoggingStateTestBase { boolean calledback; @Test diff --git a/src/test/java/module-info.java b/src/test/java/module-info.java index 823c8f3..fb1de4c 100644 --- a/src/test/java/module-info.java +++ b/src/test/java/module-info.java @@ -4,6 +4,7 @@ requires static cpw.mods.modlauncher.testjars; requires org.junit.jupiter.api; + requires org.junit.jupiter.params; requires powermock.core; requires powermock.reflect; requires org.objectweb.asm; @@ -16,5 +17,6 @@ exports cpw.mods.modlauncher.test; provides cpw.mods.modlauncher.api.ILaunchHandlerService with cpw.mods.modlauncher.test.MockLauncherHandlerService; + provides cpw.mods.modlauncher.serviceapi.ILaunchPluginService with cpw.mods.modlauncher.test.MockLaunchPluginService; provides cpw.mods.modlauncher.api.ITransformationService with cpw.mods.modlauncher.test.MockTransformerService; } diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml deleted file mode 100644 index 2bd065a..0000000 --- a/src/test/resources/log4j2.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -