From 31854ba29133cc7f22596dc25e3e013dea19f4af Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Mon, 9 May 2022 17:18:14 -0700 Subject: [PATCH 01/25] test: Move ModuleTestingEnvironment to engine-tests/org.terasology.engine.integrationenvironment for https://github.com/MovingBlocks/Terasology/issues/4545 and https://github.com/Terasology/ModuleTestingEnvironment/issues/39 from ModuleTestingEnvironment commit adf9b6df0407a62c15092a82960d8711e28bb7b7 --- engine-tests/build.gradle | 5 +- .../ChunkRegionFuture.java | 176 ++++++++++ .../integrationenvironment/Engines.java | 303 ++++++++++++++++++ .../IsolatedMTEExtension.java | 19 ++ .../integrationenvironment/MTEExtension.java | 246 ++++++++++++++ .../integrationenvironment/MainLoop.java | 248 ++++++++++++++ .../ModuleTestingEnvironment.java | 118 +++++++ .../ModuleTestingHelper.java | 108 +++++++ .../engine/integrationenvironment/Scopes.java | 42 +++ .../TestEventReceiver.java | 162 ++++++++++ .../TestingStateHeadlessSetup.java | 72 +++++ .../extension/Dependencies.java | 25 ++ .../extension/UseWorldGenerator.java | 26 ++ .../fixtures/BaseTestingClass.java | 25 ++ .../integrationenvironment/package-info.java | 16 + .../unittest/stubs/DummyComponent.java | 17 + .../terasology/unittest/stubs/DummyEvent.java | 7 + .../unittest/stubs/DummySystem.java | 20 ++ .../unittest/worlds/DummyWorldGenerator.java | 29 ++ .../unittest/worlds/EmptyWorldGenerator.java | 38 +++ .../worlds/FlatSurfaceHeightProvider.java | 45 +++ .../{stubs => worlds}/StubWorldGenerator.java | 4 +- .../unittest/worlds/package-info.java | 7 + .../default-logback.xml | 38 +++ .../AssetLoadingTest.java | 41 +++ .../ChunkRegionFutureTest.java | 60 ++++ .../ClientConnectionTest.java | 32 ++ .../ComponentSystemTest.java | 29 ++ .../integrationenvironment/ExampleTest.java | 83 +++++ .../IsolatedEngineTest.java | 62 ++++ ...MTEExtensionTestWithPerClassLifecycle.java | 94 ++++++ ...TEExtensionTestWithPerMethodLifecycle.java | 95 ++++++ .../ModuleTestingEnvironmentTest.java | 39 +++ .../integrationenvironment/NestedTest.java | 45 +++ .../SubclassInjectionTest.java | 23 ++ .../TestEventReceiverTest.java | 119 +++++++ .../WorldProviderTest.java | 39 +++ .../delay/DelayManagerTest.java | 59 ++++ 38 files changed, 2613 insertions(+), 3 deletions(-) create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ChunkRegionFuture.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/IsolatedMTEExtension.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MTEExtension.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironment.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestEventReceiver.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/Dependencies.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/UseWorldGenerator.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/fixtures/BaseTestingClass.java create mode 100644 engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java create mode 100644 engine-tests/src/main/java/org/terasology/unittest/stubs/DummyComponent.java create mode 100644 engine-tests/src/main/java/org/terasology/unittest/stubs/DummyEvent.java create mode 100644 engine-tests/src/main/java/org/terasology/unittest/stubs/DummySystem.java create mode 100644 engine-tests/src/main/java/org/terasology/unittest/worlds/DummyWorldGenerator.java create mode 100644 engine-tests/src/main/java/org/terasology/unittest/worlds/EmptyWorldGenerator.java create mode 100644 engine-tests/src/main/java/org/terasology/unittest/worlds/FlatSurfaceHeightProvider.java rename engine-tests/src/main/java/org/terasology/unittest/{stubs => worlds}/StubWorldGenerator.java (94%) create mode 100644 engine-tests/src/main/java/org/terasology/unittest/worlds/package-info.java create mode 100644 engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/default-logback.xml create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java create mode 100644 engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java diff --git a/engine-tests/build.gradle b/engine-tests/build.gradle index 0f6de9e0d88..ec03e746b23 100644 --- a/engine-tests/build.gradle +++ b/engine-tests/build.gradle @@ -57,7 +57,10 @@ dependencies { implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6' implementation group: 'org.codehaus.plexus', name: 'plexus-utils', version: '1.5.6' implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.15.3' - implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.9' + 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" diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ChunkRegionFuture.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ChunkRegionFuture.java new file mode 100644 index 00000000000..d70a01e9b62 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ChunkRegionFuture.java @@ -0,0 +1,176 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import org.joml.Vector3fc; +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.entity.internal.EntityScope; +import org.terasology.engine.logic.location.LocationComponent; +import org.terasology.engine.world.block.BlockRegion; +import org.terasology.engine.world.block.BlockRegionc; +import org.terasology.engine.world.chunks.Chunk; +import org.terasology.engine.world.chunks.ChunkRegionListener; +import org.terasology.engine.world.chunks.localChunkProvider.RelevanceSystem; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Completes when all the chunks in a region are loaded. + * + * @see MainLoop#makeBlocksRelevant + * @see MainLoop#makeChunksRelevant + */ +@SuppressWarnings("checkstyle:finalclass") +public class ChunkRegionFuture { + public static final int REQUIRED_CHUNK_MARGIN = 1; + + private static final Logger logger = LoggerFactory.getLogger(ChunkRegionFuture.class); + + protected final SettableFuture future = SettableFuture.create(); + protected final Set loadedChunks = new HashSet<>(); + protected final BlockRegion chunks = new BlockRegion(BlockRegion.INVALID); + + private final EntityRef entity; + + private ChunkRegionFuture(EntityRef entity, Function chunks) { + this.entity = entity; + this.chunks.set(chunks.apply(new Listener(this::onChunkRelevant))); + } + + /** + * Load an area of the world. + *

