Skip to content

Commit e528a09

Browse files
authored
Merge branch 'develop' into perf/fasterNetworkShutdown
2 parents b98663c + fbc40c3 commit e528a09

File tree

15 files changed

+382
-68
lines changed

15 files changed

+382
-68
lines changed

engine-tests/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ dependencies {
7878
api("org.junit.jupiter:junit-jupiter-api") {
7979
because("we export jupiter Extensions for module tests")
8080
}
81+
api("com.google.truth:truth:1.1.3") {
82+
because("we provide some helper classes")
83+
}
8184
implementation("org.mockito:mockito-inline:3.12.4") {
8285
because("classes like HeadlessEnvironment use mocks")
8386
}

engine-tests/src/main/java/org/terasology/engine/integrationenvironment/Engines.java

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@
99
import org.slf4j.Logger;
1010
import org.slf4j.LoggerFactory;
1111
import org.terasology.engine.config.Config;
12-
import org.terasology.engine.config.SystemConfig;
1312
import org.terasology.engine.context.Context;
1413
import org.terasology.engine.core.GameEngine;
1514
import org.terasology.engine.core.PathManager;
1615
import org.terasology.engine.core.PathManagerProvider;
17-
import org.terasology.engine.core.TerasologyConstants;
1816
import org.terasology.engine.core.TerasologyEngine;
1917
import org.terasology.engine.core.TerasologyEngineBuilder;
2018
import org.terasology.engine.core.modes.GameState;
@@ -40,20 +38,18 @@
4038
import org.terasology.engine.network.NetworkSystem;
4139
import org.terasology.engine.registry.CoreRegistry;
4240
import org.terasology.engine.rendering.opengl.ScreenGrabber;
43-
import org.terasology.engine.rendering.world.viewDistance.ViewDistance;
44-
import org.terasology.engine.testUtil.WithUnittestModule;
45-
import org.terasology.gestalt.module.Module;
46-
import org.terasology.gestalt.module.ModuleMetadataJsonAdapter;
47-
import org.terasology.gestalt.module.ModuleRegistry;
4841

4942
import java.io.IOException;
5043
import java.io.UncheckedIOException;
5144
import java.nio.file.Files;
5245
import java.nio.file.Path;
46+
import java.util.ArrayList;
5347
import java.util.Collections;
5448
import java.util.List;
5549
import java.util.Set;
5650

51+
import static org.junit.platform.commons.support.ReflectionSupport.newInstance;
52+
5753
/**
5854
* Manages game engines for tests.
5955
* <p>
@@ -78,15 +74,18 @@ public class Engines {
7874
protected boolean doneLoading;
7975
protected Context hostContext;
8076
protected final List<TerasologyEngine> engines = Lists.newArrayList();
77+
protected final List<Class<? extends EngineSubsystem>> subsystems = Lists.newArrayList();
8178

8279
PathManager pathManager;
8380
PathManagerProvider.Cleaner pathManagerCleaner;
8481
TerasologyEngine host;
8582
private final NetworkMode networkMode;
8683

87-
public Engines(List<String> dependencies, String worldGeneratorUri, NetworkMode networkMode) {
84+
public Engines(List<String> dependencies, String worldGeneratorUri, NetworkMode networkMode,
85+
List<Class<? extends EngineSubsystem>> subsystems) {
8886
this.networkMode = networkMode;
8987
this.dependencies.addAll(dependencies);
88+
this.subsystems.addAll(subsystems);
9089

9190
if (worldGeneratorUri != null) {
9291
this.worldGeneratorUri = worldGeneratorUri;
@@ -142,7 +141,6 @@ public void tearDown() {
142141
*/
143142
public Context createClient(MainLoop mainLoop) throws IOException {
144143
TerasologyEngine client = createHeadlessEngine();
145-
client.getFromEngineContext(Config.class).getRendering().setViewDistance(ViewDistance.LEGALLY_BLIND);
146144

147145
client.changeState(new StateMainMenu());
148146
if (!connectToHost(client, mainLoop)) {
@@ -182,11 +180,12 @@ public Context getHostContext() {
182180
TerasologyEngine createHeadlessEngine() throws IOException {
183181
TerasologyEngineBuilder terasologyEngineBuilder = new TerasologyEngineBuilder();
184182
terasologyEngineBuilder
185-
.add(new WithUnittestModule())
183+
.add(new IntegrationEnvironmentSubsystem())
186184
.add(new HeadlessGraphics())
187185
.add(new HeadlessTimer())
188186
.add(new HeadlessAudio())
189187
.add(new HeadlessInput());
188+
createExtraSubsystems().forEach(terasologyEngineBuilder::add);
190189

191190
return createEngine(terasologyEngineBuilder);
192191
}
@@ -195,16 +194,31 @@ TerasologyEngine createHeadlessEngine() throws IOException {
195194
TerasologyEngine createHeadedEngine() throws IOException {
196195
EngineSubsystem audio = new LwjglAudio();
197196
TerasologyEngineBuilder terasologyEngineBuilder = new TerasologyEngineBuilder()
198-
.add(new WithUnittestModule())
197+
.add(new IntegrationEnvironmentSubsystem())
199198
.add(audio)
200199
.add(new LwjglGraphics())
201200
.add(new LwjglTimer())
202201
.add(new LwjglInput())
203202
.add(new OpenVRInput());
203+
createExtraSubsystems().forEach(terasologyEngineBuilder::add);
204204

205205
return createEngine(terasologyEngineBuilder);
206206
}
207207

208+
List<EngineSubsystem> createExtraSubsystems() {
209+
List<EngineSubsystem> instances = new ArrayList<>();
210+
for (Class<? extends EngineSubsystem> clazz : subsystems) {
211+
try {
212+
EngineSubsystem subsystem = newInstance(clazz);
213+
instances.add(subsystem);
214+
logger.debug("Created new {}", subsystem);
215+
} catch (RuntimeException e) {
216+
throw new RuntimeException("Failed creating new " + clazz.getName(), e);
217+
}
218+
}
219+
return instances;
220+
}
221+
208222
TerasologyEngine createEngine(TerasologyEngineBuilder terasologyEngineBuilder) throws IOException {
209223
System.setProperty(ModuleManager.LOAD_CLASSPATH_MODULES_PROPERTY, "true");
210224

@@ -220,43 +234,11 @@ TerasologyEngine createEngine(TerasologyEngineBuilder terasologyEngineBuilder) t
220234

221235
TerasologyEngine terasologyEngine = terasologyEngineBuilder.build();
222236
terasologyEngine.initialize();
223-
registerCurrentDirectoryIfModule(terasologyEngine);
224237

225238
engines.add(terasologyEngine);
226239
return terasologyEngine;
227240
}
228241

229-
/**
230-
* 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
231-
* 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
232-
* always fail CI tests due to failing to load themselves.
233-
* <p>
234-
* For these cases we try to load the CWD (via the installPath) as a module and put it in the global module registry.
235-
* <p>
236-
* This process is based on how ModuleManagerImpl uses ModulePathScanner to scan for available modules.
237-
*/
238-
protected void registerCurrentDirectoryIfModule(TerasologyEngine terasologyEngine) {
239-
Path installPath = PathManager.getInstance().getInstallPath();
240-
ModuleManager moduleManager = terasologyEngine.getFromEngineContext(ModuleManager.class);
241-
ModuleRegistry registry = moduleManager.getRegistry();
242-
ModuleMetadataJsonAdapter metadataReader = moduleManager.getModuleMetadataReader();
243-
moduleManager.getModuleFactory().getModuleMetadataLoaderMap()
244-
.put(TerasologyConstants.MODULE_INFO_FILENAME.toString(), metadataReader);
245-
246-
247-
try {
248-
Module module = moduleManager.getModuleFactory().createModule(installPath.toFile());
249-
if (module != null) {
250-
registry.add(module);
251-
logger.info("Added install path as module: {}", installPath);
252-
} else {
253-
logger.info("Install path does not appear to be a module: {}", installPath);
254-
}
255-
} catch (IOException e) {
256-
logger.warn("Could not read install path as module at " + installPath);
257-
}
258-
}
259-
260242
protected void mockPathManager() {
261243
PathManager originalPathManager = PathManager.getInstance();
262244
pathManager = Mockito.spy(originalPathManager);
@@ -265,11 +247,10 @@ protected void mockPathManager() {
265247
PathManagerProvider.setPathManager(pathManager);
266248
}
267249

268-
TerasologyEngine createHost(NetworkMode networkMode) throws IOException {
250+
TerasologyEngine createHost(NetworkMode hostNetworkMode) throws IOException {
269251
TerasologyEngine host = createHeadlessEngine();
270-
host.getFromEngineContext(SystemConfig.class).writeSaveGamesEnabled.set(false);
271252
host.subscribeToStateChange(new HeadlessStateChangeListener(host));
272-
host.changeState(new TestingStateHeadlessSetup(dependencies, worldGeneratorUri, networkMode));
253+
host.changeState(new TestingStateHeadlessSetup(dependencies, worldGeneratorUri, hostNetworkMode));
273254

274255
doneLoading = false;
275256
host.subscribeToStateChange(() -> {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2022 The Terasology Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package org.terasology.engine.integrationenvironment;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.terasology.engine.config.Config;
9+
import org.terasology.engine.config.SystemConfig;
10+
import org.terasology.engine.context.Context;
11+
import org.terasology.engine.core.GameEngine;
12+
import org.terasology.engine.core.PathManager;
13+
import org.terasology.engine.core.TerasologyConstants;
14+
import org.terasology.engine.core.module.ModuleManager;
15+
import org.terasology.engine.core.subsystem.EngineSubsystem;
16+
import org.terasology.engine.integrationenvironment.jupiter.IntegrationEnvironment;
17+
import org.terasology.engine.rendering.world.viewDistance.ViewDistance;
18+
import org.terasology.engine.testUtil.WithUnittestModule;
19+
import org.terasology.gestalt.module.Module;
20+
import org.terasology.gestalt.module.ModuleMetadataJsonAdapter;
21+
import org.terasology.gestalt.module.ModuleRegistry;
22+
23+
import java.io.IOException;
24+
import java.nio.file.Path;
25+
26+
final class IntegrationEnvironmentSubsystem implements EngineSubsystem {
27+
private static final Logger logger = LoggerFactory.getLogger(IntegrationEnvironmentSubsystem.class);
28+
29+
@Override
30+
public String getName() {
31+
return this.getClass().getSimpleName();
32+
}
33+
34+
@Override
35+
public void initialise(GameEngine engine, Context rootContext) {
36+
ModuleManager moduleManager = rootContext.getValue(ModuleManager.class);
37+
WithUnittestModule.registerUnittestModule(moduleManager);
38+
registerCurrentDirectoryIfModule(moduleManager);
39+
configure(rootContext);
40+
}
41+
42+
/**
43+
* Apply test environment default configuration.
44+
* <p>
45+
* You can override this by defining your own EngineSubsystem and passing it to
46+
* {@link IntegrationEnvironment#subsystem()}; it will run after this does.
47+
*/
48+
static void configure(Context context) {
49+
Config config = context.getValue(Config.class);
50+
config.getRendering().setViewDistance(ViewDistance.LEGALLY_BLIND);
51+
52+
SystemConfig sys = context.getValue(SystemConfig.class);
53+
sys.writeSaveGamesEnabled.set(false);
54+
}
55+
56+
/**
57+
* 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
58+
* 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
59+
* always fail CI tests due to failing to load themselves.
60+
* <p>
61+
* For these cases we try to load the CWD (via the installPath) as a module and put it in the global module registry.
62+
* <p>
63+
* This process is based on how ModuleManagerImpl uses ModulePathScanner to scan for available modules.
64+
*/
65+
static void registerCurrentDirectoryIfModule(ModuleManager moduleManager) {
66+
Path installPath = PathManager.getInstance().getInstallPath();
67+
ModuleRegistry registry = moduleManager.getRegistry();
68+
ModuleMetadataJsonAdapter metadataReader = moduleManager.getModuleMetadataReader();
69+
moduleManager.getModuleFactory().getModuleMetadataLoaderMap()
70+
.put(TerasologyConstants.MODULE_INFO_FILENAME.toString(), metadataReader);
71+
72+
try {
73+
Module module = moduleManager.getModuleFactory().createModule(installPath.toFile());
74+
if (module != null) {
75+
registry.add(module);
76+
logger.info("Added install path as module: {}", installPath);
77+
} else {
78+
logger.info("Install path does not appear to be a module: {}", installPath);
79+
}
80+
} catch (IOException e) {
81+
logger.warn("Could not read install path as module at " + installPath);
82+
}
83+
}
84+
}

engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/IntegrationEnvironment.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
package org.terasology.engine.integrationenvironment.jupiter;
55

6+
import org.terasology.engine.core.subsystem.EngineSubsystem;
7+
import org.terasology.engine.logic.players.LocalPlayer;
68
import org.terasology.engine.network.NetworkMode;
79

810
import java.lang.annotation.ElementType;
@@ -13,5 +15,34 @@
1315
@Target(ElementType.TYPE)
1416
@Retention(RetentionPolicy.RUNTIME)
1517
public @interface IntegrationEnvironment {
18+
/**
19+
* The network mode the host engine starts with.
20+
* <p>
21+
* See {@link NetworkMode} for details on the options.
22+
* <p>
23+
* Some modes automatically include a {@link LocalPlayer}.
24+
* <p>
25+
* If you want to simulate multiple players with
26+
* {@link org.terasology.engine.integrationenvironment.Engines#createClient Engines.createClient},
27+
* you will need a mode with a {@linkplain NetworkMode#isServer() server}.
28+
*/
1629
NetworkMode networkMode() default NetworkMode.NONE;
30+
31+
/**
32+
* Add an additional subsystem to the engine.
33+
* <p>
34+
* A new instance will be included in the engine's subsystems when it is created.
35+
* <p>
36+
* Implementing {@link EngineSubsystem#initialise} gives you the opportunity to
37+
* make changes to the configuration <em>before</em> it would otherwise be available.
38+
*/
39+
Class<? extends EngineSubsystem> subsystem() default NO_SUBSYSTEM.class;
40+
41+
/**
42+
* Do not add an extra subsystem to the integration environment.
43+
* <p>
44+
* [Odd marker interface because annotation fields cannot default to null.]
45+
*/
46+
@SuppressWarnings("checkstyle:TypeName")
47+
abstract class NO_SUBSYSTEM implements EngineSubsystem { }
1748
}

engine-tests/src/main/java/org/terasology/engine/integrationenvironment/jupiter/MTEExtension.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.junit.jupiter.api.extension.ParameterResolver;
1414
import org.slf4j.Logger;
1515
import org.slf4j.LoggerFactory;
16+
import org.terasology.engine.core.subsystem.EngineSubsystem;
1617
import org.terasology.engine.integrationenvironment.Engines;
1718
import org.terasology.engine.integrationenvironment.MainLoop;
1819
import org.terasology.engine.integrationenvironment.ModuleTestingHelper;
@@ -66,6 +67,8 @@
6667
* <p>
6768
* You can configure the environment with these additional annotations:
6869
* <dl>
70+
* <dt>{@link IntegrationEnvironment @IntegrationEnvironment}</dt>
71+
* <dd>Configure the network mode and add subsystems.</dd>
6972
* <dt>{@link Dependencies @Dependencies}</dt>
7073
* <dd>Specify which modules to include in the environment. Put the name of your module under test here.
7174
* Any dependencies these modules declare in <code>module.txt</code> will be pulled in as well.</dd>
@@ -174,6 +177,12 @@ public NetworkMode getNetworkMode(ExtensionContext context) {
174177
return getAnnotationWithDefault(context, IntegrationEnvironment::networkMode);
175178
}
176179

180+
public List<Class<? extends EngineSubsystem>> getSubsystems(ExtensionContext context) {
181+
var subsystem = getAnnotationWithDefault(context, IntegrationEnvironment::subsystem);
182+
return subsystem.equals(IntegrationEnvironment.NO_SUBSYSTEM.class)
183+
? Collections.emptyList() : List.of(subsystem);
184+
}
185+
177186
private <T> T getAnnotationWithDefault(ExtensionContext context, Function<IntegrationEnvironment, T> method) {
178187
var ann =
179188
findAnnotation(context.getRequiredTestClass(), IntegrationEnvironment.class)
@@ -200,7 +209,8 @@ protected Engines getEngines(ExtensionContext context) {
200209
EnginesCleaner.class, k -> new EnginesCleaner(
201210
getDependencyNames(context),
202211
getWorldGeneratorUri(context),
203-
getNetworkMode(context)
212+
getNetworkMode(context),
213+
getSubsystems(context)
204214
),
205215
EnginesCleaner.class);
206216
return autoCleaner.engines;
@@ -225,8 +235,9 @@ protected ExtensionContext.Namespace namespaceFor(ExtensionContext context) {
225235
static class EnginesCleaner implements ExtensionContext.Store.CloseableResource {
226236
protected Engines engines;
227237

228-
EnginesCleaner(List<String> dependencyNames, String worldGeneratorUri, NetworkMode networkMode) {
229-
engines = new Engines(dependencyNames, worldGeneratorUri, networkMode);
238+
EnginesCleaner(List<String> dependencyNames, String worldGeneratorUri, NetworkMode networkMode,
239+
List<Class<? extends EngineSubsystem>> subsystems) {
240+
engines = new Engines(dependencyNames, worldGeneratorUri, networkMode, subsystems);
230241
engines.setup();
231242
}
232243

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2022 The Terasology Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package org.terasology.engine.testUtil;
5+
6+
import com.google.common.truth.Correspondence;
7+
8+
public final class Correspondences {
9+
private Correspondences() {
10+
}
11+
12+
public static <A, E extends Class<?>> Correspondence<A, E> instanceOfExpected() {
13+
// This is literally the example in the documentation of Correspondence.from.
14+
// They could have included the implementation in the library for us to use!
15+
return Correspondence.from((A a, E e) -> e.isInstance(a), "is an instance of");
16+
}
17+
}

engine-tests/src/main/java/org/terasology/engine/testUtil/WithUnittestModule.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2021 The Terasology Foundation
1+
// Copyright 2022 The Terasology Foundation
22
// SPDX-License-Identifier: Apache-2.0
33

44
package org.terasology.engine.testUtil;
@@ -30,6 +30,10 @@ public String getName() {
3030
public void initialise(GameEngine engine, Context rootContext) {
3131
EngineSubsystem.super.initialise(engine, rootContext);
3232
ModuleManager manager = rootContext.get(ModuleManager.class);
33+
registerUnittestModule(manager);
34+
}
35+
36+
public static void registerUnittestModule(ModuleManager manager) {
3337
Module unittestModule = manager.registerPackageModule("org.terasology.unittest");
3438
manager.resolveAndLoadEnvironment(unittestModule.getId());
3539
}

0 commit comments

Comments
 (0)