test: reducing logspam (#5022)

Co-authored-by: jdrueckert <jd.rueckert@googlemail.com>
develop
Kevin Turner 2022-06-04 10:56:59 -07:00 committed by GitHub
parent a24c07d000
commit 8a7219b637
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 448 additions and 181 deletions

View File

@ -6,6 +6,9 @@
<LanguageOptions name="Properties">
<option name="fileTypeOverride" value="1" />
</LanguageOptions>
<LanguageOptions name="XML">
<option name="fileTypeOverride" value="1" />
</LanguageOptions>
<LanguageOptions name="__TEMPLATE__">
<option name="block" value="false" />
</LanguageOptions>

7
Jenkinsfile vendored
View File

@ -54,6 +54,7 @@ pipeline {
gradlew,
gradle/wrapper/*,
templates/build.gradle,
templates/module.logback-test.xml,
config/**,
facades/PC/build/distributions/Terasology.zip,
engine/build/resources/main/org/terasology/engine/version/versionInfo.properties,
@ -76,6 +77,9 @@ pipeline {
//
// See https://docs.gradle.org/current/userguide/java_testing.html#test_reporting
junit testResults: '**/build/test-results/unitTest/*.xml'
// Jenkins truncates large test outputs, so archive it as well.
tar file: 'build/unitTest-results.tgz', archive: true, compress: true, overwrite: true,
glob: '**/build/test-results/unitTest/*.xml'
}
}
}
@ -165,6 +169,9 @@ pipeline {
//
// See https://docs.gradle.org/current/userguide/java_testing.html#test_reporting
junit testResults: '**/build/test-results/integrationTest/*.xml', allowEmptyResults: true
// Jenkins truncates large test outputs, so archive it as well.
tar file: 'build/integrationTest-results.tgz', archive: true, compress: true, overwrite: true,
glob: '**/build/test-results/integrationTest/*.xml'
}
}
}

View File

@ -16,6 +16,29 @@ plugins {
dependencies {
"pmd"("net.sourceforge.pmd:pmd-core:6.15.0")
"pmd"("net.sourceforge.pmd:pmd-java:6.15.0")
testRuntimeOnly("ch.qos.logback:logback-classic:1.2.11") {
because("runtime: to configure logging during tests with logback.xml")
}
testRuntimeOnly("org.codehaus.janino:janino:3.1.7") {
because("allows use of EvaluatorFilter in logback.xml")
}
testRuntimeOnly("org.slf4j:jul-to-slf4j:1.7.36") {
because("redirects java.util.logging (from e.g. junit) through slf4j")
}
add("testImplementation", platform("org.junit:junit-bom:5.8.1"))
testImplementation("org.junit.jupiter:junit-jupiter-api")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("org.mockito:mockito-inline:3.12.4")
testImplementation("org.mockito:mockito-junit-jupiter:3.12.4")
testImplementation("com.google.truth:truth:1.1.3")
testImplementation("com.google.truth.extensions:truth-java8-extension:1.1.3")
testImplementation("io.projectreactor:reactor-test:3.4.14")
}
tasks.withType<Test> {
@ -24,7 +47,8 @@ tasks.withType<Test> {
// ignoreFailures: Specifies whether the build should break when the verifications performed by this task fail.
ignoreFailures = true
// showStandardStreams: makes the standard streams (err and out) visible at console when running tests
testLogging.showStandardStreams = true
// If false, the outputs are still collected and visible in the test report, but they don't spam the console.
testLogging.showStandardStreams = false
reports {
junitXml.isEnabled = true
}

View File

@ -67,18 +67,7 @@ dependencies {
}
}
testRuntimeOnly("org.slf4j:jul-to-slf4j:1.7.21")
add("testImplementation", platform("org.junit:junit-bom:5.8.1"))
testImplementation("org.junit.jupiter:junit-jupiter-api")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("org.mockito:mockito-inline:3.12.4")
testImplementation("org.mockito:mockito-junit-jupiter:3.12.4")
testImplementation("com.google.truth:truth:1.1.3")
testImplementation("com.google.truth.extensions:truth-java8-extension:1.1.3")
// see terasology-metrics for test dependencies
}

View File

@ -1,21 +1,13 @@
/*
* Copyright 2020 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2022 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
import groovy.json.JsonSlurper
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
class module {
def excludedItems = ["engine", "Index", "out", "build"]
@ -58,12 +50,6 @@ class module {
}
def copyInTemplateFiles(File targetDir) {
// Copy in the template build.gradle for modules
println "In copyInTemplateFiles for module $targetDir.name - copying in a build.gradle then next checking for module.txt"
File targetBuildGradle = new File(targetDir, 'build.gradle')
targetBuildGradle.delete()
targetBuildGradle << new File('templates/build.gradle').text
// Copy in the template module.txt for modules (if one doesn't exist yet)
File moduleManifest = new File(targetDir, 'module.txt')
if (!moduleManifest.exists()) {
@ -73,8 +59,9 @@ class module {
println "WARNING: the module ${targetDir.name} did not have a module.txt! One was created, please review and submit to GitHub"
}
refreshGradle(targetDir)
// TODO: Copy in a module readme template soon
// TODO : Add in the logback.groovy from engine\src\test\resources\logback.groovy ? Local dev only, Jenkins will use the one inside engine-tests.jar. Also add to .gitignore
}
/**
@ -94,14 +81,28 @@ class module {
}
def refreshGradle(File targetDir) {
// Copy in the template build.gradle for modules
if (!new File(targetDir, "module.txt").exists()) {
println "$targetDir has no module.txt, it must not want a fresh build.gradle"
if (!(targetDir.canRead() && targetDir.canWrite())) {
println "$targetDir: ⛔ not accessible"
return
}
println "In refreshGradle for module $targetDir - copying in a fresh build.gradle"
File targetBuildGradle = new File(targetDir, 'build.gradle')
targetBuildGradle.delete()
targetBuildGradle << new File('templates/build.gradle').text
Path targetPath = targetDir.toPath()
if (Files.notExists(targetPath.resolve('module.txt'))) {
println "$targetDir/module.txt: ❓ not present, it must not want a fresh build.gradle"
return
}
Path templates = Path.of('templates')
Files.copy(templates.resolve('build.gradle'), targetPath.resolve('build.gradle'),
StandardCopyOption.REPLACE_EXISTING)
println "$targetDir/build.gradle: ✨ refreshed"
Path logbackXml = targetPath.resolve('src/test/resources/logback-test.xml')
if (Files.notExists(logbackXml)) {
Files.createDirectories(logbackXml.parent)
Files.copy(templates.resolve('module.logback-test.xml'), logbackXml)
println "$logbackXml: ✨ added"
} else {
println "$logbackXml: already there"
}
}
}

View File

@ -58,30 +58,31 @@ dependencies {
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
implementation group: 'org.codehaus.plexus', name: 'plexus-utils', version: '3.0.16'
implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.16.1'
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.11'
runtimeOnly('org.codehaus.janino:janino:3.1.7') {
because("logback filters")
}
runtimeOnly group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.21'
implementation "org.terasology:reflections:0.9.12-MB"
implementation("org.terasology.joml-ext:joml-test:0.1.0")
implementation('org.slf4j:slf4j-api:1.7.36') {
because('a backend-independent Logger')
}
testImplementation("ch.qos.logback:logback-classic:1.2.11") {
because("implementation: a test directly uses logback.classic classes")
}
// Test lib dependencies
implementation(platform("org.junit:junit-bom:5.8.1")) {
// junit-bom will set version numbers for the other org.junit dependencies.
}
implementation("org.junit.jupiter:junit-jupiter-api")
implementation("org.junit.jupiter:junit-jupiter-params")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation('org.mockito:mockito-inline:3.12.4')
api("org.junit.jupiter:junit-jupiter-api") {
because("we export jupiter Extensions for module tests")
}
implementation("org.mockito:mockito-inline:3.12.4") {
because("classes like HeadlessEnvironment use mocks")
}
implementation('org.mockito:mockito-junit-jupiter:3.12.4')
implementation("org.terasology.joml-ext:joml-test:0.1.0")
testImplementation('com.google.truth:truth:1.1.3')
testImplementation('com.google.truth.extensions:truth-java8-extension:1.1.3')
implementation("io.projectreactor:reactor-test:3.4.14")
// See terasology-metrics for other test-only internal dependencies
}
task copyResourcesToClasses(type:Copy) {

View File

@ -3,20 +3,14 @@
package org.terasology.engine.integrationenvironment.jupiter;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import com.google.common.collect.Sets;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.opentest4j.MultipleFailuresError;
import org.slf4j.LoggerFactory;
import org.terasology.engine.integrationenvironment.Engines;
import org.terasology.engine.integrationenvironment.MainLoop;
import org.terasology.engine.integrationenvironment.ModuleTestingHelper;
@ -24,8 +18,6 @@ import org.terasology.engine.integrationenvironment.Scopes;
import org.terasology.engine.registry.In;
import org.terasology.unittest.worlds.DummyWorldGenerator;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
@ -85,24 +77,12 @@ import java.util.function.Function;
* want isolated engine instances try {@link IsolatedMTEExtension}.
* <p>
* Note that classes marked {@link Nested} will share the engine context with their parent.
* <p>
* This will configure the logger and the current implementation is not subtle or polite about it, see
* {@link #setupLogging()} for notes.
*/
public class MTEExtension implements BeforeAllCallback, ParameterResolver, TestInstancePostProcessor {
public class MTEExtension implements ParameterResolver, TestInstancePostProcessor {
static final String LOGBACK_RESOURCE = "default-logback.xml";
protected Function<ExtensionContext, ExtensionContext.Namespace> helperLifecycle = Scopes.PER_CLASS;
protected Function<ExtensionContext, Class<?>> getTestClass = Scopes::getTopTestClass;
@Override
public void beforeAll(ExtensionContext context) {
if (context.getRequiredTestClass().isAnnotationPresent(Nested.class)) {
return; // nested classes get set up in the parent
}
setupLogging();
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
@ -190,41 +170,6 @@ public class MTEExtension implements BeforeAllCallback, ParameterResolver, TestI
return autoCleaner.engines;
}
/**
* Apply our default logback configuration to the logger.
* <p>
* Modules won't generally have their own logback-test.xml, so we'll install ours from {@value LOGBACK_RESOURCE}.
* <p>
* <b>TODO:</b>
* <ul>
* <li>Only reset the current LoggerContext if it really hasn't been customized by elsewhere.
* <li>When there are multiple classes with MTEExtension, do we end up doing this repeatedly
* in the same process?
* <li>Provide a way to add/change/override what this is doing that doesn't require checking
* out the MTE sources and editing default-logback.xml.
* </ul>
*/
void setupLogging() {
// This is mostly right out of the book:
// http://logback.qos.ch/xref/chapters/configuration/MyApp3.html
JoranConfigurator cfg = new JoranConfigurator();
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.reset();
cfg.setContext(context);
try (InputStream i = getClass().getResourceAsStream(LOGBACK_RESOURCE)) {
if (i == null) {
throw new RuntimeException("Failed to find " + LOGBACK_RESOURCE);
}
cfg.doConfigure(i);
} catch (IOException e) {
throw new RuntimeException("Error reading " + LOGBACK_RESOURCE, e);
} catch (JoranException e) {
throw new RuntimeException("Error during logger configuration", e);
} finally {
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
}
/**
* Manages Engines for storage in an ExtensionContext.
* <p>

View File

@ -1,38 +0,0 @@
<!-- Copyright 2022 The Terasology Foundation -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
<configuration>
<!-- Report all changes to this configuration on System.out -->
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<matcher>
<Name>reflectionsEmpty</Name>
<!-- filter out odd numbered statements -->
<regex>given scan urls are empty</regex>
</matcher>
<expression>reflectionsEmpty.matches(message)</expression>
</evaluator>
<OnMismatch>NEUTRAL</OnMismatch>
<OnMatch>DENY</OnMatch>
</filter>
</appender>
<root level="${logOverrideLevel:-info}">
<appender-ref ref="CONSOLE" />
</root>
<logger name="org.terasology" level="${logOverrideLevel:-info}" />
<logger name="org.terasology.engine.i18n" level="warn" />
<logger name="org.reflections.Reflections" level="${logOverrideLevel:-warn}" />
</configuration>

View File

@ -1,8 +1,8 @@
junit.jupiter.testinstance.lifecycle.default=per_method
# Parallel execution params:
junit.jupiter.execution.parallel.enabled=false
junit.jupiter.execution.parallel.enabled=false
junit.jupiter.execution.parallel.mode.default=SAME_THREAD
junit.jupiter.execution.parallel.mode.classes.default=SAME_THREAD
#junit.jupiter.execution.parallel.config.fixed.parallelism=4
#junit.jupiter.execution.parallel.config.fixed.parallelism=4

View File

@ -1,23 +1,25 @@
<!-- Copyright 2021 The Terasology Foundation -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
<configuration debug="false">
<!-- debug: reports all changes to this configuration on System.out -->
<configuration>
<!-- Report all changes to this configuration on System.out -->
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
<include resource="org/terasology/engine/logback/setup.xml" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<include resource="org/terasology/engine/logback/filter-reflections.xml" />
</appender>
<!-- JUnit collects console output during test execution
and include it in the report for the test. -->
<root level="${logOverrideLevel:-debug}">
<appender-ref ref="CONSOLE" />
</root>
<logger name="org.terasology" level="${logOverrideLevel:-info}" />
<logger name="org.terasology.engine.i18n" level="warn" />
<logger name="org.reflections.Reflections" level="warn" />
</configuration>

View File

@ -112,8 +112,16 @@ dependencies {
api group: 'com.miglayout', name: 'miglayout-core', version: '5.0'
implementation group: 'de.matthiasmann.twl', name: 'PNGDecoder', version: '1111'
// Logging and audio
implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.21'
// Logging
implementation('org.slf4j:slf4j-api:1.7.36') {
because('a backend-independent Logger')
}
implementation("ch.qos.logback:logback-classic:1.2.11") {
because("telemetry implementation uses logback to send to logstash " +
"and we bundle org.terasology.logback for the regex filter")
}
// audio
implementation group: 'com.projectdarkstar.ext.jorbis', name: 'jorbis', version: '0.0.17'
// Small-time 3rd party libs we've stored in our Artifactory for access
@ -144,9 +152,6 @@ dependencies {
// Wildcard dependency to catch any libs provided with the project (remote repo preferred instead)
api fileTree(dir: 'libs', include: '*.jar')
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.11'
runtimeOnly group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.21'
// TODO: Consider moving this back to the PC Facade instead of having the engine rely on it?
implementation group: 'org.terasology.crashreporter', name: 'cr-terasology', version: '4.2.0'

View File

@ -0,0 +1,65 @@
// Copyright 2022 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.logback;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.boolex.EventEvaluatorBase;
import com.google.common.base.MoreObjects;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Evaluate messages with a regular expression.
*
* @see RegexFilterAction
*/
public class RegexEvaluator<E extends ILoggingEvent> extends EventEvaluatorBase<E> {
final Pattern prefix;
final Pattern matchMessage;
private final Predicate<String> prefixTest;
private final Predicate<String> messageTest;
/**
* @param prefix Test the event's logger name against this pattern.
* May be null to apply to all loggers.
*
* @param matchMessage Test the event's formatted message against this pattern.
*/
public RegexEvaluator(Pattern prefix, Pattern matchMessage) {
this.prefix = prefix;
this.matchMessage = matchMessage;
prefixTest = (prefix == null) ? s -> true : prefix.asPredicate();
messageTest = checkNotNull(matchMessage).asPredicate();
}
/**
* @param prefixString Test the event's logger name against this prefix. This input is a
* simple string, <em>not</em> a regex pattern. May be null to apply to all loggers.
*
* @param messagePattern A regular expression {@linkplain Pattern pattern} to match against the
* {@linkplain ILoggingEvent#getFormattedMessage() formatted message} of a logging event.
*/
public RegexEvaluator(String prefixString, String messagePattern) {
this(prefixString == null ? null : Pattern.compile("^" + Pattern.quote(prefixString)),
Pattern.compile(checkNotNull(messagePattern)));
}
@Override
public boolean evaluate(E event) {
return prefixTest.test(event.getLoggerName()) && messageTest.test(event.getFormattedMessage());
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("prefix", prefix)
.add("matchMessage", matchMessage)
.toString();
}
}

View File

@ -0,0 +1,181 @@
// Copyright 2022 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.logback;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.boolex.EventEvaluator;
import ch.qos.logback.core.filter.EvaluatorFilter;
import ch.qos.logback.core.joran.action.Action;
import ch.qos.logback.core.joran.spi.ActionException;
import ch.qos.logback.core.joran.spi.InterpretationContext;
import ch.qos.logback.core.spi.FilterAttachable;
import ch.qos.logback.core.spi.FilterReply;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import org.xml.sax.Attributes;
import java.util.function.BiFunction;
/**
* Regex-based logback filter with concise syntax.
* <p>
* <b>Example:</b>
* <p><code>
* &lt;denyRegex prefix="org.reflections" message="given scan \w+ are empty" /&gt;
* </code>
* <dl>
* <dt>{@code denyRegex}</dt>
* <dd>The tag starts with {@code "deny"}, meaning events matching this filter will be dropped.</dd>
*
* <dt>{@code prefix} <i>(optional)</i></dt>
* <dd>This filter will only test events from loggers with names starting {@code "org.reflections"}.<br>
* It is {@linkplain FilterReply#NEUTRAL neutral} on other events.</dd>
*
* <dt>{@code message}</dt>
* <dd>Match the {@linkplain ILoggingEvent#getFormattedMessage() formatted message} against
* the regular expression {@linkplain java.util.regex.Pattern pattern}
* {@code /given scan \w+ are empty/}.</dd>
* </dl>
* <p>
* <b>Example:</b>
* <p><code>
* &lt;requireRegex message="module:.*-SNAPSHOT" /&gt;
* </code>
* <dl>
* <dt>{@code requireRegex}</dt>
* <dd>The tag starts with {@code "require"}, meaning events <em>must</em> match the pattern.</dd>
*
* <dt>{@code message}</dt>
* <dd>Match the {@linkplain ILoggingEvent#getFormattedMessage() formatted message} against
* the regular expression {@linkplain java.util.regex.Pattern pattern}
* {@code /module:.*-SNAPSHOT/}.</dd>
* </dl>
* No Groovy or Janino required.
* <p>
* This Action adds an {@link EvaluatorFilter} with a {@link RegexEvaluator} to
* the current element, such as an {@link ch.qos.logback.core.Appender Appender}.
* <p>
* The element must be on the top of the stack and must implement {@link FilterAttachable}.
* <p>
* <b>Implementation Note:</b> In logback 1.2, we can <em>almost</em> define an
* EvaluatorFilter using only logback-core, but core defines no LoggingEvent interface.
* We have to link against logback-classic for that.
*
* @param <E> the type of logging event the filter operates on
*/
public class RegexFilterAction<E extends ILoggingEvent> extends Action {
public static final String PREFIX_ATTRIBUTE = "prefix";
public static final String MESSAGE_ATTRIBUTE = "message";
public static final String DENY_ELEMENT = "deny";
public static final String REQUIRE_ELEMENT = "require";
protected final BiFunction<String, String, EventEvaluator<E>> evaluatorConstructor = RegexEvaluator::new;
@Override
public void begin(InterpretationContext context, String elementName, Attributes attributes) throws ActionException {
if (peekFilterContainer(context) == null) {
return;
}
var evaluator = evaluatorConstructor.apply(
findAndSubst(context, attributes, PREFIX_ATTRIBUTE),
findAndSubst(context, attributes, MESSAGE_ATTRIBUTE)
);
EvaluatorFilter<E> filter = new RegexFilter<>(
getContext(),
evaluator,
makeFilterName(context, attributes));
if (elementName.startsWith(REQUIRE_ELEMENT)) {
filter.setOnMatch(FilterReply.NEUTRAL);
filter.setOnMismatch(FilterReply.DENY);
} else if (elementName.startsWith(DENY_ELEMENT)) {
filter.setOnMatch(FilterReply.DENY);
filter.setOnMismatch(FilterReply.NEUTRAL);
}
// Push the new filter to the stack, in case anyone wants to configure it further.
context.pushObject(filter);
}
@Override
public void end(InterpretationContext context, String name) throws ActionException {
Object top = context.peekObject();
EvaluatorFilter<E> filter;
if (top instanceof EvaluatorFilter) {
// ignore Java warning about cast from Object to EvaluatorFilter<E>
//noinspection unchecked
filter = (EvaluatorFilter<E>) top;
// Remove the filter from the stack now that the parser is at the end of it.
context.popObject();
} else {
addError("Top of stack does not have Filter, but this thing: " + top);
return;
}
// We expect the stack to be back at the parent container now.
FilterAttachable<E> filterContainer = peekFilterContainer(context);
if (filterContainer == null) {
return;
}
if (filter.getOnMatch() == FilterReply.NEUTRAL && filter.getOnMismatch() == FilterReply.NEUTRAL) {
addError("onMatch and onMismatch are both neutral; this filter is a no-op: " + filter);
}
filterContainer.addFilter(filter);
filter.getEvaluator().start();
filter.start();
addInfo(String.format("Added %s to %s", filter, filterContainer));
}
protected FilterAttachable<E> peekFilterContainer(InterpretationContext context) {
Object parentObject = context.peekObject();
if (parentObject instanceof FilterAttachable) {
@SuppressWarnings("unchecked") FilterAttachable<E> container =
(FilterAttachable<E>) parentObject;
return container;
} else {
addError("Cannot attach filter to top of stack item " + parentObject);
return null;
}
}
protected String makeFilterName(InterpretationContext context, Attributes attributes) {
String filterName = findAndSubst(context, attributes, NAME_ATTRIBUTE);
if (Strings.isNullOrEmpty(filterName)) {
// If a name wasn't provided, make one from the attributes
// as they appear in the source (before variable substitution).
filterName = Strings.nullToEmpty(attributes.getValue(PREFIX_ATTRIBUTE)) + ":"
+ attributes.getValue(MESSAGE_ATTRIBUTE);
}
return filterName;
}
private String findAndSubst(InterpretationContext context, Attributes attributes, String name) {
return context.subst(attributes.getValue(name));
}
public static class RegexFilter<EE> extends EvaluatorFilter<EE> {
RegexFilter(Context context, EventEvaluator<EE> evaluator, String name) {
super();
setName(name);
setContext(context);
setEvaluator(evaluator);
}
@Override
public String toString() {
// This is the whole reason for the subclass: a useful toString()
return MoreObjects.toStringHelper(this)
.addValue(getName())
.toString();
}
}
}

View File

@ -0,0 +1,9 @@
// Copyright 2022 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
/**
* logback general evaluators and filters
* <p>
* These are not specific to {@link org.terasology.engine} at all.
*/
package org.terasology.logback;

View File

@ -0,0 +1,14 @@
<included>
<!-- Expects to be included within an <appender>
(or something else that can contain filters). -->
<!-- Requires org.terasology.logback.RegexFilterAction; see setup.xml -->
<!-- WONTFIX: gestalt routinely builds empty configs and then adds to them later. -->
<denyRegex prefix="org.reflections" message="given scan urls are empty" />
<!-- WONTFIX: these classes don't need to be reflected on,
and they're only on the classpath during tests. -->
<denyRegex prefix="org.reflections"
message="could not get type for name org\.terasology\.benchmark" />
</included>

View File

@ -0,0 +1,19 @@
<included>
<!-- Expects to be included in the root <configuration> element. -->
<!-- JUnit (and potentially other libraries) use java.util.logging, aka JUL.
The jul.LevelChangePropagator synchronizes that with this configuration.
See https://www.slf4j.org/legacy.html#jul-to-slf4j
-->
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<!-- map <denyRegex> and <requireRegex> xml elements to RegexFilter -->
<newRule pattern="*/appender/denyRegex" actionClass="org.terasology.logback.RegexFilterAction" />
<newRule pattern="*/appender/requireRegex" actionClass="org.terasology.logback.RegexFilterAction" />
<!-- allow include inside appenders (logback 1.2.11 only enabled Include on the root config by default.) -->
<newRule pattern="*/appender/include" actionClass="ch.qos.logback.core.joran.action.IncludeAction" />
</included>

View File

@ -71,6 +71,16 @@ dependencies {
// TODO: Consider whether we can move the CR dependency back here from the engine, where it is referenced from the main menu
implementation(group = "org.terasology.crashreporter", name = "cr-terasology", version = "4.2.0")
runtimeOnly("ch.qos.logback:logback-classic:1.2.11") {
because("to configure logging with logback.xml")
}
runtimeOnly("org.codehaus.janino:janino:3.1.7") {
because("allows use of EvaluatorFilter in logback.xml")
}
runtimeOnly("org.slf4j:jul-to-slf4j:1.7.36") {
because("redirects java.util.logging from miscellaneous dependencies through slf4j")
}
testImplementation(platform("org.junit:junit-bom:5.7.1")) {
// junit-bom will set version numbers for the other org.junit dependencies.
}

View File

@ -1,20 +1,10 @@
<!-- Copyright 2021 The Terasology Foundation -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
<configuration debug="false">
<!-- debug: reports all changes to this configuration on System.out -->
<configuration>
<!-- Report all changes to this configuration on System.out -->
<!--statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" /-->
<!-- Propagate all log level changes to java.util.logging -->
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<include resource="org/terasology/engine/logback/setup.xml" />
<appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
<!-- in the absence of the class attribute, it is assumed that the
desired discriminator type is MDCBasedDiscriminator -->
<discriminator>
<discriminator class="ch.qos.logback.classic.sift.MDCBasedDiscriminator">
<key>phase</key>
<defaultValue>init</defaultValue>
</discriminator>
@ -37,10 +27,11 @@
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<include resource="org/terasology/engine/logback/filter-reflections.xml" />
</appender>
<root level="${logOverrideLevel:-debug}">

View File

@ -0,0 +1,39 @@
<!-- This file, when present as src/test/resources/logback-test.xml,
configures log filters during test execution.
To configure the logs generated while in-game, see
facades/PC/src/main/resources/logback.xml
-->
<configuration debug="false">
<!-- debug: reports all changes to this configuration on System.out -->
<include resource="org/terasology/engine/logback/setup.xml" />
<!-- JUnit collects console output during test execution
and include it in the report for the test. -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<include resource="org/terasology/engine/logback/filter-reflections.xml" />
</appender>
<root level="${logOverrideLevel:-debug}">
<appender-ref ref="CONSOLE" />
</root>
<!-- NOTE You can add fine granular custom log-level overrides here.
This works in both ways; you can increase or decrease the log level
for specific packages or classes.
For example:
<logger name="org.terasology.engine.world" level="debug" />
<logger name="org.terasology.module.health.systems.HealthAuthoritySystem" level="error" />
-->
<logger name="org.terasology" level="${logOverrideLevel:-info}" />
<logger name="org.terasology.engine.i18n" level="warn" />
<logger name="org.reflections.Reflections" level="warn" />
</configuration>