diff --git a/README.md b/README.md index b77562c..1512d5f 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,8 @@ A test helper to instantiate a full headless TerasologyEngine instance in JUnit tests. -## ⚠️ Issues - -On using MTE tests, you might face the following error: -``` -java.lang.IllegalStateException: Modules only available if resolution was successful -``` -In the logs, you should see something along the following lines: -``` -08:11:52.856 [Test worker] ERROR org.terasology.moduletestingenvironment.TestingStateHeadlessSetup - Unable to resolve modules: [engine, DynamicCities, ModuleTestingEnvironment] -``` - -We do not know, yet, why this happens, but hope that with the migration to gestalt v7, the situation will improve or at least clarify a bit. -Until the migration is complete, we suggest disabling the test using the `@Disable` annotation. - ## Usage -For complete JavaDoc please see the [documentation on Github Pages](https://terasology.github.io/ModuleTestingEnvironment/). - For more examples see [the test suite](https://github.com/terasology/ModuleTestingEnvironment/tree/master/src/test/java/org/terasology/moduletestingenvironment). @@ -27,7 +11,7 @@ Here's an example taken from the test suite: ```java @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "MyModule"}) +@Dependencies("MyModule") @UseWorldGenerator("CoreWorlds:flat") public class ExampleTest { @@ -48,21 +32,21 @@ public class ExampleTest { // wait for both clients to be known to the server helper.runUntil(()-> Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).size() == 2); - Assertions.assertEquals(2, Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).size()); + assertEquals(2, Lists.newArrayList(entityManager.getEntitiesWith(ClientComponent.class)).size()); // run while a condition is true or until a timeout passes boolean timedOut = helper.runWhile(1000, ()-> true); - Assertions.assertTrue(timedOut); + assertTrue(timedOut); // send an event to a client's local player just for fun clientContext1.get(LocalPlayer.class).getClientEntity().send(new ResetCameraEvent()); // wait for a chunk to be generated - helper.forceAndWaitForGeneration(Vector3i.zero()); + helper.forceAndWaitForGeneration(new Vector3i()); // set a block's type and immediately read it back worldProvider.setBlock(Vector3i.zero(), blockManager.getBlock("engine:air")); - Assertions.assertEquals("engine:air", worldProvider.getBlock(Vector3f.zero()).getURI().toString()); + assertEquals("engine:air", worldProvider.getBlock(Vector3f.zero()).getURI().toString()); } } ``` diff --git a/src/main/java/org/terasology/moduletestingenvironment/ChunkRegionFuture.java b/src/main/java/org/terasology/moduletestingenvironment/ChunkRegionFuture.java index 39e01a4..d34ea9b 100644 --- a/src/main/java/org/terasology/moduletestingenvironment/ChunkRegionFuture.java +++ b/src/main/java/org/terasology/moduletestingenvironment/ChunkRegionFuture.java @@ -26,6 +26,12 @@ 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; @@ -44,6 +50,10 @@ private ChunkRegionFuture(EntityRef entity, Function + * 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 diff --git a/src/main/java/org/terasology/moduletestingenvironment/Engines.java b/src/main/java/org/terasology/moduletestingenvironment/Engines.java index d1dc2b8..866b451 100644 --- a/src/main/java/org/terasology/moduletestingenvironment/Engines.java +++ b/src/main/java/org/terasology/moduletestingenvironment/Engines.java @@ -58,6 +58,14 @@ * 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); diff --git a/src/main/java/org/terasology/moduletestingenvironment/MTEExtension.java b/src/main/java/org/terasology/moduletestingenvironment/MTEExtension.java index 73806e7..f50341c 100644 --- a/src/main/java/org/terasology/moduletestingenvironment/MTEExtension.java +++ b/src/main/java/org/terasology/moduletestingenvironment/MTEExtension.java @@ -31,21 +31,60 @@ import java.util.function.Function; /** - * Junit 5 Extension for using {@link ModuleTestingHelper} in your test. + * 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, constructor's or - * test's parameters. Also you can inject {@link ModuleTestingHelper} itself. + * 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. *

- * Every class annotated with this will create a single {@link ModuleTestingHelper} and use it during execution of + * 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 org.terasology.moduletestingenvironment.fixtures.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} + * 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. - *