+ * The area is defined as a {@index "relevance region"} and will not be unloaded as long as {@link #entity} exists + * and has a {@link LocationComponent}. + * + * @param entityManager used to create the entity that depends on this region + * @param relevanceSystem the authority on what is relevant + * @param center a point to center the region around, in block coordinates + * @param sizeInChunks the size of the region, in chunks + */ + static ChunkRegionFuture create(EntityManager entityManager, RelevanceSystem relevanceSystem, Vector3fc center, + Vector3ic sizeInChunks) { + EntityRef entity = entityManager.create(new LocationComponent(center)); + entity.setScope(EntityScope.GLOBAL); + + Vector3ic correctedSizeInChunks = addMargin(sizeInChunks); + + Function makeChunksRelevant = listener -> { + BlockRegionc paddedRegion = + relevanceSystem.addRelevanceEntity(entity, correctedSizeInChunks, listener); + return removeMargin(paddedRegion); + }; + + return new ChunkRegionFuture(entity, makeChunksRelevant); + } + + /** + * Removes the margin added by {@link #addMargin}. + * + * @return new instance of the contained region + */ + private static BlockRegionc removeMargin(BlockRegionc relRegion) { + return relRegion.expand(-REQUIRED_CHUNK_MARGIN, -REQUIRED_CHUNK_MARGIN, -REQUIRED_CHUNK_MARGIN, + new BlockRegion(BlockRegion.INVALID)); + } + + private static Vector3ic addMargin(Vector3ic sizeInChunks) { + Vector3i desiredSize = new Vector3i(sizeInChunks); + + // FIXME: add an interface to RelevanceSystem that takes radii as inputs, + // so we don't have to reverse-engineer its rounding algorithms. + // Dimensions of relevance regions are odd-numbered so they can be symmetrical around + // their center. + desiredSize.x |= 1; + desiredSize.y |= 1; + desiredSize.z |= 1; + + // FIXME: Is the complete relevance region not actually loaded‽ + // Need a buffer on either side. + desiredSize.add(2 * REQUIRED_CHUNK_MARGIN, 2 * REQUIRED_CHUNK_MARGIN, 2 * REQUIRED_CHUNK_MARGIN); + return desiredSize; + } + + /** + * Completes when all expected chunks have loaded. + *

+ * Experimental: Unsure which objects are useful to return, I made a bunch of them available + * through this class and we return the whole thing. Though returning a future for an object + * the caller already has doesn't make a lot of sense. + * + * @return complete when all expected chunks have loaded + */ + public ListenableFuture getFuture() { + return future; + } + + @SuppressWarnings("unused") + public Set getLoadedChunks() { + return Collections.unmodifiableSet(loadedChunks); + } + + /** The entity defining the relevance region. */ + public EntityRef getEntity() { + return entity; + } + + @SuppressWarnings("unused") + public BlockRegionc getChunkRegion() { + return chunks; + } + + protected void onChunkRelevant(Chunk chunk) { + loadedChunks.add(chunk); + if (chunks.isValid()) { + logger.debug("Got chunk {} / {}", loadedChunks.size(), chunks.volume()); + if (loadedChunks.size() >= chunks.volume() && !future.isDone()) { + future.set(this); + } + } + } + + /** Adapts a {@code Consumer} to a {@code ChunkRegionListener}. */ + private static class Listener implements ChunkRegionListener { + private final Consumer onChunk; + + Listener(Consumer onChunk) { + this.onChunk = onChunk; + } + + @Override + public void onChunkRelevant(Vector3ic pos, Chunk chunk) { + onChunk.accept(chunk); + } + + @Override + public void onChunkIrrelevant(Vector3ic pos) { + // FIXME: Document why this IS, in fact, called regularly. + // We thought this would be called when the location of the entity changes, + // meaning previously-relevant chunks are no longer in range and become irrelevant. + // But in practice, we see this being called even when we aren't doing anything to + // change the location of the region's entity. +// UnsupportedOperationException error = new UnsupportedOperationException(String.format( +// "No chunks in this region should be irrelevant! That was the whole point!" + +// "Position: %s", +// pos +// )); +// if (failOnIrrelevantEvent) { +// future.setException(error); +// throw error; +// } + // logger.warn("Irrelevant???", error); + } + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java new file mode 100644 index 00000000000..768c3eb09c0 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java @@ -0,0 +1,303 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.config.Config; +import org.terasology.engine.config.SystemConfig; +import org.terasology.engine.context.Context; +import org.terasology.engine.core.GameEngine; +import org.terasology.engine.core.PathManager; +import org.terasology.engine.core.PathManagerProvider; +import org.terasology.engine.core.TerasologyConstants; +import org.terasology.engine.core.TerasologyEngine; +import org.terasology.engine.core.TerasologyEngineBuilder; +import org.terasology.engine.core.modes.GameState; +import org.terasology.engine.core.modes.StateIngame; +import org.terasology.engine.core.modes.StateLoading; +import org.terasology.engine.core.modes.StateMainMenu; +import org.terasology.engine.core.module.ModuleManager; +import org.terasology.engine.core.subsystem.EngineSubsystem; +import org.terasology.engine.core.subsystem.headless.HeadlessAudio; +import org.terasology.engine.core.subsystem.headless.HeadlessGraphics; +import org.terasology.engine.core.subsystem.headless.HeadlessInput; +import org.terasology.engine.core.subsystem.headless.HeadlessTimer; +import org.terasology.engine.core.subsystem.headless.mode.HeadlessStateChangeListener; +import org.terasology.engine.core.subsystem.lwjgl.LwjglAudio; +import org.terasology.engine.core.subsystem.lwjgl.LwjglGraphics; +import org.terasology.engine.core.subsystem.lwjgl.LwjglInput; +import org.terasology.engine.core.subsystem.lwjgl.LwjglTimer; +import org.terasology.engine.core.subsystem.openvr.OpenVRInput; +import org.terasology.engine.network.JoinStatus; +import org.terasology.engine.network.NetworkSystem; +import org.terasology.engine.registry.CoreRegistry; +import org.terasology.engine.rendering.opengl.ScreenGrabber; +import org.terasology.engine.rendering.world.viewDistance.ViewDistance; +import org.terasology.engine.testUtil.WithUnittestModule; +import org.terasology.gestalt.module.Module; +import org.terasology.gestalt.module.ModuleMetadataJsonAdapter; +import org.terasology.gestalt.module.ModuleRegistry; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Manages game engines for tests. + *

+ * There is always one engine that serves as the host. There may also be additional engines + * simulating remote clients. + *

+ * Most tests run with a single host and do not need to make direct references to this class. + *

+ * This class is available via dependency injection with the {@link org.terasology.engine.registry.In} annotation + * or as a parameter to a JUnit {@link org.junit.jupiter.api.Test} method; see {@link MTEExtension}. + * + *

Client Engine Instances

+ * Client instances can be easily created via {@link #createClient} which returns the in-game context of the created + * engine instance. When this method returns, the client will be in the {@link StateIngame} state and connected to the + * host. Currently all engine instances are headless, though it is possible to use headed engines in the future. + */ +public class Engines { + private static final Logger logger = LoggerFactory.getLogger(Engines.class); + + protected final Set dependencies = Sets.newHashSet("engine"); + protected String worldGeneratorUri = ModuleTestingEnvironment.DEFAULT_WORLD_GENERATOR; + protected boolean doneLoading; + protected Context hostContext; + protected final List engines = Lists.newArrayList(); + + PathManager pathManager; + PathManagerProvider.Cleaner pathManagerCleaner; + TerasologyEngine host; + + public Engines(Set dependencies, String worldGeneratorUri) { + this.dependencies.addAll(dependencies); + + if (worldGeneratorUri != null) { + this.worldGeneratorUri = worldGeneratorUri; + } + } + + /** + * Set up and start the engine as configured via this environment. + *

+ * Every instance should be shut down properly by calling {@link #tearDown()}. + */ + protected void setup() { + mockPathManager(); + try { + host = createHost(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + ScreenGrabber grabber = Mockito.mock(ScreenGrabber.class); + hostContext.put(ScreenGrabber.class, grabber); + CoreRegistry.put(GameEngine.class, host); + } + + /** + * Shut down a previously started testing environment. + *

+ * Used to properly shut down and clean up a testing environment set up and started with {@link #setup()}. + */ + protected void tearDown() { + engines.forEach(TerasologyEngine::shutdown); + engines.forEach(TerasologyEngine::cleanup); + engines.clear(); + try { + pathManagerCleaner.close(); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + host = null; + hostContext = null; + } + + /** + * Creates a new client and connects it to the host. + * + * @return the created client's context object + */ + public Context createClient(MainLoop mainLoop) throws IOException { + TerasologyEngine terasologyEngine = createHeadlessEngine(); + terasologyEngine.getFromEngineContext(Config.class).getRendering().setViewDistance(ViewDistance.LEGALLY_BLIND); + + terasologyEngine.changeState(new StateMainMenu()); + connectToHost(terasologyEngine, mainLoop); + Context context = terasologyEngine.getState().getContext(); + context.put(ScreenGrabber.class, hostContext.get(ScreenGrabber.class)); + return terasologyEngine.getState().getContext(); + } + + /** + * The engines active in this instance of the module testing environment. + *

+ * Engines are created for the host and connecting clients. + * + * @return list of active engines + */ + public List getEngines() { + return Lists.newArrayList(engines); + } + + /** + * Get the host context for this module testing environment. + *

+ * The host context will be null if the testing environment has not been set up via {@link #setup()} + * beforehand. + * + * @return the engine's host context, or null if not set up yet + */ + public Context getHostContext() { + return hostContext; + } + + TerasologyEngine createHeadlessEngine() throws IOException { + TerasologyEngineBuilder terasologyEngineBuilder = new TerasologyEngineBuilder(); + terasologyEngineBuilder + .add(new WithUnittestModule()) + .add(new HeadlessGraphics()) + .add(new HeadlessTimer()) + .add(new HeadlessAudio()) + .add(new HeadlessInput()); + + return createEngine(terasologyEngineBuilder); + } + + @SuppressWarnings("unused") + TerasologyEngine createHeadedEngine() throws IOException { + EngineSubsystem audio = new LwjglAudio(); + TerasologyEngineBuilder terasologyEngineBuilder = new TerasologyEngineBuilder() + .add(new WithUnittestModule()) + .add(audio) + .add(new LwjglGraphics()) + .add(new LwjglTimer()) + .add(new LwjglInput()) + .add(new OpenVRInput()); + + return createEngine(terasologyEngineBuilder); + } + + TerasologyEngine createEngine(TerasologyEngineBuilder terasologyEngineBuilder) throws IOException { + System.setProperty(ModuleManager.LOAD_CLASSPATH_MODULES_PROPERTY, "true"); + + // create temporary home paths so the MTE engines don't overwrite config/save files in your real home path + // FIXME: Collisions when attempting to do multiple simultaneous createEngines. + // (PathManager will need to be set in Context, not a process-wide global.) + Path path = Files.createTempDirectory("terasology-mte-engine"); + PathManager.getInstance().useOverrideHomePath(path); + logger.info("Created temporary engine home path: {}", path); + + // JVM will delete these on normal termination but not exceptions. + path.toFile().deleteOnExit(); + + TerasologyEngine terasologyEngine = terasologyEngineBuilder.build(); + terasologyEngine.initialize(); + registerCurrentDirectoryIfModule(terasologyEngine); + + engines.add(terasologyEngine); + return terasologyEngine; + } + + /** + * In standalone module environments (i.e. Jenkins CI builds) the CWD is the module under test. When it uses MTE it very likely needs to + * load itself as a module, but it won't be loadable from the typical path such as ./modules. This means that modules using MTE would + * always fail CI tests due to failing to load themselves. + *

+ * For these cases we try to load the CWD (via the installPath) as a module and put it in the global module registry. + *

+ * This process is based on how ModuleManagerImpl uses ModulePathScanner to scan for available modules. + */ + protected void registerCurrentDirectoryIfModule(TerasologyEngine terasologyEngine) { + Path installPath = PathManager.getInstance().getInstallPath(); + ModuleManager moduleManager = terasologyEngine.getFromEngineContext(ModuleManager.class); + ModuleRegistry registry = moduleManager.getRegistry(); + ModuleMetadataJsonAdapter metadataReader = moduleManager.getModuleMetadataReader(); + moduleManager.getModuleFactory().getModuleMetadataLoaderMap() + .put(TerasologyConstants.MODULE_INFO_FILENAME.toString(), metadataReader); + + + try { + Module module = moduleManager.getModuleFactory().createModule(installPath.toFile()); + if (module != null) { + registry.add(module); + logger.info("Added install path as module: {}", installPath); + } else { + logger.info("Install path does not appear to be a module: {}", installPath); + } + } catch (IOException e) { + logger.warn("Could not read install path as module at " + installPath); + } + } + + protected void mockPathManager() { + PathManager originalPathManager = PathManager.getInstance(); + pathManager = Mockito.spy(originalPathManager); + Mockito.when(pathManager.getModulePaths()).thenReturn(Collections.emptyList()); + pathManagerCleaner = new PathManagerProvider.Cleaner(originalPathManager, pathManager); + PathManagerProvider.setPathManager(pathManager); + } + + TerasologyEngine createHost() throws IOException { + TerasologyEngine terasologyEngine = createHeadlessEngine(); + terasologyEngine.getFromEngineContext(SystemConfig.class).writeSaveGamesEnabled.set(false); + terasologyEngine.subscribeToStateChange(new HeadlessStateChangeListener(terasologyEngine)); + terasologyEngine.changeState(new TestingStateHeadlessSetup(dependencies, worldGeneratorUri)); + + doneLoading = false; + terasologyEngine.subscribeToStateChange(() -> { + GameState newState = terasologyEngine.getState(); + logger.debug("New engine state is {}", terasologyEngine.getState()); + if (newState instanceof StateIngame) { + hostContext = newState.getContext(); + if (hostContext == null) { + logger.warn("hostContext is NULL in engine state {}", newState); + } + doneLoading = true; + } else if (newState instanceof StateLoading) { + CoreRegistry.put(GameEngine.class, terasologyEngine); + } + }); + + boolean keepTicking; + while (!doneLoading) { + keepTicking = terasologyEngine.tick(); + if (!keepTicking) { + throw new RuntimeException(String.format( + "Engine stopped ticking before we got in game. Current state: %s", + terasologyEngine.getState() + )); + } + } + return terasologyEngine; + } + + void connectToHost(TerasologyEngine client, MainLoop mainLoop) { + CoreRegistry.put(Config.class, client.getFromEngineContext(Config.class)); + JoinStatus joinStatus = null; + try { + joinStatus = client.getFromEngineContext(NetworkSystem.class).join("localhost", 25777); + } catch (InterruptedException e) { + logger.warn("Interrupted while joining: ", e); + } + + client.changeState(new StateLoading(joinStatus)); + CoreRegistry.put(GameEngine.class, client); + + // TODO: subscribe to state change and return an asynchronous result + // so that we don't need to pass mainLoop to here. + mainLoop.runUntil(() -> client.getState() instanceof StateIngame); + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/IsolatedMTEExtension.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/IsolatedMTEExtension.java new file mode 100644 index 00000000000..41aed5e1f1a --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/IsolatedMTEExtension.java @@ -0,0 +1,19 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +/** + * Subclass of {@link MTEExtension} which isolates all test cases by creating a new engine for each test. This is much + * slower since it runs the startup and shutdown process for all tests. You should use {@link MTEExtension} unless + * you're certain that you need to use this class. + *

+ * Use this within {@link org.junit.jupiter.api.extension.ExtendWith} + */ +public class IsolatedMTEExtension extends MTEExtension { + { + // Resources are not shared between namespaces. We increase isolation by using a different + // namespace for every test method. + helperLifecycle = Scopes.PER_METHOD; + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MTEExtension.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MTEExtension.java new file mode 100644 index 00000000000..033ed0f7826 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MTEExtension.java @@ -0,0 +1,246 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +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.extension.Dependencies; +import org.terasology.engine.integrationenvironment.extension.UseWorldGenerator; +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; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +/** + * Sets up a Terasology environment for use with your {@index JUnit} 5 test. + *

+ * Supports Terasology's DI as in usual Systems. You can inject Managers via {@link In} annotation, constructors or + * test parameters. Also you can inject {@link MainLoop} or {@link Engines} to interact with the environment's engine. + *

+ * Example: + *


+ * import org.junit.jupiter.api.extension.ExtendWith;
+ * import org.junit.jupiter.api.Test;
+ * import org.terasology.engine.registry.In;
+ *
+ * @{@link org.junit.jupiter.api.extension.ExtendWith}(MTEExtension.class)
+ * @{@link Dependencies}("MyModule")
+ * @{@link UseWorldGenerator}("Pathfinding:pathfinder")
+ * public class ExampleTest {
+ *
+ *     @In
+ *     EntityManager entityManager;
+ *
+ *     @In
+ *     {@link MainLoop} mainLoop;
+ *
+ *     @Test
+ *     public void testSomething() {
+ *         // …
+ *     }
+ *
+ *     // Injection is also applied to the parameters of individual tests:
+ *     @Test
+ *     public void testSomething({@link Engines} engines, WorldProvider worldProvider) {
+ *         // …
+ *     }
+ * }
+ * 
+ *

+ * You can configure the environment with these additional annotations: + *

+ *
{@link Dependencies @Dependencies}
+ *
Specify which modules to include in the environment. Put the name of your module under test here. + * Any dependencies these modules declare in module.txt will be pulled in as well.
+ *
{@link UseWorldGenerator @UseWorldGenerator}
+ *
The URN of the world generator to use. Defaults to {@link DummyWorldGenerator}, + * a flat world.
+ *
+ * + *

+ * Every class annotated with this will create a single instance of {@link Engines} and use it during execution of + * all tests in the class. This also means that all engine instances are shared between all tests in the class. If you + * want isolated engine instances try {@link IsolatedMTEExtension}. + *

+ * Note that classes marked {@link Nested} will share the engine context with their parent. + *

+ * 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 { + + static final String LOGBACK_RESOURCE = "default-logback.xml"; + protected Function helperLifecycle = Scopes.PER_CLASS; + protected Function> 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(); + Engines engines = getEngines(extensionContext); + return engines.getHostContext().get(type) != null + || type.isAssignableFrom(Engines.class) + || type.isAssignableFrom(MainLoop.class) + || type.isAssignableFrom(ModuleTestingHelper.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + Engines engines = getEngines(extensionContext); + Class type = parameterContext.getParameter().getType(); + + return getDIInstance(engines, type); + } + + private Object getDIInstance(Engines engines, Class type) { + if (type.isAssignableFrom(Engines.class)) { + return engines; + } else if (type.isAssignableFrom(MainLoop.class)) { + return new MainLoop(engines); + } else if (type.isAssignableFrom(ModuleTestingHelper.class)) { + return new ModuleTestingHelper(engines); + } else { + return engines.getHostContext().get(type); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) { + Engines engines = getEngines(extensionContext); + List exceptionList = new LinkedList<>(); + Class type = testInstance.getClass(); + while (type != null) { + Arrays.stream(type.getDeclaredFields()) + .filter((field) -> field.getAnnotation(In.class) != null) + .peek((field) -> field.setAccessible(true)) + .forEach((field) -> { + Object candidateObject = getDIInstance(engines, field.getType()); + try { + field.set(testInstance, candidateObject); + } catch (IllegalAccessException e) { + exceptionList.add(e); + } + }); + + type = type.getSuperclass(); + } + // It is tests, then it is legal ;) + if (!exceptionList.isEmpty()) { + throw new MultipleFailuresError("I cannot provide DI instances:", exceptionList); + } + } + + public String getWorldGeneratorUri(ExtensionContext context) { + UseWorldGenerator useWorldGenerator = getTestClass.apply(context).getAnnotation(UseWorldGenerator.class); + return useWorldGenerator != null ? useWorldGenerator.value() : null; + } + + public Set getDependencyNames(ExtensionContext context) { + Dependencies dependencies = getTestClass.apply(context).getAnnotation(Dependencies.class); + return dependencies != null ? Sets.newHashSet(dependencies.value()) : Collections.emptySet(); + } + + /** + * Get the Engines for this test. + *

+ * The new Engines instance is configured using the {@link Dependencies} and {@link UseWorldGenerator} + * annotations for the test class. + *

+ * This will create a new instance when necessary. It will be stored in the + * {@link ExtensionContext} for reuse between tests that wish to avoid the expense of creating a new + * instance every time, and will be disposed of when the context closes. + * + * @param context for the current test + * @return configured for this test + */ + protected Engines getEngines(ExtensionContext context) { + ExtensionContext.Store store = context.getStore(helperLifecycle.apply(context)); + EnginesCleaner autoCleaner = store.getOrComputeIfAbsent( + EnginesCleaner.class, k -> new EnginesCleaner(getDependencyNames(context), getWorldGeneratorUri(context)), + EnginesCleaner.class); + return autoCleaner.engines; + } + + /** + * Apply our default logback configuration to the logger. + *

+ * Modules won't generally have their own logback-test.xml, so we'll install ours from {@value LOGBACK_RESOURCE}. + *

+ * TODO: + *

    + *
  • Only reset the current LoggerContext if it really hasn't been customized by elsewhere. + *
  • When there are multiple classes with MTEExtension, do we end up doing this repeatedly + * in the same process? + *
  • 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. + *
+ */ + 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. + *

+ * Implements {@link ExtensionContext.Store.CloseableResource CloseableResource} to dispose of + * the {@link Engines} when the context is closed. + */ + static class EnginesCleaner implements ExtensionContext.Store.CloseableResource { + protected Engines engines; + + EnginesCleaner(Set dependencyNames, String worldGeneratorUri) { + engines = new Engines(dependencyNames, worldGeneratorUri); + engines.setup(); + } + + @Override + public void close() { + engines.tearDown(); + engines = null; + } + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java new file mode 100644 index 00000000000..6286af0962e --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java @@ -0,0 +1,248 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.base.Preconditions; +import com.google.common.base.Verify; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.common.util.concurrent.UncheckedTimeoutException; +import org.joml.Matrix4f; +import org.joml.RoundingMode; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.terasology.engine.core.TerasologyEngine; +import org.terasology.engine.core.Time; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.block.BlockRegion; +import org.terasology.engine.world.block.BlockRegionc; +import org.terasology.engine.world.chunks.Chunks; +import org.terasology.engine.world.chunks.localChunkProvider.RelevanceSystem; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +/** + * Methods to run the main loop of the game. + *

+ * Engines can be run while a condition is true via {@link #runWhile(Supplier)}
{@code mainLoop.runWhile(()-> true);} + *

+ * or conversely run until a condition is true via {@link #runUntil(Supplier)}
{@code mainLoop.runUntil(()-> false);} + *

+ * Test scenarios which take place in a particular location of the world must first make sure that location is loaded + * with {@link #makeBlocksRelevant makeBlocksRelevant} or {@link #makeChunksRelevant makeChunksRelevant}. + *


+ *     // Load everything within 40 blocks of x=123, z=456
+ *     mainLoop.runUntil(makeBlocksRelevant(
+ *         new BlockRegion(123, SURFACE_HEIGHT, 456).expand(40, 40, 40)));
+ * 
+ *

+ * {@link MTEExtension} provides tests with a game engine, configured with a module environment + * and a world. The engine is ready by the time a test method is executed, but does not run + * until you use one of these methods. + *

+ * If there are multiple engines (a host and one or more clients), they will tick in a round-robin fashion. + *

+ * This class is available via dependency injection with the {@link org.terasology.engine.registry.In} annotation + * or as a parameter to a JUnit {@link org.junit.jupiter.api.Test} method; see {@link MTEExtension}. + */ +public class MainLoop { + // TODO: Can we get rid of this by making sure our main loop is compatible with JUnit's timeout spec? + long safetyTimeoutMs = ModuleTestingEnvironment.DEFAULT_SAFETY_TIMEOUT; + + private final Engines engines; + + public MainLoop(Engines engines) { + this.engines = engines; + } + + /** + * Creates a dummy entity with RelevanceRegion component to force a chunk's generation and availability. Blocks while waiting for the + * chunk to become loaded + * + * @param blockPos the block position of the dummy entity. Only the chunk containing this position will be available + */ + public void forceAndWaitForGeneration(Vector3ic blockPos) { + WorldProvider worldProvider = engines.getHostContext().get(WorldProvider.class); + if (worldProvider.isBlockRelevant(blockPos)) { + return; + } + + ListenableFuture chunkRegion = makeBlocksRelevant(new BlockRegion(blockPos)); + runUntil(chunkRegion); + } + + /** + * Makes sure the area containing these blocks is loaded. + *

+ * This method is asynchronous. Pass the result to {@link #runUntil(ListenableFuture)} if you need to wait until the area is ready. + * + * @see #makeChunksRelevant(BlockRegion) makeChunksRelevant if you have chunk coordinates instead of block coordinates. + * + * @param blocks blocks to mark as relevant + * @return relevant chunks + */ + public ListenableFuture makeBlocksRelevant(BlockRegionc blocks) { + BlockRegion desiredChunkRegion = Chunks.toChunkRegion(new BlockRegion(blocks)); + return makeChunksRelevant(desiredChunkRegion, blocks.center(new Vector3f())); + } + + /** + * Makes sure the area containing these chunks is loaded. + *

+ * This method is asynchronous. Pass the result to {@link #runUntil(ListenableFuture)} if you need to wait until the area is ready. + * + * @see #makeBlocksRelevant(BlockRegionc) makeBlocksRelevant if you have block coordinates instead of chunk coordinates. + * + * @param chunks to mark as relevant + * @return relevant chunks + */ + @SuppressWarnings("unused") + public ListenableFuture makeChunksRelevant(BlockRegion chunks) { + // Pick a central point (in block coordinates). + Vector3f centerPoint = chunkRegionToNewBlockRegion(chunks).center(new Vector3f()); + + return makeChunksRelevant(chunks, centerPoint); + } + + public ListenableFuture makeChunksRelevant(BlockRegion chunks, Vector3fc centerBlock) { + Preconditions.checkArgument(chunks.contains(Chunks.toChunkPos(new Vector3i(centerBlock, RoundingMode.FLOOR))), + "centerBlock should %s be within the region %s", + centerBlock, chunkRegionToNewBlockRegion(chunks)); + Vector3i desiredSize = chunks.getSize(new Vector3i()); + + EntityManager entityManager = Verify.verifyNotNull(engines.getHostContext().get(EntityManager.class)); + RelevanceSystem relevanceSystem = Verify.verifyNotNull(engines.getHostContext().get(RelevanceSystem.class)); + ChunkRegionFuture listener = ChunkRegionFuture.create(entityManager, relevanceSystem, centerBlock, desiredSize); + return listener.getFuture(); + } + + BlockRegionc chunkRegionToNewBlockRegion(BlockRegionc chunks) { + BlockRegion blocks = new BlockRegion(chunks); + return blocks.transform(new Matrix4f().scaling(new Vector3f(Chunks.CHUNK_SIZE))); + } + + /** + * Runs until this future is complete. + * + * @return the result of the future + */ + public T runUntil(ListenableFuture future) { + boolean timedOut = runUntil(future::isDone); + if (timedOut) { + // TODO: if runUntil returns timedOut but does not throw an exception, it + // means it hit DEFAULT_GAME_TIME_TIMEOUT but not SAFETY_TIMEOUT, and + // that's a weird interface due for a revision. + future.cancel(true); // let it know we no longer expect results + throw new UncheckedTimeoutException("No result within default timeout."); + } + try { + return future.get(0, TimeUnit.SECONDS); + } catch (ExecutionException e) { + throw new UncheckedExecutionException(e); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while waiting for " + future, e); + } catch (TimeoutException e) { + throw new UncheckedTimeoutException( + "Checked isDone before calling get, so this shouldn't happen.", e); + } + } + + /** + * Runs tick() on the engine until f evaluates to true or DEFAULT_GAME_TIME_TIMEOUT milliseconds have passed in game time + * + * @return true if execution timed out + */ + public boolean runUntil(Supplier f) { + return runWhile(() -> !f.get()); + } + + /** + * Runs tick() on the engine until f evaluates to true or gameTimeTimeoutMs has passed in game time + * + * @return true if execution timed out + */ + public boolean runUntil(long gameTimeTimeoutMs, Supplier f) { + return runWhile(gameTimeTimeoutMs, () -> !f.get()); + } + + /** + * Runs tick() on the engine while f evaluates to true or until DEFAULT_GAME_TIME_TIMEOUT milliseconds have passed + * + * @return true if execution timed out + */ + public boolean runWhile(Supplier f) { + return runWhile(ModuleTestingEnvironment.DEFAULT_GAME_TIME_TIMEOUT, f); + } + + /** + * Runs tick() on the engine while f evaluates to true or until gameTimeTimeoutMs has passed in game time. + * + * @return true if execution timed out + */ + public boolean runWhile(long gameTimeTimeoutMs, Supplier f) { + boolean timedOut = false; + Time hostTime = engines.getHostContext().get(Time.class); + long startRealTime = System.currentTimeMillis(); + long startGameTime = hostTime.getGameTimeInMs(); + + while (f.get() && !timedOut) { + Thread.yield(); + if (Thread.currentThread().isInterrupted()) { + throw new RuntimeException(String.format("Thread %s interrupted while waiting for %s.", + Thread.currentThread(), f)); + } + for (TerasologyEngine terasologyEngine : engines.getEngines()) { + boolean keepRunning = terasologyEngine.tick(); + if (!keepRunning && terasologyEngine == engines.host) { + throw new RuntimeException("Host has shut down: " + engines.host.getStatus()); + } + } + + // handle safety timeout + if (System.currentTimeMillis() - startRealTime > safetyTimeoutMs) { + timedOut = true; + // If we've passed the _safety_ timeout, throw an exception. + throw new UncheckedTimeoutException("MTE Safety timeout exceeded. See setSafetyTimeoutMs()"); + } + + // handle game time timeout + if (hostTime.getGameTimeInMs() - startGameTime > gameTimeTimeoutMs) { + // If we've passed the user-specified timeout but are still under the + // safety threshold, set timed-out status without throwing. + timedOut = true; + } + } + + return timedOut; + } + + /** + * @return the current safety timeout + */ + public long getSafetyTimeoutMs() { + return safetyTimeoutMs; + } + + /** + * Sets the safety timeout (default 30s). + * + * @param safetyTimeoutMs The safety timeout applies to {@link #runWhile runWhile} and related helpers, and stops execution when + * the specified number of real time milliseconds has passed. Note that this is different from the timeout parameter of those + * methods, which is specified in game time. + *

+ * When a single {@code run*} helper invocation exceeds the safety timeout, MTE asserts false to explicitly fail the test. + *

+ * The safety timeout exists to prevent indefinite execution in Jenkins or long IDE test runs, and should be adjusted as needed + * so that tests pass reliably in all environments. + */ + public void setSafetyTimeoutMs(long safetyTimeoutMs) { + this.safetyTimeoutMs = safetyTimeoutMs; + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironment.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironment.java new file mode 100644 index 00000000000..44d279dccbb --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironment.java @@ -0,0 +1,118 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.util.concurrent.ListenableFuture; +import org.joml.Vector3fc; +import org.joml.Vector3ic; +import org.terasology.engine.context.Context; +import org.terasology.engine.core.TerasologyEngine; +import org.terasology.engine.world.block.BlockRegion; +import org.terasology.engine.world.block.BlockRegionc; + +import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; + +/** + * The public methods that were available via ModuleTestingHelper v0.3.2. + */ +public interface ModuleTestingEnvironment { + long DEFAULT_SAFETY_TIMEOUT = 60000; + long DEFAULT_GAME_TIME_TIMEOUT = 30000; + String DEFAULT_WORLD_GENERATOR = "unittest:dummy"; + + /** + * Creates a dummy entity with RelevanceRegion component to force a chunk's generation and availability. Blocks while waiting for the + * chunk to become loaded + * + * @param blockPos the block position of the dummy entity. Only the chunk containing this position will be available + */ + void forceAndWaitForGeneration(Vector3ic blockPos); + + /** + * @param blocks blocks to mark as relevant + * @return relevant chunks + */ + ListenableFuture makeBlocksRelevant(BlockRegionc blocks); + + ListenableFuture makeChunksRelevant(BlockRegion chunks); + + ListenableFuture makeChunksRelevant(BlockRegion chunks, Vector3fc centerBlock); + + T runUntil(ListenableFuture future); + + /** + * Runs tick() on the engine until f evaluates to true or DEFAULT_GAME_TIME_TIMEOUT milliseconds have passed in game time + * + * @return true if execution timed out + */ + boolean runUntil(Supplier f); + + /** + * Runs tick() on the engine until f evaluates to true or gameTimeTimeoutMs has passed in game time + * + * @return true if execution timed out + */ + boolean runUntil(long gameTimeTimeoutMs, Supplier f); + + /** + * Runs tick() on the engine while f evaluates to true or until DEFAULT_GAME_TIME_TIMEOUT milliseconds have passed + * + * @return true if execution timed out + */ + boolean runWhile(Supplier f); + + /** + * Runs tick() on the engine while f evaluates to true or until gameTimeTimeoutMs has passed in game time. + * + * @return true if execution timed out + */ + boolean runWhile(long gameTimeTimeoutMs, Supplier f); + + /** + * Creates a new client and connects it to the host + * + * @return the created client's context object + */ + Context createClient() throws IOException; + + /** + * The engines active in this instance of the module testing environment. + *

+ * Engines are created for the host and connecting clients. + * + * @return list of active engines + */ + List getEngines(); + + /** + * Get the host context for this module testing environment. + *

+ * The host context will be null if the testing environment has not been set up via {@link Engines#setup()} + * beforehand. + * + * @return the engine's host context, or null if not set up yet + */ + Context getHostContext(); + + /** + * @return the current safety timeout + */ + long getSafetyTimeoutMs(); + + /** + * Sets the safety timeout (default 30s). + * + * @param safetyTimeoutMs The safety timeout applies to {@link #runWhile runWhile} and related helpers, and stops execution when + * the specified number of real time milliseconds has passed. Note that this is different from the timeout parameter of those + * methods, which is specified in game time. + *

+ * When a single {@code run*} helper invocation exceeds the safety timeout, MTE asserts false to explicitly fail the test. + *

+ * The safety timeout exists to prevent indefinite execution in Jenkins or long IDE test runs, and should be adjusted as needed + * so that tests pass reliably in all environments. + */ + void setSafetyTimeoutMs(long safetyTimeoutMs); +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java new file mode 100644 index 00000000000..d864f59e1f9 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java @@ -0,0 +1,108 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import com.google.common.util.concurrent.ListenableFuture; +import org.joml.Vector3fc; +import org.joml.Vector3ic; +import org.terasology.engine.context.Context; +import org.terasology.engine.core.TerasologyEngine; +import org.terasology.engine.core.modes.StateIngame; +import org.terasology.engine.world.block.BlockRegion; +import org.terasology.engine.world.block.BlockRegionc; + +import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; + +/** + * Methods for interacting with the engine in the test environment. + *

+ * Most tests only need the methods of {@link MainLoop}. Expect this class to be deprecated after we figure out better + * asynchronous methods for {@link #createClient()}. + * + *

Client Engine Instances

+ * Client instances can be easily created via {@link #createClient()} which returns the in-game context of the created + * engine instance. When this method returns, the client will be in the {@link StateIngame} state and connected to the + * host. Currently all engine instances are headless, though it is possible to use headed engines in the future. + */ +public class ModuleTestingHelper implements ModuleTestingEnvironment { + + final Engines engines; + final MainLoop mainLoop; + + ModuleTestingHelper(Engines engines) { + this.engines = engines; + this.mainLoop = new MainLoop(engines); + } + + @Override + public void forceAndWaitForGeneration(Vector3ic blockPos) { + mainLoop.forceAndWaitForGeneration(blockPos); + } + + @Override + public ListenableFuture makeBlocksRelevant(BlockRegionc blocks) { + return mainLoop.makeBlocksRelevant(blocks); + } + + @Override + public ListenableFuture makeChunksRelevant(BlockRegion chunks) { + return mainLoop.makeChunksRelevant(chunks); + } + + @Override + public ListenableFuture makeChunksRelevant(BlockRegion chunks, Vector3fc centerBlock) { + return mainLoop.makeChunksRelevant(chunks, centerBlock); + } + + @Override + public T runUntil(ListenableFuture future) { + return mainLoop.runUntil(future); + } + + @Override + public boolean runUntil(Supplier f) { + return mainLoop.runUntil(f); + } + + @Override + public boolean runUntil(long gameTimeTimeoutMs, Supplier f) { + return mainLoop.runUntil(gameTimeTimeoutMs, f); + } + + @Override + public boolean runWhile(Supplier f) { + return mainLoop.runWhile(f); + } + + @Override + public boolean runWhile(long gameTimeTimeoutMs, Supplier f) { + return mainLoop.runWhile(gameTimeTimeoutMs, f); + } + + @Override + public Context createClient() throws IOException { + return engines.createClient(mainLoop); + } + + @Override + public List getEngines() { + return engines.getEngines(); + } + + @Override + public Context getHostContext() { + return engines.getHostContext(); + } + + @Override + public long getSafetyTimeoutMs() { + return mainLoop.getSafetyTimeoutMs(); + } + + @Override + public void setSafetyTimeoutMs(long safetyTimeoutMs) { + mainLoop.setSafetyTimeoutMs(safetyTimeoutMs); + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java new file mode 100644 index 00000000000..54f0f47cc97 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java @@ -0,0 +1,42 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.collect.ObjectArrays; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.util.function.Function; + +public final class Scopes { + + /** One per top-level test class. */ + public static final Function PER_CLASS = context -> mteNamespace(getTopTestClass(context)); + + /** One per test method. */ + public static final Function PER_METHOD = context -> mteNamespace(context.getTestMethod()); + + private Scopes() { }; + + static ExtensionContext.Namespace mteNamespace(Object... parts) { + // Start with this Extension, so it's clear where this came from. + return ExtensionContext.Namespace.create(ObjectArrays.concat(MTEExtension.class, parts)); + } + + /** + * The outermost class defining this test. + *

+ * For nested tests, this + * returns the outermost class in which this test is nested. + *

+ * Most tests are not nested, in which case this returns the class defining the test. + * + * @param context for the current test + * @return a test class + */ + public static Class getTopTestClass(ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + return testClass.isAnnotationPresent(Nested.class) ? testClass.getEnclosingClass() : testClass; + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestEventReceiver.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestEventReceiver.java new file mode 100644 index 00000000000..48bee462870 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestEventReceiver.java @@ -0,0 +1,162 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import com.google.common.collect.Lists; +import org.terasology.engine.context.Context; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.entity.internal.EntityInfoComponent; +import org.terasology.engine.entitySystem.event.internal.EventReceiver; +import org.terasology.engine.entitySystem.event.internal.EventSystem; +import org.terasology.gestalt.entitysystem.component.Component; +import org.terasology.gestalt.entitysystem.event.Event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * Test helper for listening to {@link Event}s + *

+ * A test should instantiate a {@code TestEventReceiver}, execute some code, and then examine the list of entityRefs or + * events provided by {@link #getEntityRefs()} and {@link #getEvents()}. + *

+ * The receiver automatically collects events of the given type sent to its {@link Context}. + * + *

+ * {@code
+ * TestEventReceiver dropReceiver = new TestEventReceiver<>(getHostContext(), DropItemEvent.class)
+ * // fire some events
+ * for (DropItemEvent event : dropReceiver.getEvents()) {
+ *   // do something with the events
+ * }
+ * }
+ * 
+ * Users can optionally supply a {@link BiConsumer} to handle the events with custom logic. + *
+ * {@code
+ * TestEventReceiver receiver = new TestEventReceiver<>(context, DropItemEvent.class, (event, entity) -> {
+ *   // do something with the event or entity
+ * });
+ * }
+ * 
+ * Additionally, a list of required component types can be passed to the event receiver. This is equivalent + * to listing the components in the {@code ReceiveEvent(components = {...}} block of a regular event handler. + *
+ * {@code
+ * TestEventReceiver receiver = new TestEventReceiver<>(context, DropItemEvent.class, (event, entity) -> {
+ *   // do something with the event or entity
+ * }, MagicItemComponent.class);
+ * }
+ * 
+ *

+ * You can automatically unregister your receiver using a try-with-resources block ({@code TestEventReceiver} is {@link + * AutoCloseable}): + * + *

+ * {@code
+ * try (TestEventReceiver spy = new TestEventReceiver<>(getHostContext(), DropItemEvent.class)) {
+ *   drops = spy.getEntityRefs();
+ * }
+ * }
+ * 
+ *

+ * Note that listeners are discarded with the rest of the engine between tests, so closing your receiver is only useful + * if you need to stop handling events within a single test method. + */ +public class TestEventReceiver implements AutoCloseable, EventReceiver { + private final EventSystem eventSystem; + private final Class eventClass; + private final BiConsumer handler; + + private final List entityRefs = new ArrayList<>(); + private final List events = new ArrayList<>(); + + /** + * Constructs a new {@code TestEventReceiver} and registers it to listen for events. + *

+ * The following signature of a {@code TestEventReceiver} is equivalent to the event handler below: + *

+     * TestEventReceiver receiver = new TestEventReceiver<>(context, MyEvent.class, (event, entity) -> {
+     *   // do something with the event or entity
+     * }, MyComponent.class);
+     *
+     * // ... corresponds to
+     *
+     * @ReceiveEvent(components = {MyComponent.class})
+     * public void handler(MyEvent event, EntityRef entity) {
+     *     // do something with the event or entity
+     * }
+     * 
+ * + * @param context the context object for the test; this should probably be obtained through {@link + * ModuleTestingHelper#createClient()} and is needed so we can obtain an {@link EventSystem} instance to + * register our event handler. + * @param eventClass the {@link Event} subclass to listen for + * @param handler an optional {@link BiConsumer} fired when events are received + * @param componentTypes list of component types that need to be present on the entity receiving the event + */ + public TestEventReceiver(Context context, Class eventClass, BiConsumer handler, Class... componentTypes) { + this.eventClass = eventClass; + this.handler = handler; + eventSystem = context.get(EventSystem.class); + + Class[] components = + Lists.asList(EntityInfoComponent.class, componentTypes).toArray(new Class[componentTypes.length + 1]); + + eventSystem.registerEventReceiver(this, eventClass, components); + } + + /** + * @see #TestEventReceiver(Context, Class, BiConsumer, Class[]) + */ + public TestEventReceiver(Context context, Class eventClass) { + this(context, eventClass, (event, entity) -> { + }); + } + + /** + * Unregisters this {@code TestEventReceiver} so it stops listening for events. + */ + public void close() { + eventSystem.unregisterEventReceiver(this, eventClass, EntityInfoComponent.class); + } + + /** + * Returns a read-only view of the list of entities which are sent events. + *

+ * Note that entities appear in the order they received the events, and may appear multiple times. Each entity + * corresponds to the {@link #getEvents()} member with the same index. + *

+ * If the {@code TestEventReceiver} has not been {@linkplain #close() closed}, then this list will continue to be + * updated if further events occur. + */ + public List getEntityRefs() { + return Collections.unmodifiableList(entityRefs); + } + + /** + * Returns a read-only view of the list of events. + *

+ * If the {@code TestEventReceiver} has not been {@linkplain #close() closed}, then this list will continue to be + * updated if further events occur. + */ + public List getEvents() { + return Collections.unmodifiableList(events); + } + + /** + * Records the event. + *

+ * Note that this doesn't put the entity in an inventory or otherwise interfere with the event itself, but it does + * store a reference to the entity and event. Consequently, the entity still exists in the world, and if other + * actors modify or destroy it, those changes would be reflected in the list of entityRefs. + */ + public void onEvent(T event, EntityRef entity) { + handler.accept(event, entity); + events.add(event); + entityRefs.add(entity); + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java new file mode 100644 index 00000000000..ca7b70d48ea --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java @@ -0,0 +1,72 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.config.Config; +import org.terasology.engine.config.ModuleConfig; +import org.terasology.engine.config.WorldGenerationConfig; +import org.terasology.engine.core.GameEngine; +import org.terasology.engine.core.SimpleUri; +import org.terasology.engine.core.TerasologyConstants; +import org.terasology.engine.core.TerasologyEngine; +import org.terasology.engine.core.subsystem.headless.mode.StateHeadlessSetup; +import org.terasology.engine.game.GameManifest; +import org.terasology.engine.world.time.WorldTime; +import org.terasology.gestalt.naming.Name; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkArgument; + +public class TestingStateHeadlessSetup extends StateHeadlessSetup { + private static final Logger logger = LoggerFactory.getLogger(TestingStateHeadlessSetup.class); + + static final Name MTE_MODULE_NAME = new Name("ModuleTestingEnvironment"); + static final String WORLD_TITLE = "testworld"; + static final String DEFAULT_SEED = "seed"; + + private final Collection dependencies; + private final SimpleUri worldGeneratorUri; + public TestingStateHeadlessSetup(Collection dependencies, String worldGeneratorUri) { + this.dependencies = dependencies; + this.worldGeneratorUri = new SimpleUri(worldGeneratorUri); + checkArgument(this.worldGeneratorUri.isValid(), "Not a valid URI `%s`", worldGeneratorUri); + } + + void configForTest(Config config) { + Set dependencyNames = dependencies.stream().map(Name::new).collect(Collectors.toSet()); + + // Include the MTE module to provide world generators and suchlike. + dependencyNames.add(MTE_MODULE_NAME); + + ModuleConfig moduleSelection = config.getDefaultModSelection(); + moduleSelection.clear(); + dependencyNames.forEach(moduleSelection::addModule); + + WorldGenerationConfig worldGenerationConfig = config.getWorldGeneration(); + worldGenerationConfig.setDefaultGenerator(worldGeneratorUri); + worldGenerationConfig.setWorldTitle(WORLD_TITLE); + worldGenerationConfig.setDefaultSeed(DEFAULT_SEED); + } + + @Override + public GameManifest createGameManifest() { + GameManifest gameManifest = super.createGameManifest(); + + float timeOffset = 0.25f + 0.025f; // Time at dawn + little offset to spawn in a brighter env. + gameManifest.getWorldInfo(TerasologyConstants.MAIN_WORLD).setTime((long) (WorldTime.DAY_LENGTH * timeOffset)); + return gameManifest; + } + + @Override + public void init(GameEngine engine) { + // We want to modify Config before super.init calls createGameManifest, but the child context + // does not exist before we call super.init. + configForTest(((TerasologyEngine) engine).getFromEngineContext(Config.class)); + super.init(engine); + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/Dependencies.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/Dependencies.java new file mode 100644 index 00000000000..280ae2e55ca --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/Dependencies.java @@ -0,0 +1,25 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment.extension; + +import org.terasology.engine.integrationenvironment.MTEExtension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares the modules to load in the environment. + * + * @see MTEExtension + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Dependencies { + /** + * Names of modules, as defined by the id in their module.txt. + */ + String[] value(); +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/UseWorldGenerator.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/UseWorldGenerator.java new file mode 100644 index 00000000000..ff3cd63b1b0 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/UseWorldGenerator.java @@ -0,0 +1,26 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment.extension; + +import org.terasology.engine.integrationenvironment.MTEExtension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares which {@index "world generator"} to use. + * + * @see MTEExtension + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface UseWorldGenerator { + + /** + * The URN of the world generator, e.g. "CoreWorlds:facetedPerlin" + */ + String value(); +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/fixtures/BaseTestingClass.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/fixtures/BaseTestingClass.java new file mode 100644 index 00000000000..2d42b7f3a12 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/fixtures/BaseTestingClass.java @@ -0,0 +1,25 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment.fixtures; + +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.integrationenvironment.ModuleTestingHelper; +import org.terasology.engine.registry.In; + +// A dummy class for testing injection of super class fields +public class BaseTestingClass { + @In + private EntityManager entityManager; + + @In + private ModuleTestingHelper helper; + + public EntityManager getEntityManager() { + return entityManager; + } + + public ModuleTestingHelper getHelper() { + return helper; + } +} diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java new file mode 100644 index 00000000000..2aeec8381fb --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java @@ -0,0 +1,16 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Provides a Terasology engine for use with unit tests. + *

+ * Key points of interest for test authors are: + *

    + *
  • {@link org.terasology.engine.integrationenvironment.MTEExtension MTEExtension}: Use this on your JUnit 5 test classes. + *
  • {@link org.terasology.engine.integrationenvironment.MainLoop MainLoop}: Methods for running the engine during your test scenarios. + *
  • {@link org.terasology.engine.integrationenvironment.Engines}: You can add additional engines to simulate remote connections to the + * host. [Experimental] + *
+ */ +package org.terasology.engine.integrationenvironment; + diff --git a/engine-tests/src/main/java/org/terasology/unittest/stubs/DummyComponent.java b/engine-tests/src/main/java/org/terasology/unittest/stubs/DummyComponent.java new file mode 100644 index 00000000000..3387d669f6d --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/unittest/stubs/DummyComponent.java @@ -0,0 +1,17 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.unittest.stubs; + + +import org.terasology.gestalt.entitysystem.component.Component; + +public class DummyComponent implements Component { + public boolean eventReceived = false; + public String name; + + @Override + public void copyFrom(DummyComponent other) { + eventReceived = other.eventReceived; + name = other.name; + } +} diff --git a/engine-tests/src/main/java/org/terasology/unittest/stubs/DummyEvent.java b/engine-tests/src/main/java/org/terasology/unittest/stubs/DummyEvent.java new file mode 100644 index 00000000000..0860ae9ef4a --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/unittest/stubs/DummyEvent.java @@ -0,0 +1,7 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.unittest.stubs; + +import org.terasology.gestalt.entitysystem.event.Event; + +public class DummyEvent implements Event { } diff --git a/engine-tests/src/main/java/org/terasology/unittest/stubs/DummySystem.java b/engine-tests/src/main/java/org/terasology/unittest/stubs/DummySystem.java new file mode 100644 index 00000000000..a5ea4f838ae --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/unittest/stubs/DummySystem.java @@ -0,0 +1,20 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.unittest.stubs; + +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.systems.BaseComponentSystem; +import org.terasology.engine.entitySystem.systems.RegisterMode; +import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.registry.Share; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; + +@Share(DummySystem.class) +@RegisterSystem(RegisterMode.AUTHORITY) +public class DummySystem extends BaseComponentSystem { + @ReceiveEvent + public void onDummyEvent(DummyEvent event, EntityRef entity, DummyComponent component) { + component.eventReceived = true; + entity.saveComponent(component); + } +} diff --git a/engine-tests/src/main/java/org/terasology/unittest/worlds/DummyWorldGenerator.java b/engine-tests/src/main/java/org/terasology/unittest/worlds/DummyWorldGenerator.java new file mode 100644 index 00000000000..248074b0f18 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/unittest/worlds/DummyWorldGenerator.java @@ -0,0 +1,29 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.unittest.worlds; + +import org.terasology.engine.core.SimpleUri; +import org.terasology.engine.registry.In; +import org.terasology.engine.world.generation.BaseFacetedWorldGenerator; +import org.terasology.engine.world.generation.WorldBuilder; +import org.terasology.engine.world.generator.RegisterWorldGenerator; +import org.terasology.engine.world.generator.plugin.WorldGeneratorPluginLibrary; + +@RegisterWorldGenerator(id = "dummy", displayName = "dummy") +public class DummyWorldGenerator extends BaseFacetedWorldGenerator { + public static final int SURFACE_HEIGHT = 40; + + @In + private WorldGeneratorPluginLibrary worldGeneratorPluginLibrary; + + public DummyWorldGenerator(SimpleUri uri) { + super(uri); + } + + @Override + protected WorldBuilder createWorld() { + return new WorldBuilder(worldGeneratorPluginLibrary) + .addProvider(new FlatSurfaceHeightProvider(SURFACE_HEIGHT)) + .addPlugins(); + } +} diff --git a/engine-tests/src/main/java/org/terasology/unittest/worlds/EmptyWorldGenerator.java b/engine-tests/src/main/java/org/terasology/unittest/worlds/EmptyWorldGenerator.java new file mode 100644 index 00000000000..fb14c9dd184 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/unittest/worlds/EmptyWorldGenerator.java @@ -0,0 +1,38 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.unittest.worlds; + +import org.terasology.engine.core.SimpleUri; +import org.terasology.engine.registry.In; +import org.terasology.engine.world.generation.BaseFacetedWorldGenerator; +import org.terasology.engine.world.generation.WorldBuilder; +import org.terasology.engine.world.generator.RegisterWorldGenerator; +import org.terasology.engine.world.generator.plugin.WorldGeneratorPlugin; +import org.terasology.engine.world.generator.plugin.WorldGeneratorPluginLibrary; + +import java.util.ArrayList; +import java.util.List; + +/** + * Bare World Generator to generating empty chunks for testing. + */ +@RegisterWorldGenerator(id = "empty", displayName = "empty") +public class EmptyWorldGenerator extends BaseFacetedWorldGenerator { + @In + private WorldGeneratorPluginLibrary worldGeneratorPluginLibrary; + + public EmptyWorldGenerator(SimpleUri uri) { + super(uri); + } + + @Override + protected WorldBuilder createWorld() { + return new WorldBuilder(new WorldGeneratorPluginLibrary() { + @Override + public List instantiateAllOfType(Class ofType) { + return new ArrayList<>(); + } + }); + } +} diff --git a/engine-tests/src/main/java/org/terasology/unittest/worlds/FlatSurfaceHeightProvider.java b/engine-tests/src/main/java/org/terasology/unittest/worlds/FlatSurfaceHeightProvider.java new file mode 100644 index 00000000000..244b8c824d9 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/unittest/worlds/FlatSurfaceHeightProvider.java @@ -0,0 +1,45 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.unittest.worlds; + +import org.joml.Vector2ic; +import org.terasology.engine.world.generation.FacetProvider; +import org.terasology.engine.world.generation.GeneratingRegion; +import org.terasology.engine.world.generation.Produces; +import org.terasology.engine.world.generation.facets.ElevationFacet; +import org.terasology.engine.world.generation.facets.SurfacesFacet; + +@Produces({SurfacesFacet.class, ElevationFacet.class}) +public class FlatSurfaceHeightProvider implements FacetProvider { + private int height; + + public FlatSurfaceHeightProvider(int height) { + this.height = height; + } + + @Override + public void setSeed(long seed) { + } + + @Override + public void process(GeneratingRegion region) { + ElevationFacet elevationFacet = new ElevationFacet(region.getRegion(), region.getBorderForFacet(ElevationFacet.class)); + SurfacesFacet surfacesFacet = new SurfacesFacet(region.getRegion(), region.getBorderForFacet(SurfacesFacet.class)); + + for (Vector2ic pos : elevationFacet.getRelativeArea()) { + elevationFacet.set(pos, height); + } + + if (surfacesFacet.getWorldRegion().minY() <= height && height <= surfacesFacet.getWorldRegion().maxY()) { + for (int x = surfacesFacet.getWorldRegion().minX(); x <= surfacesFacet.getWorldRegion().maxX(); x++) { + for (int z = surfacesFacet.getWorldRegion().minZ(); z <= surfacesFacet.getWorldRegion().maxZ(); z++) { + surfacesFacet.setWorld(x, height, z, true); + } + } + } + + region.setRegionFacet(ElevationFacet.class, elevationFacet); + region.setRegionFacet(SurfacesFacet.class, surfacesFacet); + } +} + diff --git a/engine-tests/src/main/java/org/terasology/unittest/stubs/StubWorldGenerator.java b/engine-tests/src/main/java/org/terasology/unittest/worlds/StubWorldGenerator.java similarity index 94% rename from engine-tests/src/main/java/org/terasology/unittest/stubs/StubWorldGenerator.java rename to engine-tests/src/main/java/org/terasology/unittest/worlds/StubWorldGenerator.java index 1514fb0808a..53c459784ca 100644 --- a/engine-tests/src/main/java/org/terasology/unittest/stubs/StubWorldGenerator.java +++ b/engine-tests/src/main/java/org/terasology/unittest/worlds/StubWorldGenerator.java @@ -1,7 +1,7 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -package org.terasology.unittest.stubs; +package org.terasology.unittest.worlds; import org.terasology.engine.core.SimpleUri; import org.terasology.engine.world.chunks.Chunk; diff --git a/engine-tests/src/main/java/org/terasology/unittest/worlds/package-info.java b/engine-tests/src/main/java/org/terasology/unittest/worlds/package-info.java new file mode 100644 index 00000000000..e873f40faf7 --- /dev/null +++ b/engine-tests/src/main/java/org/terasology/unittest/worlds/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +@API +package org.terasology.unittest.worlds; + +import org.terasology.gestalt.module.sandbox.API; diff --git a/engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/default-logback.xml b/engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/default-logback.xml new file mode 100644 index 00000000000..c4b542d0dd4 --- /dev/null +++ b/engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/default-logback.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + reflectionsEmpty + + given scan urls are empty + + + reflectionsEmpty.matches(message) + + NEUTRAL + DENY + + + + + + + + + + + + diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java new file mode 100644 index 00000000000..333f74b4e19 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java @@ -0,0 +1,41 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.entitySystem.prefab.Prefab; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.registry.In; +import org.terasology.engine.world.block.Block; +import org.terasology.engine.world.block.BlockManager; +import org.terasology.gestalt.assets.management.AssetManager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.terasology.engine.testUtil.Assertions.assertNotEmpty; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class AssetLoadingTest { + + @In + private BlockManager blockManager; + @In + private AssetManager assetManager; + + @Test + public void blockPrefabLoadingTest() { + Block block = blockManager.getBlock("engine:air"); + assertNotNull(block); + assertEquals(0, block.getHardness()); + assertEquals("Air", block.getDisplayName()); + } + + @Test + public void simpleLoadingTest() { + assertNotEmpty(assetManager.getAsset("engine:test", Prefab.class)); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java new file mode 100644 index 00000000000..cc5d15c8ee6 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java @@ -0,0 +1,60 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.registry.In; +import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.block.Block; +import org.terasology.engine.world.chunks.Chunks; +import org.terasology.engine.world.chunks.localChunkProvider.RelevanceSystem; +import org.terasology.unittest.worlds.DummyWorldGenerator; + +import static com.google.common.truth.Truth.assertThat; +import static org.terasology.engine.world.block.BlockManager.AIR_ID; +import static org.terasology.engine.world.block.BlockManager.UNLOADED_ID; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +class ChunkRegionFutureTest { + + Vector3fc center = new Vector3f(2021, DummyWorldGenerator.SURFACE_HEIGHT, 1117); + Vector3ic sizeInChunks = new Vector3i(9, 3, 5); + + @In + WorldProvider world; + + @Test + void createChunkRegionFuture(EntityManager entityManager, RelevanceSystem relevanceSystem, MainLoop mainLoop) { + ChunkRegionFuture chunkRegionFuture = ChunkRegionFuture.create(entityManager, relevanceSystem, center, sizeInChunks); + + mainLoop.runUntil(chunkRegionFuture.getFuture()); + + Vector3fc someplaceInside = center.add( + sizeInChunks.x() * Chunks.SIZE_X / 3f, 0, 0, + new Vector3f()); + Vector3fc someplaceOutside = center.add( + 0, 0, sizeInChunks.z() * Chunks.SIZE_Z * 3f, + new Vector3f()); + + Block blockAtCenter = world.getBlock(center); + Block blockInside = world.getBlock(someplaceInside); + Block blockOutside = world.getBlock(someplaceOutside); + + assertThat(blockAtCenter).isNotNull(); + assertThat(blockAtCenter.getURI()).isEqualTo(AIR_ID); + + assertThat(blockInside).isNotNull(); + assertThat(blockInside.getURI()).isEqualTo(AIR_ID); + + assertThat(blockOutside.getURI()).isEqualTo(UNLOADED_ID); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java new file mode 100644 index 00000000000..f14adad8e69 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java @@ -0,0 +1,32 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.context.Context; +import org.terasology.engine.core.TerasologyEngine; +import org.terasology.engine.core.modes.StateIngame; +import org.terasology.engine.integrationenvironment.extension.Dependencies; + +import java.io.IOException; +import java.util.List; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class ClientConnectionTest { + + @Test + public void testClientConnection(ModuleTestingHelper helper) throws IOException { + Context clientContext = helper.createClient(); + List engines = helper.getEngines(); + Assertions.assertEquals(2, engines.size()); + Assertions.assertAll(engines + .stream() + .map((engine) -> + () -> Assertions.assertEquals(StateIngame.class, engine.getState().getClass()))); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java new file mode 100644 index 00000000000..080172e80d7 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java @@ -0,0 +1,29 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.registry.In; +import org.terasology.unittest.stubs.DummyComponent; +import org.terasology.unittest.stubs.DummyEvent; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class ComponentSystemTest { + @In + private EntityManager entityManager; + + @Test + public void simpleEventTest() { + EntityRef entity = entityManager.create(new DummyComponent()); + entity.send(new DummyEvent()); + Assertions.assertTrue(entity.getComponent(DummyComponent.class).eventReceived); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java new file mode 100644 index 00000000000..4769b781bde --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java @@ -0,0 +1,83 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import com.google.common.collect.Lists; +import org.joml.Vector3i; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.context.Context; +import org.terasology.engine.core.Time; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.logic.players.LocalPlayer; +import org.terasology.engine.logic.players.event.ResetCameraEvent; +import org.terasology.engine.network.ClientComponent; +import org.terasology.engine.registry.In; +import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.block.BlockManager; + +import java.io.IOException; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies("ModuleTestingEnvironment") +public class ExampleTest { + + @In + private WorldProvider worldProvider; + @In + private BlockManager blockManager; + @In + private EntityManager entityManager; + @In + private Time time; + @In + private ModuleTestingHelper helper; + + @Test + public void testClientConnection() throws IOException { + int currentClients = Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).size(); + + // create some clients (the library connects them automatically) + Context clientContext1 = helper.createClient(); + Context clientContext2 = helper.createClient(); + + int expectedClients = currentClients + 2; + + // wait for both clients to be known to the server + helper.runUntil(() -> Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).size() >= expectedClients); + Assertions.assertEquals(expectedClients, + Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).size()); + } + + @Test + public void testRunWhileTimeout() { + // run while a condition is true or until a timeout passes + long expectedTime = time.getGameTimeInMs() + 500; + boolean timedOut = helper.runWhile(500, () -> true); + Assertions.assertTrue(timedOut); + long currentTime = time.getGameTimeInMs(); + Assertions.assertTrue(currentTime >= expectedTime); + } + + @Test + public void testSendEvent() throws IOException { + Context clientContext = helper.createClient(); + + // send an event to a client's local player just for fun + clientContext.get(LocalPlayer.class).getClientEntity().send(new ResetCameraEvent()); + } + + @Test + public void testWorldProvider() { + // wait for a chunk to be generated + helper.forceAndWaitForGeneration(new Vector3i()); + + // set a block's type and immediately read it back + worldProvider.setBlock(new org.joml.Vector3i(), blockManager.getBlock("engine:air")); + Assertions.assertEquals("engine:air", worldProvider.getBlock(new org.joml.Vector3i()).getURI().toString()); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java new file mode 100644 index 00000000000..729ffaa189c --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java @@ -0,0 +1,62 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.collect.Sets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.registry.In; +import org.terasology.unittest.stubs.DummyComponent; +import org.terasology.unittest.stubs.DummyEvent; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("MteTest") +@ExtendWith(IsolatedMTEExtension.class) +@Dependencies("ModuleTestingEnvironment") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class IsolatedEngineTest { + private final Set entityManagerSet = Sets.newHashSet(); + private EntityRef entity; + + @In + private EntityManager entityManager; + + @BeforeEach + public void prepareEntityForTest() { + entity = entityManager.create(new DummyComponent()); + } + + @Test + @Order(1) + public void someTest() { + // make sure we don't reuse the EntityManager + assertFalse(entityManagerSet.contains(entityManager)); + entityManagerSet.add(entityManager); + + entity.send(new DummyEvent()); + assertTrue(entity.getComponent(DummyComponent.class).eventReceived); + } + + @Test + @Order(2) + public void someOtherTest() { + // make sure we don't reuse the EntityManager + assertFalse(entityManagerSet.contains(entityManager)); + + assertFalse(entity.getComponent(DummyComponent.class).eventReceived, + "This entity should not have its field set yet!"); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java new file mode 100644 index 00000000000..9f08526fb5b --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java @@ -0,0 +1,94 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.collect.Lists; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.registry.In; +import org.terasology.unittest.stubs.DummyComponent; +import org.terasology.unittest.stubs.DummyEvent; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Ensure a test class with a per-class Jupiter lifecycle can share an engine between tests. + */ +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) // Lifecycle of the test instance +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MTEExtensionTestWithPerClassLifecycle { + @In + public EntityManager entityManager; + + // java 8 doesn't have ConcurrentSet + private final ConcurrentMap seenNames = new ConcurrentHashMap<>(); + + @BeforeAll + public void createEntity(TestInfo testInfo) { + // Create some entity to be shared by all the tests. + EntityRef entity = entityManager.create(new DummyComponent()); + + // Do some stuff to configure it. + entity.send(new DummyEvent()); + + entity.updateComponent(DummyComponent.class, component -> { + // Mark with something unique (and not reliant on the entity id system) + component.name = testInfo.getDisplayName() + "#" + UUID.randomUUID(); + return component; + }); + } + + @Test + @Order(1) + public void firstTestFindsThings() { + List entities = Lists.newArrayList(entityManager.getEntitiesWith(DummyComponent.class)); + // There should be one entity, created by the @BeforeAll method + assertEquals(1, entities.size()); + + DummyComponent component = entities.get(0).getComponent(DummyComponent.class); + assertTrue(component.eventReceived); + + // Remember that a test has seen this one. + assertNotNull(component.name); + assertFalse(seenNames.containsKey(component.name)); + seenNames.put(component.name, 1); + } + + @Test + @Order(2) + public void thingsStillExistForSecondTest() { + List entities = Lists.newArrayList(entityManager.getEntitiesWith(DummyComponent.class)); + // There should be one entity, created by the @BeforeAll method + assertEquals(1, entities.size()); + + // Make sure that this is the same one that the first test saw. + DummyComponent component = entities.get(0).getComponent(DummyComponent.class); + assertTrue(component.eventReceived); + assertNotNull(component.name); + assertTrue(seenNames.containsKey(component.name), () -> + String.format("This is not the same entity as seen in the first test!%n" + + "Current entity: %s%n" + + "Previously seen: %s", + component.name, seenNames.keySet())); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java new file mode 100644 index 00000000000..27666c45510 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java @@ -0,0 +1,95 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.collect.Lists; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.registry.In; +import org.terasology.unittest.stubs.DummyComponent; +import org.terasology.unittest.stubs.DummyEvent; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Ensure a test class with a per-method Jupiter lifecycle can share an engine between tests. + */ +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@TestInstance(TestInstance.Lifecycle.PER_METHOD) // The default, but here for explicitness. +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MTEExtensionTestWithPerMethodLifecycle { + // java 8 doesn't have ConcurrentSet + @SuppressWarnings("checkstyle:constantname") + private static final ConcurrentMap seenNames = new ConcurrentHashMap<>(); + + @In + public EntityManager entityManager; + + @BeforeAll + public static void createEntity(EntityManager entityManager, TestInfo testInfo) { + // Create some entity to be shared by all the tests. + EntityRef entity = entityManager.create(new DummyComponent()); + + // Do some stuff to configure it. + entity.send(new DummyEvent()); + + entity.updateComponent(DummyComponent.class, component -> { + // Mark with something unique (and not reliant on the entity id system) + component.name = testInfo.getDisplayName() + "#" + UUID.randomUUID(); + return component; + }); + } + + @Test + @Order(1) + public void firstTestFindsThings() { + List entities = Lists.newArrayList(entityManager.getEntitiesWith(DummyComponent.class)); + // There should be one entity, created by the @BeforeAll method + assertEquals(1, entities.size()); + + DummyComponent component = entities.get(0).getComponent(DummyComponent.class); + assertTrue(component.eventReceived); + + // Remember that a test has seen this one. + assertNotNull(component.name); + assertFalse(seenNames.containsKey(component.name)); + seenNames.put(component.name, 1); + } + + @Test + @Order(2) + public void thingsStillExistForSecondTest() { + List entities = Lists.newArrayList(entityManager.getEntitiesWith(DummyComponent.class)); + // There should be one entity, created by the @BeforeAll method + assertEquals(1, entities.size()); + + // Make sure that this is the same one that the first test saw. + DummyComponent component = entities.get(0).getComponent(DummyComponent.class); + assertTrue(component.eventReceived); + assertNotNull(component.name); + assertTrue(seenNames.containsKey(component.name), () -> + String.format("This is not the same entity as seen in the first test!%n" + + "Current entity: %s%n" + + "Previously seen: %s", + component.name, seenNames.keySet())); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java new file mode 100644 index 00000000000..2d9c4103587 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java @@ -0,0 +1,39 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.common.util.concurrent.UncheckedTimeoutException; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +public class ModuleTestingEnvironmentTest { + + public static final int THE_ANSWER = 42; + + @Test + public void runUntilWithUnsatisfiedFutureExplainsTimeout(MainLoop mainLoop) { + SettableFuture unsatisfiedFuture = SettableFuture.create(); + + UncheckedTimeoutException exception = assertThrows(UncheckedTimeoutException.class, + // TODO: change the timeout for this test so it doesn't always take + // a minimum of 30 seconds. + () -> mainLoop.runUntil(unsatisfiedFuture)); + assertThat(exception).hasMessageThat().contains("default timeout"); + } + + @Test + public void runUntilWithImmediateFutureReturnsValue(MainLoop mainLoop) { + ListenableFuture valueFuture = Futures.immediateFuture(THE_ANSWER); + assertThat(mainLoop.runUntil(valueFuture)).isEqualTo(THE_ANSWER); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java new file mode 100644 index 00000000000..7bb82d90a3e --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java @@ -0,0 +1,45 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.registry.In; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class NestedTest { + @In + public static Engines outerEngines; + + @In + public static EntityManager outerManager; + + @Test + public void outerTest() { + Assertions.assertNotNull(outerEngines); + Assertions.assertNotNull(outerManager); + } + + @Nested + class NestedTestClass { + @In + Engines innerEngines; + + @In + EntityManager innerManager; + + @Test + public void innerTest() { + Assertions.assertSame(innerManager, outerManager); + Assertions.assertSame(innerEngines, outerEngines); + } + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java new file mode 100644 index 00000000000..e66b273585a --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java @@ -0,0 +1,23 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.integrationenvironment.fixtures.BaseTestingClass; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class SubclassInjectionTest extends BaseTestingClass { + @Test + public void testInjection() { + // ensure the superclass's private fields were injected correctly + Assertions.assertNotNull(getEntityManager()); + Assertions.assertNotNull(getHelper()); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java new file mode 100644 index 00000000000..699562a38fe --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java @@ -0,0 +1,119 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.context.Context; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.logic.location.LocationComponent; +import org.terasology.engine.registry.In; +import org.terasology.unittest.stubs.DummyComponent; +import org.terasology.unittest.stubs.DummyEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class TestEventReceiverTest { + + @In + private ModuleTestingHelper helper; + + @In + private EntityManager entityManager; + + @Test + public void componentFilterTest() { + EntityRef entityWithDummy = entityManager.create(new DummyComponent()); + EntityRef entityWithDummyAndLocation = entityManager.create(new DummyComponent(), new LocationComponent()); + + // AtomicInteger is used because Java complained when just using a primitive int + AtomicInteger callbackInvocations = new AtomicInteger(); + BiConsumer callback = (event, entity) -> callbackInvocations.addAndGet(1); + + try (TestEventReceiver receiver = + new TestEventReceiver<>(getHostContext(), DummyEvent.class, callback, DummyComponent.class, LocationComponent.class)) { + entityWithDummy.send(new DummyEvent()); + entityWithDummyAndLocation.send(new DummyEvent()); + List actualEntities = receiver.getEntityRefs(); + + // Only the entity with both Dummy and Location should get hit + Assertions.assertEquals(1, callbackInvocations.get()); + Assertions.assertTrue(actualEntities.contains(entityWithDummyAndLocation)); + Assertions.assertFalse(actualEntities.contains(entityWithDummy)); + } + + entityWithDummy.destroy(); + entityWithDummyAndLocation.destroy(); + } + + @Test + public void repeatedEventTest() { + final List expectedEntities = new ArrayList<>(); + try (TestEventReceiver receiver = new TestEventReceiver<>(getHostContext(), DummyEvent.class)) { + List actualEntities = receiver.getEntityRefs(); + Assertions.assertTrue(actualEntities.isEmpty()); + for (int i = 0; i < 5; i++) { + expectedEntities.add(sendEvent()); + Assertions.assertEquals(i + 1, actualEntities.size()); + Assertions.assertEquals(expectedEntities.get(i), actualEntities.get(i)); + } + } + } + + @Test + public void properClosureTest() { + final List entities; + try (TestEventReceiver receiver = new TestEventReceiver<>(getHostContext(), DummyEvent.class)) { + entities = receiver.getEntityRefs(); + } + sendEvent(); + Assertions.assertTrue(entities.isEmpty()); + } + + @Test + public void userCallbackTest() { + final List events = new ArrayList<>(); + + TestEventReceiver receiver = new TestEventReceiver<>(getHostContext(), DummyEvent.class, (event, entity) -> { + events.add(event); + }); + + for (int i = 0; i < 3; i++) { + sendEvent(); + } + + // ensure all interesting events were caught + Assertions.assertEquals(3, events.size()); + + // shouldn't receive events after closing + receiver.close(); + sendEvent(); + Assertions.assertEquals(3, events.size()); + } + + /** + * Drops a generic item into the world. + * + * @return the item + */ + private EntityRef sendEvent() { + final EntityManager entityManager = getHostContext().get(EntityManager.class); + final EntityRef entityRef = entityManager.create(new DummyComponent()); + entityRef.send(new DummyEvent()); + return entityRef; + } + + private Context getHostContext() { + return helper.getHostContext(); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java new file mode 100644 index 00000000000..9d5d4cc2f5e --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java @@ -0,0 +1,39 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.integrationenvironment; + +import org.joml.Vector3i; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.registry.In; +import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.block.BlockManager; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class WorldProviderTest { + + @In + WorldProvider worldProvider; + @In + BlockManager blockManager; + @In + MainLoop mainLoop; + + @Test + public void defaultWorldSetBlockTest() { + mainLoop.forceAndWaitForGeneration(new Vector3i()); + + // this will change if the worldgenerator changes or the seed is altered, the main point is that this is a real + // block type and not engine:unloaded + Assertions.assertEquals("engine:air", worldProvider.getBlock(0, 0, 0).getURI().toString()); + + // also verify that we can set and immediately get blocks from the worldprovider + worldProvider.setBlock(new Vector3i(), blockManager.getBlock("engine:unloaded")); + Assertions.assertEquals("engine:unloaded", worldProvider.getBlock(0, 0, 0).getURI().toString()); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java new file mode 100644 index 00000000000..027d386a1e2 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java @@ -0,0 +1,59 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.integrationenvironment.delay; + +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.core.Time; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.MTEExtension; +import org.terasology.engine.integrationenvironment.ModuleTestingHelper; +import org.terasology.engine.integrationenvironment.TestEventReceiver; +import org.terasology.engine.integrationenvironment.extension.Dependencies; +import org.terasology.engine.logic.delay.DelayManager; +import org.terasology.engine.logic.delay.DelayedActionTriggeredEvent; +import org.terasology.engine.network.ClientComponent; +import org.terasology.engine.registry.In; + +import java.io.IOException; + +@Tag("MteTest") +@ExtendWith(MTEExtension.class) +@Dependencies({"engine", "ModuleTestingEnvironment"}) +public class DelayManagerTest { + private static final Logger logger = LoggerFactory.getLogger(DelayManagerTest.class); + + @In + DelayManager delayManager; + + @In + EntityManager entityManager; + + @In + Time time; + + @Test + public void delayedActionIsTriggeredTest(ModuleTestingHelper helper) throws IOException { + helper.createClient(); + helper.runWhile(() -> Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).isEmpty()); + + final TestEventReceiver eventReceiver = + new TestEventReceiver<>(helper.getHostContext(), DelayedActionTriggeredEvent.class); + + EntityRef player = Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).get(0); + delayManager.addDelayedAction(player, "ModuleTestingEnvironment:delayManagerTest", 1000); + + Assertions.assertTrue(eventReceiver.getEvents().isEmpty()); + + long stop = time.getGameTimeInMs() + 1200; + helper.runWhile(() -> time.getGameTimeInMs() < stop); + Assertions.assertFalse(eventReceiver.getEvents().isEmpty()); + } +} From 2b5222690b847e303dcf3555ad0ee2d605262fce Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Mon, 9 May 2022 17:56:41 -0700 Subject: [PATCH 02/25] test(integrationenvironment): remove references to ModuleTestingEnvironment module --- .../integrationenvironment/TestingStateHeadlessSetup.java | 2 +- .../engine/integrationenvironment/AssetLoadingTest.java | 2 -- .../engine/integrationenvironment/ClientConnectionTest.java | 2 -- .../engine/integrationenvironment/ComponentSystemTest.java | 2 -- .../terasology/engine/integrationenvironment/ExampleTest.java | 2 -- .../engine/integrationenvironment/IsolatedEngineTest.java | 2 -- .../terasology/engine/integrationenvironment/NestedTest.java | 2 -- .../engine/integrationenvironment/SubclassInjectionTest.java | 2 -- .../engine/integrationenvironment/TestEventReceiverTest.java | 2 -- .../engine/integrationenvironment/WorldProviderTest.java | 2 -- .../engine/integrationenvironment/delay/DelayManagerTest.java | 2 -- 11 files changed, 1 insertion(+), 21 deletions(-) diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java index ca7b70d48ea..f8ec83616de 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java @@ -25,7 +25,7 @@ public class TestingStateHeadlessSetup extends StateHeadlessSetup { private static final Logger logger = LoggerFactory.getLogger(TestingStateHeadlessSetup.class); - static final Name MTE_MODULE_NAME = new Name("ModuleTestingEnvironment"); + static final Name MTE_MODULE_NAME = new Name("unittest"); static final String WORLD_TITLE = "testworld"; static final String DEFAULT_SEED = "seed"; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java index 333f74b4e19..aa0a5896535 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.prefab.Prefab; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.registry.In; import org.terasology.engine.world.block.Block; import org.terasology.engine.world.block.BlockManager; @@ -18,7 +17,6 @@ @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class AssetLoadingTest { @In diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java index f14adad8e69..d64fe1583e9 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java @@ -9,14 +9,12 @@ import org.terasology.engine.context.Context; import org.terasology.engine.core.TerasologyEngine; import org.terasology.engine.core.modes.StateIngame; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import java.io.IOException; import java.util.List; @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class ClientConnectionTest { @Test diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java index 080172e80d7..e33a28f4cd8 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java @@ -8,14 +8,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; import org.terasology.unittest.stubs.DummyEvent; @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class ComponentSystemTest { @In private EntityManager entityManager; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java index 4769b781bde..a682b8f36db 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java @@ -11,7 +11,6 @@ import org.terasology.engine.context.Context; import org.terasology.engine.core.Time; import org.terasology.engine.entitySystem.entity.EntityManager; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.logic.players.LocalPlayer; import org.terasology.engine.logic.players.event.ResetCameraEvent; import org.terasology.engine.network.ClientComponent; @@ -23,7 +22,6 @@ @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies("ModuleTestingEnvironment") public class ExampleTest { @In diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java index 729ffaa189c..903c7d44301 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java @@ -13,7 +13,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; import org.terasology.unittest.stubs.DummyEvent; @@ -25,7 +24,6 @@ @Tag("MteTest") @ExtendWith(IsolatedMTEExtension.class) -@Dependencies("ModuleTestingEnvironment") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class IsolatedEngineTest { private final Set entityManagerSet = Sets.newHashSet(); diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java index 7bb82d90a3e..4fbc103ca8a 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java @@ -9,12 +9,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.registry.In; @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class NestedTest { @In public static Engines outerEngines; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java index e66b273585a..e3de96631d2 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java @@ -7,12 +7,10 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.integrationenvironment.fixtures.BaseTestingClass; @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class SubclassInjectionTest extends BaseTestingClass { @Test public void testInjection() { diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java index 699562a38fe..2d55e05ea63 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java @@ -9,7 +9,6 @@ import org.terasology.engine.context.Context; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.logic.location.LocationComponent; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; @@ -22,7 +21,6 @@ @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class TestEventReceiverTest { @In diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java index 9d5d4cc2f5e..b167cff4217 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java @@ -7,14 +7,12 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.registry.In; import org.terasology.engine.world.WorldProvider; import org.terasology.engine.world.block.BlockManager; @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class WorldProviderTest { @In diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java index 027d386a1e2..2692709c9dd 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java @@ -16,7 +16,6 @@ import org.terasology.engine.integrationenvironment.MTEExtension; import org.terasology.engine.integrationenvironment.ModuleTestingHelper; import org.terasology.engine.integrationenvironment.TestEventReceiver; -import org.terasology.engine.integrationenvironment.extension.Dependencies; import org.terasology.engine.logic.delay.DelayManager; import org.terasology.engine.logic.delay.DelayedActionTriggeredEvent; import org.terasology.engine.network.ClientComponent; @@ -26,7 +25,6 @@ @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) public class DelayManagerTest { private static final Logger logger = LoggerFactory.getLogger(DelayManagerTest.class); From 693ecb3aebeec6055b653902e453a2d9bd5c7d8a Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 10:49:06 -0700 Subject: [PATCH 03/25] test: remove unused BehaviorEmptyLookup The deserializer doesn't load it ("" is an invalid URN) and I couldn't find it referenced as part of any check-loader-errors test. --- .../unittest/assets/behaviors/BehaviorEmptyLookup.behavior | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 engine-tests/src/main/resources/org/terasology/unittest/assets/behaviors/BehaviorEmptyLookup.behavior diff --git a/engine-tests/src/main/resources/org/terasology/unittest/assets/behaviors/BehaviorEmptyLookup.behavior b/engine-tests/src/main/resources/org/terasology/unittest/assets/behaviors/BehaviorEmptyLookup.behavior deleted file mode 100644 index 4fabd72883e..00000000000 --- a/engine-tests/src/main/resources/org/terasology/unittest/assets/behaviors/BehaviorEmptyLookup.behavior +++ /dev/null @@ -1,5 +0,0 @@ -{ - lookup : { - tree: "" - } -} \ No newline at end of file From 920f1d1a8f19022329bd73bd1637dc97ace96e98 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 10:50:03 -0700 Subject: [PATCH 04/25] test(StubWorldGenerator): remove unused constructor --- .../org/terasology/unittest/worlds/StubWorldGenerator.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/engine-tests/src/main/java/org/terasology/unittest/worlds/StubWorldGenerator.java b/engine-tests/src/main/java/org/terasology/unittest/worlds/StubWorldGenerator.java index 53c459784ca..b384e9523a5 100644 --- a/engine-tests/src/main/java/org/terasology/unittest/worlds/StubWorldGenerator.java +++ b/engine-tests/src/main/java/org/terasology/unittest/worlds/StubWorldGenerator.java @@ -17,10 +17,6 @@ public class StubWorldGenerator implements WorldGenerator { private final SimpleUri uri; - public StubWorldGenerator() { - this(new SimpleUri("unittest", "stub")); - } - public StubWorldGenerator(SimpleUri uri) { this.uri = checkNotNull(uri); } From b6e85b6e44e58e78ad3cc975fd8bbddcdee3f50e Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 11:28:00 -0700 Subject: [PATCH 05/25] fix(LocalPlayer): do not crash when the camera is not on Restores the alternate behavior described in the docstring, correcting a regression introduced in #4795. This "LocalPlayer without camera" situation has come up in headless MTE tests. --- .../engine/logic/players/LocalPlayer.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/logic/players/LocalPlayer.java b/engine/src/main/java/org/terasology/engine/logic/players/LocalPlayer.java index 5acc5be5732..edf810d6fe3 100644 --- a/engine/src/main/java/org/terasology/engine/logic/players/LocalPlayer.java +++ b/engine/src/main/java/org/terasology/engine/logic/players/LocalPlayer.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.logic.players; @@ -115,27 +115,35 @@ public Quaternionf getRotation(Quaternionf dest) { } /** - * position of camera if one is present else use {@link #getPosition(Vector3f)} + * position of camera if one is present else use {@link #getPosition} * * @param dest will hold the result * @return dest */ public Vector3f getViewPosition(Vector3f dest) { ClientComponent clientComponent = getClientEntity().getComponent(ClientComponent.class); - LocationComponent location = clientComponent.camera.getComponent(LocationComponent.class); - return location.getWorldPosition(dest); + if (clientComponent.camera.exists()) { + LocationComponent location = clientComponent.camera.getComponent(LocationComponent.class); + return location.getWorldPosition(dest); + } else { + return getPosition(dest); + } } /** - * orientation of camera if one is present else use {@link #getPosition(Vector3f)} + * orientation of camera if one is present else use {@link #getRotation} * * @param dest will hold the result * @return dest */ public Quaternionf getViewRotation(Quaternionf dest) { ClientComponent clientComponent = getClientEntity().getComponent(ClientComponent.class); - LocationComponent location = clientComponent.camera.getComponent(LocationComponent.class); - return location.getWorldRotation(dest); + if (clientComponent.camera.exists()) { + LocationComponent location = clientComponent.camera.getComponent(LocationComponent.class); + return location.getWorldRotation(dest); + } else { + return getRotation(dest); + } } /** From 1176bb13ecde2016090bd7d99fafdd2e19466a7b Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 11:49:23 -0700 Subject: [PATCH 06/25] fix(engine): no longer add all subsystem classpaths to the engine module by default --- .../engine/core/TerasologyEngine.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/core/TerasologyEngine.java b/engine/src/main/java/org/terasology/engine/core/TerasologyEngine.java index 912b3830dc3..7e989e34910 100644 --- a/engine/src/main/java/org/terasology/engine/core/TerasologyEngine.java +++ b/engine/src/main/java/org/terasology/engine/core/TerasologyEngine.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.core; @@ -105,6 +105,15 @@ public class TerasologyEngine implements GameEngine { private static final int ONE_MEBIBYTE = 1024 * 1024; + /** + * Subsystem classes that automatically make their classpath part of the engine module. + *

+ * You don't want to add to this! If you need a module, make a module! + */ + private static final Set LEGACY_ENGINE_MODULE_POLLUTERS = Set.of( + "org.terasology.subsystem.discordrpc.DiscordRPCSubSystem" + ); + private final List> classesOnClasspathsToAddToEngine = new ArrayList<>(); private GameState currentState; @@ -173,8 +182,12 @@ public TerasologyEngine(TimeSubsystem timeSubsystem, Collection this.allSubsystems.add(new I18nSubsystem()); this.allSubsystems.add(new TelemetrySubSystem()); - // add all subsystem as engine module part. (needs for ECS classes loaded from external subsystems) - allSubsystems.stream().map(Object::getClass).forEach(this::addToClassesOnClasspathsToAddToEngine); + for (EngineSubsystem subsystem : allSubsystems) { + if (LEGACY_ENGINE_MODULE_POLLUTERS.contains(subsystem.getClass().getName())) { + // add subsystem as engine module part. (needed for ECS classes loaded from external subsystems) + addToClassesOnClasspathsToAddToEngine(subsystem.getClass()); + } + } // the TypeHandlerLibrary is technically not a subsystem (although it lives in the subsystem space) // therefore, we have to manually register the type handler classes with the engine module From 106b68d0f7564b3515a1a7301bfde8c4921d02af Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 12:25:34 -0700 Subject: [PATCH 07/25] test: checkstyle cleanup --- .../integrationenvironment/TestingStateHeadlessSetup.java | 3 --- .../engine/integrationenvironment/TestEventReceiverTest.java | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java index f8ec83616de..e6eb3295e09 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.integrationenvironment; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.terasology.engine.config.Config; import org.terasology.engine.config.ModuleConfig; import org.terasology.engine.config.WorldGenerationConfig; @@ -23,7 +21,6 @@ import static com.google.common.base.Preconditions.checkArgument; public class TestingStateHeadlessSetup extends StateHeadlessSetup { - private static final Logger logger = LoggerFactory.getLogger(TestingStateHeadlessSetup.class); static final Name MTE_MODULE_NAME = new Name("unittest"); static final String WORLD_TITLE = "testworld"; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java index 2d55e05ea63..a79705031f2 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java @@ -105,8 +105,8 @@ public void userCallbackTest() { * @return the item */ private EntityRef sendEvent() { - final EntityManager entityManager = getHostContext().get(EntityManager.class); - final EntityRef entityRef = entityManager.create(new DummyComponent()); + final EntityRef entityRef = getHostContext().get(EntityManager.class) + .create(new DummyComponent()); entityRef.send(new DummyEvent()); return entityRef; } From 1f4bcb7204c231f76f38db2bbd53219653bc04da Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 13:21:03 -0700 Subject: [PATCH 08/25] fix(ChatSystem): null check --- .../java/org/terasology/engine/logic/chat/ChatSystem.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/engine/src/main/java/org/terasology/engine/logic/chat/ChatSystem.java b/engine/src/main/java/org/terasology/engine/logic/chat/ChatSystem.java index 8d82d4be69f..339c1381278 100644 --- a/engine/src/main/java/org/terasology/engine/logic/chat/ChatSystem.java +++ b/engine/src/main/java/org/terasology/engine/logic/chat/ChatSystem.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.logic.chat; @@ -66,6 +66,9 @@ public void onToggleChat(ChatButton event, EntityRef entity) { @ReceiveEvent(components = ClientComponent.class) public void onMessage(MessageEvent event, EntityRef entity) { + if (overlay == null) { + return; + } ClientComponent client = entity.getComponent(ClientComponent.class); if (client.local) { Message message = event.getFormattedMessage(); From d57480c10fce4efef1ea06716c54253db73ff70e Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 13:21:43 -0700 Subject: [PATCH 09/25] chore(network): null check --- .../engine/network/internal/ServerHandshakeHandler.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/network/internal/ServerHandshakeHandler.java b/engine/src/main/java/org/terasology/engine/network/internal/ServerHandshakeHandler.java index 0282db3e213..1e80f0ac655 100644 --- a/engine/src/main/java/org/terasology/engine/network/internal/ServerHandshakeHandler.java +++ b/engine/src/main/java/org/terasology/engine/network/internal/ServerHandshakeHandler.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.network.internal; @@ -25,6 +25,8 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import static com.google.common.base.Verify.verifyNotNull; + /** * Authentication handler for the server end of the handshake */ @@ -41,8 +43,8 @@ public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); serverConnectionHandler = ctx.pipeline().get(ServerConnectionHandler.class); - - PublicIdentityCertificate serverPublicCert = config.getSecurity().getServerPublicCertificate(); + PublicIdentityCertificate serverPublicCert = verifyNotNull(config.getSecurity(), "config.security") + .getServerPublicCertificate(); new SecureRandom().nextBytes(serverRandom); serverHello = NetData.HandshakeHello.newBuilder() From b0ed2eb45ad39c4c3a3726ce4fab0b901d2964a1 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 13:24:48 -0700 Subject: [PATCH 10/25] fix(WorldGeneratorManager): null check It does have error catching, but detecting this gives a much clearer log than a NullPointerException and stack trace. --- .../generator/internal/WorldGeneratorManager.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java b/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java index da7dea64054..ad365ef587f 100644 --- a/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java +++ b/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.world.generator.internal; @@ -28,7 +28,7 @@ public class WorldGeneratorManager { private static final Logger logger = LoggerFactory.getLogger(WorldGeneratorManager.class); - private Context context; + private final Context context; private ImmutableList generatorInfo; @@ -48,11 +48,17 @@ public void refresh() { if (resolutionResult.isSuccess()) { try (ModuleEnvironment tempEnvironment = moduleManager.loadEnvironment(resolutionResult.getModules(), false)) { for (Class generatorClass : tempEnvironment.getTypesAnnotatedWith(RegisterWorldGenerator.class)) { - if (tempEnvironment.getModuleProviding(generatorClass).equals(module.getId())) { + Name providedBy = tempEnvironment.getModuleProviding(generatorClass); + if (providedBy == null) { + // These tend to be engine-module-is-weird cases. + logger.warn("{} found while inspecting {} but is not provided by any module.", + generatorClass, moduleId); + } else if (providedBy.equals(module.getId())) { RegisterWorldGenerator annotation = generatorClass.getAnnotation(RegisterWorldGenerator.class); if (isValidWorldGenerator(generatorClass)) { SimpleUri uri = new SimpleUri(moduleId, annotation.id()); infos.add(new WorldGeneratorInfo(uri, annotation.displayName(), annotation.description())); + logger.debug("{} added from {}", uri, generatorClass); } else { logger.error("{} marked to be registered as a World Generator, " + "but is not a subclass of WorldGenerator or lacks the correct constructor", generatorClass); From 911b6e0c0024f653a5ed18330547b4d758036c4d Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Tue, 10 May 2022 13:27:46 -0700 Subject: [PATCH 11/25] chore(ModuleManager): debugging log --- .../java/org/terasology/engine/core/module/ModuleManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java b/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java index f6012eb5e9e..3440d0fce64 100644 --- a/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java +++ b/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.core.module; @@ -202,6 +202,7 @@ static Module loadAndConfigureEngineModule(ModuleFactory moduleFactory, List Date: Fri, 13 May 2022 17:19:28 -0700 Subject: [PATCH 12/25] test: move classes implementing Jupiter interfaces to their own subpackage --- .../terasology/engine/integrationenvironment/Engines.java | 5 +++-- .../engine/integrationenvironment/MainLoop.java | 1 + .../integrationenvironment/ModuleTestingHelper.java | 2 +- .../terasology/engine/integrationenvironment/Scopes.java | 1 + .../{extension => jupiter}/Dependencies.java | 4 +--- .../{ => jupiter}/IsolatedMTEExtension.java | 4 +++- .../{ => jupiter}/MTEExtension.java | 8 +++++--- .../{extension => jupiter}/UseWorldGenerator.java | 4 +--- .../engine/integrationenvironment/package-info.java | 2 +- .../{ => jupiter}/default-logback.xml | 0 .../engine/integrationenvironment/AssetLoadingTest.java | 1 + .../integrationenvironment/ChunkRegionFutureTest.java | 1 + .../integrationenvironment/ClientConnectionTest.java | 1 + .../integrationenvironment/ComponentSystemTest.java | 1 + .../engine/integrationenvironment/ExampleTest.java | 1 + .../engine/integrationenvironment/IsolatedEngineTest.java | 1 + .../MTEExtensionTestWithPerClassLifecycle.java | 1 + .../MTEExtensionTestWithPerMethodLifecycle.java | 1 + .../ModuleTestingEnvironmentTest.java | 1 + .../engine/integrationenvironment/NestedTest.java | 1 + .../integrationenvironment/SubclassInjectionTest.java | 1 + .../integrationenvironment/TestEventReceiverTest.java | 1 + .../engine/integrationenvironment/WorldProviderTest.java | 1 + .../integrationenvironment/delay/DelayManagerTest.java | 2 +- 24 files changed, 31 insertions(+), 15 deletions(-) rename engine-tests/src/main/java/org/terasology/engine/integrationenvironment/{extension => jupiter}/Dependencies.java (80%) rename engine-tests/src/main/java/org/terasology/engine/integrationenvironment/{ => jupiter}/IsolatedMTEExtension.java (85%) rename engine-tests/src/main/java/org/terasology/engine/integrationenvironment/{ => jupiter}/MTEExtension.java (96%) rename engine-tests/src/main/java/org/terasology/engine/integrationenvironment/{extension => jupiter}/UseWorldGenerator.java (80%) rename engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/{ => jupiter}/default-logback.xml (100%) diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java index 768c3eb09c0..c329fbd0779 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java @@ -33,6 +33,7 @@ import org.terasology.engine.core.subsystem.lwjgl.LwjglInput; import org.terasology.engine.core.subsystem.lwjgl.LwjglTimer; import org.terasology.engine.core.subsystem.openvr.OpenVRInput; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.network.JoinStatus; import org.terasology.engine.network.NetworkSystem; import org.terasology.engine.registry.CoreRegistry; @@ -93,7 +94,7 @@ public Engines(Set dependencies, String worldGeneratorUri) { *

* Every instance should be shut down properly by calling {@link #tearDown()}. */ - protected void setup() { + public void setup() { mockPathManager(); try { host = createHost(); @@ -110,7 +111,7 @@ protected void setup() { *

* Used to properly shut down and clean up a testing environment set up and started with {@link #setup()}. */ - protected void tearDown() { + public void tearDown() { engines.forEach(TerasologyEngine::shutdown); engines.forEach(TerasologyEngine::cleanup); engines.clear(); diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java index 6286af0962e..7bc1080704f 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MainLoop.java @@ -17,6 +17,7 @@ import org.terasology.engine.core.TerasologyEngine; import org.terasology.engine.core.Time; import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.world.WorldProvider; import org.terasology.engine.world.block.BlockRegion; import org.terasology.engine.world.block.BlockRegionc; diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java index d864f59e1f9..6d49b7d3e15 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingHelper.java @@ -31,7 +31,7 @@ public class ModuleTestingHelper implements ModuleTestingEnvironment { final Engines engines; final MainLoop mainLoop; - ModuleTestingHelper(Engines engines) { + public ModuleTestingHelper(Engines engines) { this.engines = engines; this.mainLoop = new MainLoop(engines); } diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java index 54f0f47cc97..74d9ff1f58f 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Scopes.java @@ -6,6 +6,7 @@ import com.google.common.collect.ObjectArrays; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.extension.ExtensionContext; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import java.util.function.Function; diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/Dependencies.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/Dependencies.java similarity index 80% rename from engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/Dependencies.java rename to engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/Dependencies.java index 280ae2e55ca..4aa5da9b628 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/Dependencies.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/Dependencies.java @@ -1,9 +1,7 @@ // Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -package org.terasology.engine.integrationenvironment.extension; - -import org.terasology.engine.integrationenvironment.MTEExtension; +package org.terasology.engine.integrationenvironment.jupiter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/IsolatedMTEExtension.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/IsolatedMTEExtension.java similarity index 85% rename from engine-tests/src/main/java/org/terasology/engine/integrationenvironment/IsolatedMTEExtension.java rename to engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/IsolatedMTEExtension.java index 41aed5e1f1a..230fd697493 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/IsolatedMTEExtension.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/IsolatedMTEExtension.java @@ -1,7 +1,9 @@ // Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -package org.terasology.engine.integrationenvironment; +package org.terasology.engine.integrationenvironment.jupiter; + +import org.terasology.engine.integrationenvironment.Scopes; /** * Subclass of {@link MTEExtension} which isolates all test cases by creating a new engine for each test. This is much diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MTEExtension.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/MTEExtension.java similarity index 96% rename from engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MTEExtension.java rename to engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/MTEExtension.java index 033ed0f7826..3eb0d757393 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/MTEExtension.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/MTEExtension.java @@ -1,7 +1,7 @@ // Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -package org.terasology.engine.integrationenvironment; +package org.terasology.engine.integrationenvironment.jupiter; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; @@ -17,8 +17,10 @@ import org.junit.jupiter.api.extension.TestInstancePostProcessor; import org.opentest4j.MultipleFailuresError; import org.slf4j.LoggerFactory; -import org.terasology.engine.integrationenvironment.extension.Dependencies; -import org.terasology.engine.integrationenvironment.extension.UseWorldGenerator; +import org.terasology.engine.integrationenvironment.Engines; +import org.terasology.engine.integrationenvironment.MainLoop; +import org.terasology.engine.integrationenvironment.ModuleTestingHelper; +import org.terasology.engine.integrationenvironment.Scopes; import org.terasology.engine.registry.In; import org.terasology.unittest.worlds.DummyWorldGenerator; diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/UseWorldGenerator.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/UseWorldGenerator.java similarity index 80% rename from engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/UseWorldGenerator.java rename to engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/UseWorldGenerator.java index ff3cd63b1b0..75bbd1217b6 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/extension/UseWorldGenerator.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/UseWorldGenerator.java @@ -1,9 +1,7 @@ // Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -package org.terasology.engine.integrationenvironment.extension; - -import org.terasology.engine.integrationenvironment.MTEExtension; +package org.terasology.engine.integrationenvironment.jupiter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java index 2aeec8381fb..e88a11277ef 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/package-info.java @@ -6,7 +6,7 @@ *

* Key points of interest for test authors are: *

    - *
  • {@link org.terasology.engine.integrationenvironment.MTEExtension MTEExtension}: Use this on your JUnit 5 test classes. + *
  • {@link org.terasology.engine.integrationenvironment.jupiter.MTEExtension MTEExtension}: Use this on your JUnit 5 test classes. *
  • {@link org.terasology.engine.integrationenvironment.MainLoop MainLoop}: Methods for running the engine during your test scenarios. *
  • {@link org.terasology.engine.integrationenvironment.Engines}: You can add additional engines to simulate remote connections to the * host. [Experimental] diff --git a/engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/default-logback.xml b/engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/jupiter/default-logback.xml similarity index 100% rename from engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/default-logback.xml rename to engine-tests/src/main/resources/org/terasology/engine/integrationenvironment/jupiter/default-logback.xml diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java index aa0a5896535..4462ca8610b 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/AssetLoadingTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.prefab.Prefab; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.registry.In; import org.terasology.engine.world.block.Block; import org.terasology.engine.world.block.BlockManager; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java index cc5d15c8ee6..a986420155e 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ChunkRegionFutureTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.registry.In; import org.terasology.engine.world.WorldProvider; import org.terasology.engine.world.block.Block; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java index d64fe1583e9..017378cdfa8 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientConnectionTest.java @@ -9,6 +9,7 @@ import org.terasology.engine.context.Context; import org.terasology.engine.core.TerasologyEngine; import org.terasology.engine.core.modes.StateIngame; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import java.io.IOException; import java.util.List; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java index e33a28f4cd8..2c47ab90222 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ComponentSystemTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; import org.terasology.unittest.stubs.DummyEvent; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java index a682b8f36db..7896c0ab939 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ExampleTest.java @@ -11,6 +11,7 @@ import org.terasology.engine.context.Context; import org.terasology.engine.core.Time; import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.logic.players.LocalPlayer; import org.terasology.engine.logic.players.event.ResetCameraEvent; import org.terasology.engine.network.ClientComponent; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java index 903c7d44301..a6eb3f0109a 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/IsolatedEngineTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.jupiter.IsolatedMTEExtension; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; import org.terasology.unittest.stubs.DummyEvent; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java index 9f08526fb5b..6c4b8cc0fc5 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerClassLifecycle.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; import org.terasology.unittest.stubs.DummyEvent; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java index 27666c45510..ae2633492c2 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/MTEExtensionTestWithPerMethodLifecycle.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; import org.terasology.unittest.stubs.DummyEvent; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java index 2d9c4103587..cfbd7a7b319 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironmentTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java index 4fbc103ca8a..7e6904a85c2 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/NestedTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.registry.In; @Tag("MteTest") diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java index e3de96631d2..b1d0b906085 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/SubclassInjectionTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.terasology.engine.integrationenvironment.fixtures.BaseTestingClass; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; @Tag("MteTest") @ExtendWith(MTEExtension.class) diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java index a79705031f2..f565f3ae754 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TestEventReceiverTest.java @@ -9,6 +9,7 @@ import org.terasology.engine.context.Context; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.logic.location.LocationComponent; import org.terasology.engine.registry.In; import org.terasology.unittest.stubs.DummyComponent; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java index b167cff4217..738190f8cd1 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/WorldProviderTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.registry.In; import org.terasology.engine.world.WorldProvider; import org.terasology.engine.world.block.BlockManager; diff --git a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java index 2692709c9dd..8ad0ec2df73 100644 --- a/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/integrationenvironment/delay/DelayManagerTest.java @@ -13,9 +13,9 @@ import org.terasology.engine.core.Time; import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; -import org.terasology.engine.integrationenvironment.MTEExtension; import org.terasology.engine.integrationenvironment.ModuleTestingHelper; import org.terasology.engine.integrationenvironment.TestEventReceiver; +import org.terasology.engine.integrationenvironment.jupiter.MTEExtension; import org.terasology.engine.logic.delay.DelayManager; import org.terasology.engine.logic.delay.DelayedActionTriggeredEvent; import org.terasology.engine.network.ClientComponent; From 5498b63fdb756b52f34db6c76e29d7332175bde1 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 15 May 2022 14:02:05 -0700 Subject: [PATCH 13/25] fix(ChunkProcessingPipeline): base thread pool size on available processors (#5014) --- .../world/chunks/pipeline/ChunkProcessingPipeline.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ChunkProcessingPipeline.java b/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ChunkProcessingPipeline.java index d3cefbd6a4f..5323ec245c5 100644 --- a/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ChunkProcessingPipeline.java +++ b/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ChunkProcessingPipeline.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.world.chunks.pipeline; @@ -34,6 +34,8 @@ import java.util.function.Function; import java.util.function.Supplier; +import static com.google.common.primitives.Ints.constrainToRange; + /** * Manages execution of chunk processing. *

    @@ -41,7 +43,9 @@ */ public class ChunkProcessingPipeline { - private static final int NUM_TASK_THREADS = 4; + @SuppressWarnings("UnstableApiUsage") + private static final int NUM_TASK_THREADS = constrainToRange( + Runtime.getRuntime().availableProcessors() - 1, 1, 8); private static final Logger logger = LoggerFactory.getLogger(ChunkProcessingPipeline.class); private final List stages = Lists.newArrayList(); @@ -71,6 +75,7 @@ protected RunnableFuture newTaskFor(Callable callable) { return new PositionFuture<>(newTaskFor, ((PositionalCallable) callable).getPosition()); } }; + logger.debug("allocated {} threads", NUM_TASK_THREADS); chunkProcessor = new ExecutorCompletionService<>(executor, new PriorityBlockingQueue<>(800, comparable)); reactor = new Thread(this::chunkTaskHandler); From 8d2051548513755b33b524d9e2b2f81de1e2c513 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 15 May 2022 17:57:31 -0700 Subject: [PATCH 14/25] fix(test): set jacoco excludes for engine-test's unitTest and integrationTest tasks (#5013) Co-authored-by: Tobias Nett --- config/gradle/common.gradle | 8 +++----- engine-tests/build.gradle | 5 +++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/gradle/common.gradle b/config/gradle/common.gradle index 302006bdd59..873ae8ba70b 100644 --- a/config/gradle/common.gradle +++ b/config/gradle/common.gradle @@ -66,14 +66,12 @@ test { dependsOn rootProject.extractNatives } - jacoco { - excludes = ["org.terasology.protobuf.*", - "*MethodAccess","*FieldAccess"] - } + // Keep in sync with other exclude-lists for Jacoco, e.g., in 'engine-tests/build.gradle' + jacoco.excludes = ["org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess"] } jacoco { - toolVersion = "0.8.5" + toolVersion = "0.8.8" } jacocoTestReport { diff --git a/engine-tests/build.gradle b/engine-tests/build.gradle index 41a61f513bf..26aea75c40c 100644 --- a/engine-tests/build.gradle +++ b/engine-tests/build.gradle @@ -107,6 +107,9 @@ task unitTest(type: Test) { excludeTags "MteTest", "TteTest" } systemProperty("junit.jupiter.execution.timeout.default", "1m") + + // Keep in sync with other exclude-lists for Jacoco, e.g., in 'common.gradle' + jacoco.excludes = ["org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess"] } task integrationTest(type: Test) { @@ -121,6 +124,8 @@ task integrationTest(type: Test) { includeTags "MteTest", "TteTest" } systemProperty("junit.jupiter.execution.timeout.default", "5m") + + jacoco.excludes = ["org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess"] } idea { From cf4ccb5e4fc14e2783728dc1c84b4367c3f1bab0 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Wed, 18 May 2022 12:44:30 -0700 Subject: [PATCH 15/25] chore: remove unused ByteCodeReflectFactory (#5016) --- .../reflect/ByteCodeReflectFactoryTest.java | 70 ------- engine/build.gradle | 1 - .../ReflectionFactoryBenchmark.java | 174 ------------------ .../engine/reflection/package-info.java | 18 -- .../reflect/ByteCodeReflectFactory.java | 147 --------------- 5 files changed, 410 deletions(-) delete mode 100644 engine-tests/src/test/java/org/terasology/reflection/reflect/ByteCodeReflectFactoryTest.java delete mode 100644 engine/src/jmh/java/org/terasology/benchmark/reflectFactory/ReflectionFactoryBenchmark.java delete mode 100644 engine/src/main/java/org/terasology/engine/reflection/package-info.java delete mode 100644 engine/src/main/java/org/terasology/engine/reflection/reflect/ByteCodeReflectFactory.java diff --git a/engine-tests/src/test/java/org/terasology/reflection/reflect/ByteCodeReflectFactoryTest.java b/engine-tests/src/test/java/org/terasology/reflection/reflect/ByteCodeReflectFactoryTest.java deleted file mode 100644 index 09e653ebee3..00000000000 --- a/engine-tests/src/test/java/org/terasology/reflection/reflect/ByteCodeReflectFactoryTest.java +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.reflection.reflect; - -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; -import org.terasology.engine.logic.characters.events.AttackRequest; -import org.terasology.engine.logic.location.LocationComponent; -import org.terasology.engine.reflection.reflect.ByteCodeReflectFactory; -import org.terasology.unittest.stubs.GetterSetterComponent; -import org.terasology.unittest.stubs.IntegerComponent; -import org.terasology.unittest.stubs.StringComponent; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ByteCodeReflectFactoryTest { - - @Test - public void testCreateConstructorObjectWithPublicConstructor() throws NoSuchMethodException { - ReflectFactory reflectFactory = new ByteCodeReflectFactory(); - ObjectConstructor constructor = reflectFactory.createConstructor(LocationComponent.class); - LocationComponent locationComponent = constructor.construct(); - assertNotNull(locationComponent); - } - - @Test - public void testCreateConstructorObjectWithProtectedConstructor() throws Exception { - ReflectFactory reflectFactory = new ByteCodeReflectFactory(); - ObjectConstructor constructor = reflectFactory.createConstructor(AttackRequest.class); - AttackRequest result = constructor.construct(); - assertNotNull(result); - } - - @Test - public void testCreateFieldAccessorWithGetterSetter() throws Exception { - ReflectFactory reflectFactory = new ByteCodeReflectFactory(); - FieldAccessor fieldAccessor = reflectFactory.createFieldAccessor(GetterSetterComponent.class, - GetterSetterComponent.class.getDeclaredField("value"), Vector3f.class); - GetterSetterComponent comp = new GetterSetterComponent(); - Vector3f newVal = new Vector3f(1, 2, 3); - fieldAccessor.setValue(comp, newVal); - assertTrue(comp.setterUsed); - - assertEquals(newVal, fieldAccessor.getValue(comp)); - assertTrue(comp.getterUsed); - } - - @Test - public void testCreateFieldAccessorDirectToField() throws Exception { - ReflectFactory reflectFactory = new ByteCodeReflectFactory(); - FieldAccessor fieldAccessor - = reflectFactory.createFieldAccessor(StringComponent.class, StringComponent.class.getDeclaredField("value"), String.class); - StringComponent comp = new StringComponent(); - fieldAccessor.setValue(comp, "String"); - assertEquals("String", fieldAccessor.getValue(comp)); - } - - @Test - public void testAccessIntegerField() throws Exception { - ReflectFactory reflectFactory = new ByteCodeReflectFactory(); - FieldAccessor fieldAccessor - = reflectFactory.createFieldAccessor(IntegerComponent.class, IntegerComponent.class.getDeclaredField("value")); - IntegerComponent comp = new IntegerComponent(); - fieldAccessor.setValue(comp, 1); - assertEquals(1, fieldAccessor.getValue(comp)); - } - -} diff --git a/engine/build.gradle b/engine/build.gradle index a219b9875b3..dffa7e31211 100644 --- a/engine/build.gradle +++ b/engine/build.gradle @@ -88,7 +88,6 @@ dependencies { // Java magic implementation group: 'net.java.dev.jna', name: 'jna-platform', version: '5.6.0' implementation "org.terasology:reflections:0.9.12-MB" - implementation group: 'org.javassist', name: 'javassist', version: '3.27.0-GA' implementation group: 'com.esotericsoftware', name: 'reflectasm', version: '1.11.9' // Graphics, 3D, UI, etc diff --git a/engine/src/jmh/java/org/terasology/benchmark/reflectFactory/ReflectionFactoryBenchmark.java b/engine/src/jmh/java/org/terasology/benchmark/reflectFactory/ReflectionFactoryBenchmark.java deleted file mode 100644 index 6413cbbad0f..00000000000 --- a/engine/src/jmh/java/org/terasology/benchmark/reflectFactory/ReflectionFactoryBenchmark.java +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 - -package org.terasology.benchmark.reflectFactory; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.terasology.engine.logic.common.DisplayNameComponent; -import org.terasology.engine.logic.location.LocationComponent; -import org.terasology.engine.reflection.reflect.ByteCodeReflectFactory; -import org.terasology.reflection.reflect.FieldAccessor; -import org.terasology.reflection.reflect.ObjectConstructor; -import org.terasology.reflection.reflect.ReflectFactory; -import org.terasology.reflection.reflect.ReflectionReflectFactory; - -import java.util.concurrent.TimeUnit; - -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@Warmup(iterations = 1) -@Measurement(iterations = 1) -public class ReflectionFactoryBenchmark { - - @Benchmark - public Object byteCodeConstructor(ByteCodeState state) { - return state.constructor.construct(); - } - - @Benchmark - public Object reflectionConstructor(ReflectionState state) { - return state.constructor.construct(); - } - - @Benchmark - public Object byteCodeFieldAccessGet(ByteCodeState state, FieldComponentState fieldComponentState) { - return state.fieldAccessor.getValue(fieldComponentState.component); - } - - @Benchmark - public Object reflectionFieldAccessGet(ReflectionState state, FieldComponentState fieldComponentState) { - return state.fieldAccessor.getValue(fieldComponentState.component); - } - - @Benchmark - public void byteCodeFieldAccessSet(ByteCodeState state, FieldComponentState fieldComponentState) { - state.fieldAccessor.setValue(fieldComponentState.component, fieldComponentState.value); - } - - @Benchmark - public void reflectionFieldAccessSet(ReflectionState state, FieldComponentState fieldComponentState) { - state.fieldAccessor.setValue(fieldComponentState.component, fieldComponentState.value); - } - - @Benchmark - public Object byteCodeGetterSetterAccessGet(ByteCodeState state, - GetterSetterComponentState getterSetterComponentState) { - return state.getterSetterAccessor.getValue(getterSetterComponentState.component); - } - - @Benchmark - public Object reflectionGetterSetterAccessGet(ReflectionState state, - GetterSetterComponentState getterSetterComponentState) { - return state.getterSetterAccessor.getValue(getterSetterComponentState.component); - } - - - @Benchmark - public void byteCodeGetterSetterAccessSet(ByteCodeState state, - GetterSetterComponentState getterSetterComponentState) { - state.getterSetterAccessor.setValue(getterSetterComponentState.component, getterSetterComponentState.value); - } - - @Benchmark - public void reflectionGetterSetterAccessSet(ReflectionState state, - GetterSetterComponentState getterSetterComponentState) { - state.getterSetterAccessor.setValue(getterSetterComponentState.component, getterSetterComponentState.value); - } - - - @Benchmark - public void directGetterSetterSet(GetterSetterComponentState getterSetterComponentState) { - getterSetterComponentState.component.setValue(getterSetterComponentState.value); - } - - @Benchmark - public Object directGetterSetterGet(GetterSetterComponentState getterSetterComponentState) { - return getterSetterComponentState.component.getValue(); - } - - @Benchmark - public void directFieldSet(FieldComponentState state) { - state.component.name = state.value; - } - - @Benchmark - public Object directFieldGet(FieldComponentState state) { - return state.component.name; - } - - @Benchmark - public Object directConstructor() { - return new LocationComponent(); - } - - @State(Scope.Thread) - public static class GetterSetterComponentState { - private GetterSetterComponent component; - private int value; - - @Setup - public void setup() { - component = new GetterSetterComponent(); - component.setValue(1); - value = 90; - } - } - - @State(Scope.Thread) - public static class FieldComponentState { - private DisplayNameComponent component; - private String value; - - @Setup - public void setup() { - component = new DisplayNameComponent(); - component.name = "dummy"; - value = "dummy1"; - } - } - - - @State(Scope.Thread) - public static class ByteCodeState extends StateObject { - - @Override - ReflectFactory getReflectFactory() { - return new ByteCodeReflectFactory(); - } - } - - @State(Scope.Thread) - public static class ReflectionState extends StateObject { - - @Override - ReflectFactory getReflectFactory() { - return new ReflectionReflectFactory(); - } - } - - public abstract static class StateObject { - ObjectConstructor constructor; - FieldAccessor fieldAccessor; - FieldAccessor getterSetterAccessor; - - @Setup - public void setup() throws Exception { - ReflectFactory reflectFactory = getReflectFactory(); - constructor = reflectFactory.createConstructor(LocationComponent.class); - fieldAccessor = reflectFactory.createFieldAccessor(DisplayNameComponent.class, - DisplayNameComponent.class.getField("description")); - getterSetterAccessor = reflectFactory.createFieldAccessor(GetterSetterComponent.class, - GetterSetterComponent.class.getDeclaredField("value")); - } - - abstract ReflectFactory getReflectFactory(); - } -} diff --git a/engine/src/main/java/org/terasology/engine/reflection/package-info.java b/engine/src/main/java/org/terasology/engine/reflection/package-info.java deleted file mode 100644 index ac41eff5b39..00000000000 --- a/engine/src/main/java/org/terasology/engine/reflection/package-info.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 - -/** - * This package provides a low-level system for describing classes and fields, with support for construction and field access. - * Essentially it is a simplified reflection - * framework. - *

    - * To support this functionality, a copy-strategy library is used to provide copying support for types. This is used instead of cloning because - *

      - *
    1. Not all types support cloning, including types outside of our control
    2. - *
    3. Cloning doesn't allow for possible performance improvements though runtime generated code
    4. - *
    5. Cloning is generally a poorly implemented feature of Java - copy constructors and methods are preferred
    6. - *
    - *

    - * Additionally, ReflectFactory is used to provide support for construction and field access, to allow for alternate implementations. - */ -package org.terasology.engine.reflection; diff --git a/engine/src/main/java/org/terasology/engine/reflection/reflect/ByteCodeReflectFactory.java b/engine/src/main/java/org/terasology/engine/reflection/reflect/ByteCodeReflectFactory.java deleted file mode 100644 index 8f4b52c31aa..00000000000 --- a/engine/src/main/java/org/terasology/engine/reflection/reflect/ByteCodeReflectFactory.java +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.engine.reflection.reflect; - -import com.esotericsoftware.reflectasm.FieldAccess; -import com.esotericsoftware.reflectasm.MethodAccess; -import javassist.CannotCompileException; -import javassist.ClassPool; -import javassist.CtClass; -import javassist.CtMethod; -import javassist.CtNewMethod; -import javassist.NotFoundException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.terasology.reflection.reflect.FieldAccessor; -import org.terasology.reflection.reflect.InaccessibleFieldException; -import org.terasology.reflection.reflect.ObjectConstructor; -import org.terasology.reflection.reflect.ReflectFactory; -import org.terasology.reflection.reflect.ReflectionReflectFactory; -import org.terasology.engine.utilities.ReflectionUtil; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -public class ByteCodeReflectFactory implements ReflectFactory { - private static final Logger logger = LoggerFactory.getLogger(ByteCodeReflectFactory.class); - - private ClassPool pool; - private CtClass objectConstructorInterface; - - private ReflectFactory backupFactory = new ReflectionReflectFactory(); - - public ByteCodeReflectFactory() { - try { - ClassPool.doPruning = true; - pool = ClassPool.getDefault(); - objectConstructorInterface = pool.get(ObjectConstructor.class.getName()); - } catch (NotFoundException e) { - throw new RuntimeException("Error establishing reflection factory", e); - } - } - - @Override - public ObjectConstructor createConstructor(Class type) throws NoSuchMethodException { - String constructorClassName = type.getName() + "_ReflectConstructor"; - try { - return (ObjectConstructor) type.getClassLoader().loadClass(constructorClassName).getConstructor().newInstance(); - } catch (ClassNotFoundException ignored) { - try { - if (Modifier.isPrivate(type.getDeclaredConstructor().getModifiers())) { - logger.warn("Constructor for '{}' exists but is private, falling back on reflection", type); - return backupFactory.createConstructor(type); - } - - CtClass constructorClass = pool.makeClass(type.getName() + "_ReflectConstructor"); - constructorClass.setInterfaces(new CtClass[]{objectConstructorInterface}); - - CtMethod method = CtNewMethod.make("public Object construct() { return new " + type.getName() + "();}", constructorClass); - constructorClass.addMethod(method); - return (ObjectConstructor) (constructorClass.toClass(type.getClassLoader(), type.getProtectionDomain()) - .getConstructor().newInstance()); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | CannotCompileException e) { - logger.error("Error instantiating constructor object for '{}', falling back on reflection", type, e); - return backupFactory.createConstructor(type); - } catch (NoSuchMethodException e) { - return null; - } - } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { - logger.error("Error instantiating constructor object for '{}', falling back on reflection", type, e); - return backupFactory.createConstructor(type); - } - - } - - @Override - public FieldAccessor createFieldAccessor(Class ownerType, Field field) throws InaccessibleFieldException { - return createFieldAccessor(ownerType, field, field.getType()); - } - - @Override - public FieldAccessor createFieldAccessor(Class ownerType, Field field, Class fieldType) throws InaccessibleFieldException { - try { - return new ReflectASMFieldAccessor<>(ownerType, field, fieldType); - } catch (IllegalArgumentException | InaccessibleFieldException e) { - logger.warn("Failed to create accessor for field '{}' of type '{}', falling back on reflection", field.getName(), ownerType.getName()); - return backupFactory.createFieldAccessor(ownerType, field, fieldType); - } - } - - public void setClassPool(ClassPool classPool) { - pool = classPool; - } - - private static class ReflectASMFieldAccessor implements FieldAccessor { - - private static final int NO_METHOD = -1; - - private MethodAccess methodAccess; - private int getterIndex = NO_METHOD; - private int setterIndex = NO_METHOD; - private FieldAccess fieldAccess; - private int fieldIndex; - - ReflectASMFieldAccessor(Class ownerType, Field field, Class fieldType) throws InaccessibleFieldException { - methodAccess = MethodAccess.get(ownerType); - Method getter = ReflectionUtil.findGetter(field); - if (getter != null) { - getterIndex = methodAccess.getIndex(getter.getName()); - } - Method setter = ReflectionUtil.findSetter(field); - if (setter != null) { - setterIndex = methodAccess.getIndex(setter.getName()); - } - - if (getterIndex == NO_METHOD || setterIndex == NO_METHOD) { - fieldAccess = FieldAccess.get(ownerType); - try { - fieldIndex = fieldAccess.getIndex(field.getName()); - } catch (IllegalArgumentException e) { - throw new InaccessibleFieldException("Failed to create accessor for field '" + field.getName() - + "' of type '" + ownerType.getName() + "'", e); - } - } - } - - @Override - @SuppressWarnings("unchecked") - public U getValue(T target) { - if (getterIndex != NO_METHOD) { - return (U) methodAccess.invoke(target, getterIndex); - } else { - return (U) fieldAccess.get(target, fieldIndex); - } - } - - @Override - public void setValue(T target, U value) { - if (setterIndex != NO_METHOD) { - methodAccess.invoke(target, setterIndex, value); - } else { - fieldAccess.set(target, fieldIndex, value); - } - } - } -} From 199155678a33188ee3fbddefc98b410c6fd51ebd Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Thu, 19 May 2022 16:34:06 -0700 Subject: [PATCH 16/25] test: turn down WorldGeneratorManager log level while running MTE tests Gosh, that is a _lot_ of lines for a log statement, but slf4j 1.7 doesn't have a way to parameterize the severity level. --- .../terasology/engine/core/module/ModuleManager.java | 2 +- .../generator/internal/WorldGeneratorManager.java | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java b/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java index 3440d0fce64..52a5a249944 100644 --- a/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java +++ b/engine/src/main/java/org/terasology/engine/core/module/ModuleManager.java @@ -98,7 +98,7 @@ public ModuleManager(Config config, List> classesOnClasspathsToAddToEng this(config.getNetwork().getMasterServer(), classesOnClasspathsToAddToEngine); } - protected static boolean isLoadingClasspathModules() { + public static boolean isLoadingClasspathModules() { return Boolean.getBoolean(LOAD_CLASSPATH_MODULES_PROPERTY); } diff --git a/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java b/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java index ad365ef587f..52b540bab8b 100644 --- a/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java +++ b/engine/src/main/java/org/terasology/engine/world/generator/internal/WorldGeneratorManager.java @@ -49,10 +49,14 @@ public void refresh() { try (ModuleEnvironment tempEnvironment = moduleManager.loadEnvironment(resolutionResult.getModules(), false)) { for (Class generatorClass : tempEnvironment.getTypesAnnotatedWith(RegisterWorldGenerator.class)) { Name providedBy = tempEnvironment.getModuleProviding(generatorClass); - if (providedBy == null) { - // These tend to be engine-module-is-weird cases. - logger.warn("{} found while inspecting {} but is not provided by any module.", - generatorClass, moduleId); + if (providedBy == null) { // These tend to be engine-module-is-weird cases. + String s = "{} found while inspecting {} but is not provided by any module."; + if (!ModuleManager.isLoadingClasspathModules()) { + logger.warn(s, generatorClass, moduleId); // Deserves WARNING level in production. + } else { + // …but happens a *lot* when loading modules from classpath, such as MTE. + logger.debug(s, generatorClass, moduleId); + } } else if (providedBy.equals(module.getId())) { RegisterWorldGenerator annotation = generatorClass.getAnnotation(RegisterWorldGenerator.class); if (isValidWorldGenerator(generatorClass)) { From b5b42ae4db1a0e21e6fbed503855ebcdc58e67aa Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Thu, 19 May 2022 16:34:58 -0700 Subject: [PATCH 17/25] test: fail earlier when explicitly-named test dependencies are not found --- .../integrationenvironment/TestingStateHeadlessSetup.java | 5 +++++ .../core/subsystem/headless/mode/StateHeadlessSetup.java | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java index e6eb3295e09..0bd16b057ec 100644 --- a/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java +++ b/engine-tests/src/main/java/org/terasology/engine/integrationenvironment/TestingStateHeadlessSetup.java @@ -28,6 +28,11 @@ public class TestingStateHeadlessSetup extends StateHeadlessSetup { private final Collection dependencies; private final SimpleUri worldGeneratorUri; + + { + strictModuleRequirements = true; + } + public TestingStateHeadlessSetup(Collection dependencies, String worldGeneratorUri) { this.dependencies = dependencies; this.worldGeneratorUri = new SimpleUri(worldGeneratorUri); diff --git a/engine/src/main/java/org/terasology/engine/core/subsystem/headless/mode/StateHeadlessSetup.java b/engine/src/main/java/org/terasology/engine/core/subsystem/headless/mode/StateHeadlessSetup.java index 4eae2b18c95..87bfc5ee37f 100644 --- a/engine/src/main/java/org/terasology/engine/core/subsystem/headless/mode/StateHeadlessSetup.java +++ b/engine/src/main/java/org/terasology/engine/core/subsystem/headless/mode/StateHeadlessSetup.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.core.subsystem.headless.mode; @@ -33,6 +33,8 @@ public class StateHeadlessSetup extends AbstractState { private static final Logger logger = LoggerFactory.getLogger(StateHeadlessSetup.class); + protected boolean strictModuleRequirements; + public StateHeadlessSetup() { } @@ -68,6 +70,8 @@ public GameManifest createGameManifest() { Module module = moduleManager.getRegistry().getLatestModuleVersion(moduleName); if (module != null) { gameManifest.addModule(module.getId(), module.getVersion()); + } else if (strictModuleRequirements) { + throw new RuntimeException("ModuleRegistry has no latest version for module " + moduleName); } else { logger.warn("ModuleRegistry has no latest version for module {}", moduleName); } From 3cf39f5120da933a26aa7bac24fe85a88eb0883d Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Thu, 19 May 2022 16:43:06 -0700 Subject: [PATCH 18/25] build(modules): copy assets and module.txt every time processResources is run Instead of waiting for the jar task. This is probably the reason things didn't seem to work unless you built jars _before_ running the code. --- .../main/kotlin/terasology-module.gradle.kts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/build-logic/src/main/kotlin/terasology-module.gradle.kts b/build-logic/src/main/kotlin/terasology-module.gradle.kts index a92054aa69e..9ba7a4823e9 100644 --- a/build-logic/src/main/kotlin/terasology-module.gradle.kts +++ b/build-logic/src/main/kotlin/terasology-module.gradle.kts @@ -144,21 +144,14 @@ tasks.register("syncDeltas") { into("${mainSourceSet.output.classesDirs.first()}/deltas") } -// Instructions for packaging a jar file - is a manifest even needed for modules? -tasks.named("jar") { - // Make sure the assets directory is included - dependsOn("syncAssets") - dependsOn("syncOverrides") - dependsOn("syncDeltas") - - // Jarring needs to copy module.txt and all the assets into the output - doFirst { - copy { - from("module.txt") - into(mainSourceSet.output.classesDirs.first()) - } - } +tasks.register("syncModuleInfo") { + from("module.txt") + into(mainSourceSet.output.classesDirs.first()) +} +tasks.named("processResources") { + // Make sure the assets directory is included + dependsOn("syncAssets", "syncOverrides", "syncDeltas", "syncModuleInfo") } tasks.named("test") { From b1a35c06f41d69d5d944d4764e8a7c3b46b076f4 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 21 May 2022 11:07:50 -0700 Subject: [PATCH 19/25] build: extract project metrics/analytics to terasology-metrics gradle plugin --- Jenkinsfile | 2 +- build-logic/build.gradle.kts | 7 +- .../main/kotlin/terasology-metrics.gradle.kts | 85 +++++++++++++++++++ .../main/kotlin/terasology-module.gradle.kts | 1 + build.gradle | 10 +-- config/gradle/common.gradle | 84 ------------------ engine-tests/build.gradle | 1 + engine/build.gradle | 3 +- facades/PC/build.gradle.kts | 3 +- 9 files changed, 102 insertions(+), 94 deletions(-) create mode 100644 build-logic/src/main/kotlin/terasology-metrics.gradle.kts diff --git a/Jenkinsfile b/Jenkinsfile index eed491396e4..b465aab7155 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -134,7 +134,7 @@ pipeline { recordIssues(skipBlames: true, enabledForFailure: true, tools: [ - spotBugs(pattern: '**/build/reports/spotbugs/main/*.xml', useRankAsPriority: true), + spotBugs(pattern: '**/build/reports/spotbugs/*.xml', useRankAsPriority: true), pmdParser(pattern: '**/build/reports/pmd/*.xml') ]) diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 55509f1f4e1..26a260ad7a7 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 import java.net.URI @@ -10,6 +10,7 @@ plugins { repositories { mavenCentral() google() // gestalt uses an annotation package by Google + gradlePluginPortal() maven { name = "Terasology Artifactory" @@ -40,6 +41,10 @@ dependencies { // for inspecting modules implementation("org.terasology.gestalt:gestalt-module:7.1.0") + // plugins we configure + implementation("com.github.spotbugs.snom:spotbugs-gradle-plugin:4.8.0") // TODO: upgrade with gradle 7.x + implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3") + api(kotlin("test")) } diff --git a/build-logic/src/main/kotlin/terasology-metrics.gradle.kts b/build-logic/src/main/kotlin/terasology-metrics.gradle.kts new file mode 100644 index 00000000000..3200aa61993 --- /dev/null +++ b/build-logic/src/main/kotlin/terasology-metrics.gradle.kts @@ -0,0 +1,85 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +import com.github.spotbugs.snom.SpotBugsExtension +import com.github.spotbugs.snom.SpotBugsTask + +plugins { + java + checkstyle + jacoco + pmd + `project-report` + id("com.github.spotbugs") + id("org.sonarqube") +} + +dependencies { + "pmd"("net.sourceforge.pmd:pmd-core:6.15.0") + "pmd"("net.sourceforge.pmd:pmd-java:6.15.0") +} + +the().toolVersion = "0.8.8" + +tasks.withType { + // TODO: does this need explicit `dependsOn`? + reports { + csv.isEnabled = false + html.isEnabled = true + xml.isEnabled = true + } +} + +tasks.withType { + useJUnitPlatform() + + // 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 + reports { + junitXml.isEnabled = true + } + jvmArgs("-Xms512m", "-Xmx1024m") + + // Make sure the natives have been extracted, but only for multi-workspace setups (not for solo module builds) + if (project.name != project(":").name) { + dependsOn(tasks.getByPath(":extractNatives")) + } + + configure { + excludes = listOf("org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess") + } +} + +// The config files here work in both a multi-project workspace (IDEs, running from source) and for solo module builds +// Solo module builds in Jenkins get a copy of the config dir from the engine harness so it still lives at root/config +// TODO: Maybe update other projects like modules to pull the zipped dependency so fewer quirks are needed in Jenkins +configure { + isIgnoreFailures = false + + val checkstyleDir = rootDir.resolve("config/metrics/checkstyle") + configDirectory.set(checkstyleDir) + setConfigProperties("samedir" to checkstyleDir) +} + +configure { + isIgnoreFailures = true + ruleSetFiles = files(rootDir.resolve("config/metrics/pmd/pmd.xml")) + // By default, gradle uses both ruleset file AND the rulesets. Override the ruleSets to use only those from the file + ruleSets = listOf() +} + +configure { + // The version of the spotbugs tool https://github.com/spotbugs/spotbugs + // not necessarily the same as the version of spotbugs-gradle-plugin + toolVersion.set("4.7.0") + ignoreFailures.set(true) + excludeFilter.set(file(rootDir.resolve("config/metrics/findbugs/findbugs-exclude.xml"))) +} + +tasks.named("spotbugsMain") { + reports.register("xml") { + enabled = true + } +} diff --git a/build-logic/src/main/kotlin/terasology-module.gradle.kts b/build-logic/src/main/kotlin/terasology-module.gradle.kts index a92054aa69e..39ff890b29d 100644 --- a/build-logic/src/main/kotlin/terasology-module.gradle.kts +++ b/build-logic/src/main/kotlin/terasology-module.gradle.kts @@ -11,6 +11,7 @@ plugins { `java-library` idea eclipse + id("terasology-metrics") } val moduleMetadata = ModuleMetadataForGradle.forProject(project) diff --git a/build.gradle b/build.gradle index c1e5ed907ed..09fb7111e28 100644 --- a/build.gradle +++ b/build.gradle @@ -25,12 +25,6 @@ buildscript { } dependencies { - //Spotbugs - classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.0.0" - - // SonarQube / Cloud scanning - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8" - // Protobuf plugin classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.16' @@ -52,6 +46,10 @@ plugins { // For the "Build and run using: Intellij IDEA | Gradle" switch id "org.jetbrains.gradle.plugin.idea-ext" version "1.0" + // Things applied by terasology-metrics + id("com.github.spotbugs") version "4.8.0" apply false // TODO: upgrade with gradle 7.x + id("org.sonarqube") version "3.3" apply false + id("terasology-repositories") } diff --git a/config/gradle/common.gradle b/config/gradle/common.gradle index 873ae8ba70b..0bb76c48bbf 100644 --- a/config/gradle/common.gradle +++ b/config/gradle/common.gradle @@ -8,14 +8,6 @@ apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' -// Analytics -apply plugin: 'project-report' -apply plugin: 'checkstyle' -apply plugin: 'pmd' -apply plugin: 'com.github.spotbugs' -apply plugin: 'jacoco' -apply plugin: 'org.sonarqube' - apply plugin: 'terasology-repositories' java { @@ -28,12 +20,6 @@ tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } -dependencies { - pmd('net.sourceforge.pmd:pmd-core:6.15.0') - pmd('net.sourceforge.pmd:pmd-java:6.15.0') - // the FindBugs version is set in the configuration -} - task sourceJar(type: Jar) { description = "Create a JAR with all sources" from sourceSets.main.allSource @@ -47,76 +33,6 @@ task javadocJar(type: Jar, dependsOn: javadoc) { archiveClassifier = 'javadoc' } -// Extra details provided for unit tests -test { - useJUnitPlatform() - - // 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 - reports { - junitXml.enabled = true - } - // Arguments to include while running tests - jvmArgs '-Xms512m', '-Xmx1024m' - - // Make sure the natives have been extracted, but only for multi-workspace setups (not for solo module builds) - if (project.name != project(':').name) { - dependsOn rootProject.extractNatives - } - - // Keep in sync with other exclude-lists for Jacoco, e.g., in 'engine-tests/build.gradle' - jacoco.excludes = ["org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess"] -} - -jacoco { - toolVersion = "0.8.8" -} - -jacocoTestReport { - dependsOn test // Despite doc saying this should be automatic we need to explicitly add it anyway :-( - reports { - // Used to be exec, but that had a binary incompatibility between JaCoCo 0.7.4 and 0.7.5 and issues with some plugins - xml.enabled true - csv.enabled false - html.enabled true - } -} - -// The config files here work in both a multi-project workspace (IDEs, running from source) and for solo module builds -// Solo module builds in Jenkins get a copy of the config dir from the engine harness so it still lives at root/config -// TODO: Maybe update other projects like modules to pull the zipped dependency so fewer quirks are needed in Jenkins -checkstyle { - ignoreFailures = false - - def checkstyleDir = rootProject.file('config/metrics/checkstyle') - configDirectory = checkstyleDir - configProperties.samedir = checkstyleDir -} - -pmd { - ignoreFailures = true - ruleSetFiles = files("$rootDir/config/metrics/pmd/pmd.xml") - // By default, gradle uses both ruleset file AND the rulesets. Override the ruleSets to use only those from the file - ruleSets = [] -} - -spotbugs { - toolVersion = '4.0.0' - ignoreFailures = true - excludeFilter = new File(rootDir, "config/metrics/findbugs/findbugs-exclude.xml") -} - -spotbugsMain { - reports { - xml { - enabled = true - destination = file("$buildDir/reports/spotbugs/main/spotbugs.xml") - } - } -} - tasks.javadoc { // Disable doclint messages about missing "@param" or "@return" tags. options.addBooleanOption("Xdoclint:all,-missing", true) diff --git a/engine-tests/build.gradle b/engine-tests/build.gradle index 26aea75c40c..eb2787e515f 100644 --- a/engine-tests/build.gradle +++ b/engine-tests/build.gradle @@ -6,6 +6,7 @@ plugins { id "java-library" id "org.jetbrains.gradle.plugin.idea-ext" + id "terasology-metrics" } // Grab all the common stuff like plugins to use, artifact repositories, code analysis config diff --git a/engine/build.gradle b/engine/build.gradle index dffa7e31211..58498ec2b1a 100644 --- a/engine/build.gradle +++ b/engine/build.gradle @@ -6,9 +6,10 @@ plugins { id "java-library" id "org.jetbrains.gradle.plugin.idea-ext" + id "com.google.protobuf" + id "terasology-metrics" } -apply plugin: 'com.google.protobuf' // Grab all the common stuff like plugins to use, artifact repositories, code analysis config, etc apply from: "$rootDir/config/gradle/publish.gradle" diff --git a/facades/PC/build.gradle.kts b/facades/PC/build.gradle.kts index 7f86a010240..75bac4831c0 100644 --- a/facades/PC/build.gradle.kts +++ b/facades/PC/build.gradle.kts @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 // The PC facade is responsible for the primary distribution - a plain Java application runnable on PCs @@ -15,6 +15,7 @@ import kotlin.test.fail plugins { application id("terasology-dist") + id("terasology-metrics") id("facade") } From 90af4b2c8f14392aeefc4e227cbdf6fe25d262e7 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 21 May 2022 12:11:28 -0700 Subject: [PATCH 20/25] test: remove jacoco CI wasn't even reading the jacoco report at all. --- .../main/kotlin/terasology-metrics.gradle.kts | 16 ---------------- engine-tests/build.gradle | 5 ----- 2 files changed, 21 deletions(-) diff --git a/build-logic/src/main/kotlin/terasology-metrics.gradle.kts b/build-logic/src/main/kotlin/terasology-metrics.gradle.kts index 3200aa61993..efdd3da3237 100644 --- a/build-logic/src/main/kotlin/terasology-metrics.gradle.kts +++ b/build-logic/src/main/kotlin/terasology-metrics.gradle.kts @@ -7,7 +7,6 @@ import com.github.spotbugs.snom.SpotBugsTask plugins { java checkstyle - jacoco pmd `project-report` id("com.github.spotbugs") @@ -19,17 +18,6 @@ dependencies { "pmd"("net.sourceforge.pmd:pmd-java:6.15.0") } -the().toolVersion = "0.8.8" - -tasks.withType { - // TODO: does this need explicit `dependsOn`? - reports { - csv.isEnabled = false - html.isEnabled = true - xml.isEnabled = true - } -} - tasks.withType { useJUnitPlatform() @@ -46,10 +34,6 @@ tasks.withType { if (project.name != project(":").name) { dependsOn(tasks.getByPath(":extractNatives")) } - - configure { - excludes = listOf("org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess") - } } // The config files here work in both a multi-project workspace (IDEs, running from source) and for solo module builds diff --git a/engine-tests/build.gradle b/engine-tests/build.gradle index eb2787e515f..1dc83eed609 100644 --- a/engine-tests/build.gradle +++ b/engine-tests/build.gradle @@ -108,9 +108,6 @@ task unitTest(type: Test) { excludeTags "MteTest", "TteTest" } systemProperty("junit.jupiter.execution.timeout.default", "1m") - - // Keep in sync with other exclude-lists for Jacoco, e.g., in 'common.gradle' - jacoco.excludes = ["org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess"] } task integrationTest(type: Test) { @@ -125,8 +122,6 @@ task integrationTest(type: Test) { includeTags "MteTest", "TteTest" } systemProperty("junit.jupiter.execution.timeout.default", "5m") - - jacoco.excludes = ["org.terasology.protobuf.*", "*MethodAccess", "*FieldAccess"] } idea { From 8a322cb6fe46900ec66c0153d86192c17cbed48e Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 21 May 2022 13:39:17 -0700 Subject: [PATCH 21/25] build(modules): simplify module template We can do this now that spotbugs and sonarqube moved to terasology-metrics in build-logic --- templates/build.gradle | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/templates/build.gradle b/templates/build.gradle index 3c124902cfb..b05d96aa3fe 100644 --- a/templates/build.gradle +++ b/templates/build.gradle @@ -1,14 +1,9 @@ // Copyright 2020 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -// Since SpotBugs and SonarQube in the legacy style have external dependencies we have to have this block here. -// Alternatively we untangle and update the common.gradle / Kotlin Gradle plugin stuff or just remove these two buildscript { repositories { mavenCentral() google() - maven { - url = uri("https://plugins.gradle.org/m2/") - } maven { // required to provide runtime dependencies to build-logic. name = "Terasology Artifactory" @@ -24,14 +19,6 @@ buildscript { allowInsecureProtocol = true // 😱 } } - - dependencies { - //Spotbugs - classpath("gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.0.0") - - // SonarQube / Cloud scanning - classpath("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8") - } } plugins { From ec8decfad594f768a221af47250d699c64a3ef07 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 21 May 2022 13:41:22 -0700 Subject: [PATCH 22/25] build: make terasology-common for easy inclusion of things applicable to all subprojects --- build-logic/src/main/kotlin/facade.gradle.kts | 3 ++- build-logic/src/main/kotlin/terasology-common.gradle.kts | 7 +++++++ build-logic/src/main/kotlin/terasology-module.gradle.kts | 4 ++-- engine-tests/build.gradle | 2 +- engine/build.gradle | 2 +- facades/PC/build.gradle.kts | 1 - subsystems/DiscordRPC/build.gradle.kts | 5 +++-- subsystems/TypeHandlerLibrary/build.gradle.kts | 3 ++- 8 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 build-logic/src/main/kotlin/terasology-common.gradle.kts diff --git a/build-logic/src/main/kotlin/facade.gradle.kts b/build-logic/src/main/kotlin/facade.gradle.kts index 906f29e200d..3a7e5d17b45 100644 --- a/build-logic/src/main/kotlin/facade.gradle.kts +++ b/build-logic/src/main/kotlin/facade.gradle.kts @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 import org.terasology.gradology.JAR_COLLECTION @@ -6,6 +6,7 @@ import org.terasology.gradology.namedAttribute plugins { application + id("terasology-common") } val dirNatives: String by rootProject.extra diff --git a/build-logic/src/main/kotlin/terasology-common.gradle.kts b/build-logic/src/main/kotlin/terasology-common.gradle.kts new file mode 100644 index 00000000000..295a77a9709 --- /dev/null +++ b/build-logic/src/main/kotlin/terasology-common.gradle.kts @@ -0,0 +1,7 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id("terasology-repositories") + id("terasology-metrics") +} diff --git a/build-logic/src/main/kotlin/terasology-module.gradle.kts b/build-logic/src/main/kotlin/terasology-module.gradle.kts index 39ff890b29d..0fe9adfa049 100644 --- a/build-logic/src/main/kotlin/terasology-module.gradle.kts +++ b/build-logic/src/main/kotlin/terasology-module.gradle.kts @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 // Simple build file for modules - the one under the Core module is the template, will be copied as needed to modules @@ -11,7 +11,7 @@ plugins { `java-library` idea eclipse - id("terasology-metrics") + id("terasology-common") } val moduleMetadata = ModuleMetadataForGradle.forProject(project) diff --git a/engine-tests/build.gradle b/engine-tests/build.gradle index 1dc83eed609..d0205c1a3b8 100644 --- a/engine-tests/build.gradle +++ b/engine-tests/build.gradle @@ -6,7 +6,7 @@ plugins { id "java-library" id "org.jetbrains.gradle.plugin.idea-ext" - id "terasology-metrics" + id "terasology-common" } // Grab all the common stuff like plugins to use, artifact repositories, code analysis config diff --git a/engine/build.gradle b/engine/build.gradle index 58498ec2b1a..d7aaa07acc1 100644 --- a/engine/build.gradle +++ b/engine/build.gradle @@ -7,7 +7,7 @@ plugins { id "java-library" id "org.jetbrains.gradle.plugin.idea-ext" id "com.google.protobuf" - id "terasology-metrics" + id "terasology-common" } // Grab all the common stuff like plugins to use, artifact repositories, code analysis config, etc diff --git a/facades/PC/build.gradle.kts b/facades/PC/build.gradle.kts index 75bac4831c0..741fed95abd 100644 --- a/facades/PC/build.gradle.kts +++ b/facades/PC/build.gradle.kts @@ -15,7 +15,6 @@ import kotlin.test.fail plugins { application id("terasology-dist") - id("terasology-metrics") id("facade") } diff --git a/subsystems/DiscordRPC/build.gradle.kts b/subsystems/DiscordRPC/build.gradle.kts index df6cd5c98cc..d309e0170bf 100644 --- a/subsystems/DiscordRPC/build.gradle.kts +++ b/subsystems/DiscordRPC/build.gradle.kts @@ -1,9 +1,10 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 plugins { java `java-library` + id("terasology-common") } apply(from = "$rootDir/config/gradle/common.gradle") @@ -12,4 +13,4 @@ dependencies { implementation(project(":engine")) api("com.jagrosh:DiscordIPC:0.4") implementation("ch.qos.logback:logback-classic:1.2.3") -} \ No newline at end of file +} diff --git a/subsystems/TypeHandlerLibrary/build.gradle.kts b/subsystems/TypeHandlerLibrary/build.gradle.kts index 4fc9250f370..04d08725f1d 100644 --- a/subsystems/TypeHandlerLibrary/build.gradle.kts +++ b/subsystems/TypeHandlerLibrary/build.gradle.kts @@ -1,9 +1,10 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 plugins { java `java-library` + id("terasology-common") } apply(from = "$rootDir/config/gradle/publish.gradle") From f780a161e124a2f3ae50447012c2fd0369cfa800 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sat, 21 May 2022 13:57:54 -0700 Subject: [PATCH 23/25] build: more cleanup of things moved to build-logic --- build.gradle | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 09fb7111e28..6c4da8f63fc 100644 --- a/build.gradle +++ b/build.gradle @@ -25,9 +25,6 @@ buildscript { } dependencies { - // Protobuf plugin - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.16' - // Our locally included /build-logic classpath("org.terasology.gradology:build-logic") } @@ -46,10 +43,7 @@ plugins { // For the "Build and run using: Intellij IDEA | Gradle" switch id "org.jetbrains.gradle.plugin.idea-ext" version "1.0" - // Things applied by terasology-metrics - id("com.github.spotbugs") version "4.8.0" apply false // TODO: upgrade with gradle 7.x - id("org.sonarqube") version "3.3" apply false - + id("com.google.protobuf") version "0.8.16" apply false id("terasology-repositories") } From b8e09e3a6ab7990c2e32e5aa091ee1cb651f7605 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 22 May 2022 13:54:40 -0700 Subject: [PATCH 24/25] doc: replace IRC link with Discord (#5019) --- .github/CONTRIBUTING.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index da292837ad0..7a4a01ffb0a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,7 +4,8 @@ If you would like to contribute code, documentation, or other assets you can do *When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible.* -Read on for an overview and [check the wiki for more details](https://github.com/MovingBlocks/Terasology/wiki). For questions please join us in our [forum](http://forum.terasology.org/forum/) or on `#terasology` (irc.freenode.net). +Read on for an overview and [check the wiki for more details](https://github.com/MovingBlocks/Terasology/wiki). +For questions please join us in our [forum] or [Discord]. ## File an Issue @@ -12,10 +13,13 @@ You can report bugs and feature requests to [GitHub Issues](https://github.com/M For finding easy to do issues to start with look at the [Good First Issue](https://github.com/MovingBlocks/Terasology/labels/Good%20First%20Issue) issues. -We prefer questions and support requests be posted in the [forum](http://forum.terasology.org/forum). +We prefer questions and support requests be posted in the [forum] or [Discord]. __Please provide as much information as possible to help us solve problems and answer questions better!__ +[forum]: https://forum.terasology.org/forum/ +[Discord]: https://discord.gg/Terasology + ## PR Title / Commit Message Guidelines We try to follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) style for commit messages and pull request titles. From 22bf4a41484310c5e5d0703bd835c4c6e812a9d0 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Sun, 22 May 2022 14:11:37 -0700 Subject: [PATCH 25/25] fix(Context)!: correct signature of Context.get (#5017) --- .../engine/registry/CoreRegistryTest.java | 4 +-- .../terasology/engine/context/Context.java | 31 +++++++++++++++++-- .../engine/context/internal/ContextImpl.java | 8 ++--- .../engine/context/internal/MockContext.java | 4 +-- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java b/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java index dad43023317..39f85c11bbe 100644 --- a/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.registry; @@ -82,7 +82,7 @@ private static class ContextImplementation implements Context { private final Map, Object> map = Maps.newConcurrentMap(); @Override - public T get(Class type) { + public T get(Class type) { T result = type.cast(map.get(type)); if (result != null) { return result; diff --git a/engine/src/main/java/org/terasology/engine/context/Context.java b/engine/src/main/java/org/terasology/engine/context/Context.java index 2384ebd6c9c..74eed81db2d 100644 --- a/engine/src/main/java/org/terasology/engine/context/Context.java +++ b/engine/src/main/java/org/terasology/engine/context/Context.java @@ -1,9 +1,12 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.context; import org.terasology.gestalt.module.sandbox.API; +import java.util.NoSuchElementException; +import java.util.Optional; + /** * Provides classes with the utility objects that belong to the context they are running in. * @@ -21,9 +24,31 @@ public interface Context { /** - * @return the object that is known in this context for this type. + * Get the object that is known in this context for this type. + */ + T get(Class type); + + /** + * Get the object that is known in this context for this type. Never null. + * + * @throws NoSuchElementException No instance was registered with that type. + */ + @SuppressWarnings("unused") + default T getValue(Class type) { + T value = get(type); + if (value == null) { + throw new NoSuchElementException(type.toString()); + } + return value; + } + + /** + * Get the object that is known in this context for this type. */ - T get(Class type); + @SuppressWarnings("unused") + default Optional getMaybe(Class type) { + return Optional.ofNullable(get(type)); + } /** * Makes the object known in this context to be the object to work with for the given type. diff --git a/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java b/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java index 28e94772338..33c13c977d7 100644 --- a/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java +++ b/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.context.internal; @@ -13,7 +13,7 @@ public class ContextImpl implements Context { private final Context parent; - private final Map, Object> map = Maps.newConcurrentMap(); + private final Map, Object> map = Maps.newConcurrentMap(); /** @@ -30,7 +30,7 @@ public ContextImpl() { } @Override - public T get(Class type) { + public T get(Class type) { if (type == Context.class) { return type.cast(this); } @@ -41,7 +41,7 @@ public T get(Class type) { if (parent != null) { return parent.get(type); } - return result; + return null; } @Override diff --git a/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java b/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java index f5b5c7ed614..90e77ab705f 100644 --- a/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java +++ b/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.context.internal; @@ -6,7 +6,7 @@ public class MockContext implements Context { @Override - public T get(Class type) { + public T get(Class type) { return null; }