Skip to content

Commit

Permalink
doc: Update docs for MainLoop class.
Browse files Browse the repository at this point in the history
Remove references to JUnit 4 methods.
  • Loading branch information
keturn committed Nov 5, 2021
1 parent 3e89662 commit 2323e70
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 120 deletions.
26 changes: 5 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,16 @@

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).

Here's an example taken from the test suite:

```java
@ExtendWith(MTEExtension.class)
@Dependencies({"engine", "MyModule"})
@Dependencies("MyModule")
@UseWorldGenerator("CoreWorlds:flat")
public class ExampleTest {

Expand All @@ -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());
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,6 +50,10 @@ private ChunkRegionFuture(EntityRef entity, Function<ChunkRegionListener, BlockR
}

/**
* Load an area of the world.
* <p>
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
* simulating remote clients.
* <p>
* Most tests run with a single host and do not need to make direct references to this class.
* <p>
* 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}.
*
* <h2>Client Engine Instances</h2>
* 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
* <p>
* Every class annotated with this will create a single {@link ModuleTestingHelper} and use it during execution of
* Example:
* <pre><code>
* import org.junit.jupiter.api.extension.ExtendWith;
* import org.junit.jupiter.api.Test;
* import org.terasology.engine.registry.In;
*
* &#64;{@link org.junit.jupiter.api.extension.ExtendWith}(MTEExtension.class)
* &#64;{@link Dependencies}("MyModule")
* &#64;{@link UseWorldGenerator}("Pathfinding:pathfinder")
* public class ExampleTest {
*
* &#64;In
* EntityManager entityManager;
*
* &#64;In
* {@link MainLoop} mainLoop;
*
* &#64;Test
* public void testSomething() {
* // …
* }
*
* // Injection is also applied to the parameters of individual tests:
* &#64;Test
* public void testSomething({@link Engines} engines, WorldProvider worldProvider) {
* // …
* }
* }
* </code></pre>
* <p>
* You can configure the environment with these additional annotations:
* <dl>
* <dt>{@link Dependencies @Dependencies}</dt>
* <dd>Specify which modules to include in the environment. Put the name of your module under test here.
* Any dependencies these modules declare in <code>module.txt</code> will be pulled in as well.</dd>
* <dt>{@link UseWorldGenerator @UseWorldGenerator}</dt>
* <dd>The URN of the world generator to use. Defaults to {@link org.terasology.moduletestingenvironment.fixtures.DummyWorldGenerator},
* a flat world.</dd>
* </dl>
*
* <p>
* 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}.
* <p>
* Note that classes marked {@link Nested} will share the engine context with their parent.
* <p>
* This will configure the logger and the current implementation is not subtle or polite about it, see
* {@link #setupLogging()} for notes.
* <p>
* Use this within {@link org.junit.jupiter.api.extension.ExtendWith}
*/
public class MTEExtension implements BeforeAllCallback, ParameterResolver, TestInstancePostProcessor {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,29 @@
/**
* Methods to run the main loop of the game.
* <p>
* Engines can be run while a condition is true via {@link #runWhile(Supplier)} <br>{@code mainLoop.runWhile(()-> true);}
* <p>
* or conversely run until a condition is true via {@link #runUntil(Supplier)} <br>{@code mainLoop.runUntil(()-> false);}
* <p>
* 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}.
* <pre><code>
* // Load everything within 40 blocks of x=123, z=456
* mainLoop.runUntil(makeBlocksRelevant(
* new BlockRegion(123, SURFACE_HEIGHT, 456).expand(40, 40, 40)));
* </code></pre>
* <p>
* {@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 <em>run</em>
* until you use one of these methods.
* <p>
* If there are multiple engines (a host and one or more clients), they will tick in a round-robin fashion.
* <p>
* 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;
Expand All @@ -63,6 +79,12 @@ public void forceAndWaitForGeneration(Vector3ic blockPos) {
}

/**
* Makes sure the area containing these blocks is loaded.
* <p>
* 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
*/
Expand All @@ -71,6 +93,16 @@ public ListenableFuture<ChunkRegionFuture> makeBlocksRelevant(BlockRegionc block
return makeChunksRelevant(desiredChunkRegion, blocks.center(new Vector3f()));
}

/**
* Makes sure the area containing these chunks is loaded.
* <p>
* 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<ChunkRegionFuture> makeChunksRelevant(BlockRegion chunks) {
// Pick a central point (in block coordinates).
Expand All @@ -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> T runUntil(ListenableFuture<T> future) {
boolean timedOut = runUntil(future::isDone);
if (timedOut) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <h2>Introduction</h2>
* 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.
* <p>
* 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()}.
*
* <h2>Client Engine Instances</h2>
* 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.
* <p>
* Engines can be run while a condition is true via {@link #runWhile(Supplier)} <br>{@code runWhile(()-> true);}
* <p>
* or conversely run until a condition is true via {@link #runUntil(Supplier)} <br>{@code runUntil(()-> false);}
*
* <h2>Specifying Dependencies</h2>
* By default the environment will load only the engine itself. FIXME
*
* <h2>Specifying World Generator</h2>
* 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
*
* <h2>Reuse the MTE for Multiple Tests</h2>
* 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.
* <pre>
* private static ModuleTestingHelper context;
*
* &#64;BeforeAll
* public static void setup() throws Exception {
* context = new ModuleTestingHelper() {
* &#64;Override
* public Set&lt;String&gt; getDependencies() {
* return Sets.newHashSet("ModuleTestingHelper");
* }
* };
* context.setup();
* }
*
* &#64;AfterAll
* public static void tearDown() throws Exception {
* context.tearDown();
* }
*
* &#64;Test
* public void someTest() {
* Context hostContext = context.getHostContext();
* EntityManager entityManager = hostContext.get(EntityManager.class);
* // ...
* }
* </pre>
*/
public class ModuleTestingHelper implements ModuleTestingEnvironment {

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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 <code>id</code> in their module.txt.
*/
String[] value();
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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. <code>"CoreWorlds:facetedPerlin"</code>
*/
String value();
}

0 comments on commit 2323e70

Please sign in to comment.