- * Use this within {@link org.junit.jupiter.api.extension.ExtendWith} */ public class MTEExtension implements BeforeAllCallback, ParameterResolver, TestInstancePostProcessor { diff --git a/src/main/java/org/terasology/moduletestingenvironment/MainLoop.java b/src/main/java/org/terasology/moduletestingenvironment/MainLoop.java index e1a4c8f..65f7e51 100644 --- a/src/main/java/org/terasology/moduletestingenvironment/MainLoop.java +++ b/src/main/java/org/terasology/moduletestingenvironment/MainLoop.java @@ -31,13 +31,29 @@ /** * 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; @@ -63,6 +79,12 @@ public void forceAndWaitForGeneration(Vector3ic blockPos) { } /** + * 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 */ @@ -71,6 +93,16 @@ public ListenableFuture makeBlocksRelevant(BlockRegionc block 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). @@ -96,6 +128,11 @@ BlockRegionc chunkRegionToNewBlockRegion(BlockRegionc 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) { diff --git a/src/main/java/org/terasology/moduletestingenvironment/ModuleTestingHelper.java b/src/main/java/org/terasology/moduletestingenvironment/ModuleTestingHelper.java index 0206377..28b2883 100644 --- a/src/main/java/org/terasology/moduletestingenvironment/ModuleTestingHelper.java +++ b/src/main/java/org/terasology/moduletestingenvironment/ModuleTestingHelper.java @@ -16,65 +16,15 @@ import java.util.function.Supplier; /** - * Base class for tests involving full {@link TerasologyEngine} instances. View the tests included in this module for - * simple usage examples. - * - *

Introduction

- * If test classes extend this class will create a new host engine for each {@code @Test} method. If the testing - * environment is used by composition {@link #setup()} and {@link #tearDown()} need to be called explicitly. This can be - * done once for the test class or for each test. + * Methods for interacting with the engine in the test environment. *

- * The in-game {@link Context} for this engine can be accessed via {@link #getHostContext()}. The result of this getter - * is equivalent to the CoreRegistry available to module code at runtime. However, it is very important that you do not - * use CoreRegistry in your test code, as this is manipulated by the test environment to allow multiple instances of the - * engine to peacefully coexist. You should always use the returned context reference to manipulate or inspect the - * CoreRegistry of a given engine instance. + * 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. - *

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

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

Specifying Dependencies

- * By default the environment will load only the engine itself. FIXME - * - *

Specifying World Generator

- * By default the environment will use a dummy world generator which creates nothing but air. To specify a more useful - * world generator you must FIXME - * - *

Reuse the MTE for Multiple Tests

- * To use the same engine for multiple tests the testing environment can be set up explicitly and shared between tests. - * To configure module dependencies or the world generator an anonymous class may be used. - *
- * private static ModuleTestingHelper context;
- *
- * @BeforeAll
- * public static void setup() throws Exception {
- *     context = new ModuleTestingHelper() {
- *     @Override
- *     public Set<String> getDependencies() {
- *         return Sets.newHashSet("ModuleTestingHelper");
- *     }
- *     };
- *     context.setup();
- * }
- *
- * @AfterAll
- * public static void tearDown() throws Exception {
- *     context.tearDown();
- * }
- *
- * @Test
- * public void someTest() {
- *     Context hostContext = context.getHostContext();
- *     EntityManager entityManager = hostContext.get(EntityManager.class);
- *     // ...
- * }
- * 
*/ public class ModuleTestingHelper implements ModuleTestingEnvironment { diff --git a/src/main/java/org/terasology/moduletestingenvironment/extension/Dependencies.java b/src/main/java/org/terasology/moduletestingenvironment/extension/Dependencies.java index 56bac9b..ce94d83 100644 --- a/src/main/java/org/terasology/moduletestingenvironment/extension/Dependencies.java +++ b/src/main/java/org/terasology/moduletestingenvironment/extension/Dependencies.java @@ -1,18 +1,5 @@ -/* - * Copyright 2020 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.moduletestingenvironment.extension; @@ -21,13 +8,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Declares the modules to load in the environment. + * + * @see org.terasology.moduletestingenvironment.MTEExtension + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Dependencies { /** - * Override this to change which modules must be loaded for the environment - * - * @return The set of module names to load + * Names of modules, as defined by the id in their module.txt. */ String[] value(); } diff --git a/src/main/java/org/terasology/moduletestingenvironment/extension/UseWorldGenerator.java b/src/main/java/org/terasology/moduletestingenvironment/extension/UseWorldGenerator.java index 8bcc1da..69b124f 100644 --- a/src/main/java/org/terasology/moduletestingenvironment/extension/UseWorldGenerator.java +++ b/src/main/java/org/terasology/moduletestingenvironment/extension/UseWorldGenerator.java @@ -1,18 +1,5 @@ -/* - * Copyright 2020 MovingBlocks - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 package org.terasology.moduletestingenvironment.extension; @@ -21,15 +8,17 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Declares which {@index "world generator"} to use. + * + * @see org.terasology.moduletestingenvironment.MTEExtension + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface UseWorldGenerator { /** - * Override this to change which world generator to use. Defaults to a dummy generator that leaves all blocks as - * air - * - * @return the uri of the desired world generator + * The URN of the world generator, e.g. "CoreWorlds:facetedPerlin" */ String value(); } diff --git a/src/main/java/org/terasology/moduletestingenvironment/package-info.java b/src/main/java/org/terasology/moduletestingenvironment/package-info.java new file mode 100644 index 0000000..67c0a04 --- /dev/null +++ b/src/main/java/org/terasology/moduletestingenvironment/package-info.java @@ -0,0 +1,16 @@ +// Copyright 2021 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: + *

+ */ +package org.terasology.moduletestingenvironment; + diff --git a/src/test/java/org/terasology/moduletestingenvironment/ExampleTest.java b/src/test/java/org/terasology/moduletestingenvironment/ExampleTest.java index c0e1906..85b59b0 100644 --- a/src/test/java/org/terasology/moduletestingenvironment/ExampleTest.java +++ b/src/test/java/org/terasology/moduletestingenvironment/ExampleTest.java @@ -13,17 +13,17 @@ import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.logic.players.LocalPlayer; import org.terasology.engine.logic.players.event.ResetCameraEvent; -import org.terasology.moduletestingenvironment.extension.Dependencies; 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 org.terasology.moduletestingenvironment.extension.Dependencies; import java.io.IOException; @Tag("MteTest") @ExtendWith(MTEExtension.class) -@Dependencies({"engine", "ModuleTestingEnvironment"}) +@Dependencies("ModuleTestingEnvironment") public class ExampleTest { @In