diff --git a/.gitignore b/.gitignore
index c1af677b996..4825b0aa7be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ extensions
/facades/*
!/facades/PC
!/facades/TeraEd
+!/facades/WorkspaceValidation
!/facades/subprojects.gradle
/modules/*
!/modules/subprojects.gradle
diff --git a/facades/WorkspaceValidation/.gitignore b/facades/WorkspaceValidation/.gitignore
new file mode 100644
index 00000000000..7263b6ee479
--- /dev/null
+++ b/facades/WorkspaceValidation/.gitignore
@@ -0,0 +1,11 @@
+# Boo eclipse! - every eclipse project needs its own .gitignore file
+
+# Ignore Eclipse
+/.checkstyle
+/.project
+/.classpath
+/.settings/
+/bin/
+
+# Ignore gradle
+/build/
diff --git a/facades/WorkspaceValidation/build.gradle b/facades/WorkspaceValidation/build.gradle
new file mode 100644
index 00000000000..989aa4af735
--- /dev/null
+++ b/facades/WorkspaceValidation/build.gradle
@@ -0,0 +1,70 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+// The Editor facade is responsible for the (shader) editor - a plain Java application runnable on PCs
+
+// Grab all the common stuff like plugins to use, artifact repositories, code analysis config
+apply from: "$rootDir/config/gradle/publish.gradle"
+
+// Base the engine tests on the same version number as the engine
+version = project(':engine').version
+println "TeraEd VERSION: $version"
+
+// Jenkins-Artifactory integration catches on to this as part of the Maven-type descriptor
+group = 'org.terasology.facades'
+
+sourceSets {
+ // Adjust output path (changed with the Gradle 6 upgrade, this puts it back)
+ main.java.outputDir = new File("$buildDir/classes")
+ test.java.outputDir = new File("$buildDir/testClasses")
+}
+
+dependencies {
+ implementation project(':engine')
+ implementation "org.terasology:reflections:0.9.12-MB"
+
+ implementation(platform(project(":modules")))
+
+ implementation(project(":engine-tests"))
+
+ implementation("org.terasology.modules:ModuleTestingEnvironment:0.3.3-SNAPSHOT")
+
+ implementation(group: 'com.google.guava', name: 'guava', version: '30.1-jre')
+
+ implementation(project(":subsystems:DiscordRPC"))
+ implementation(project(":subsystems:TypeHandlerLibrary"))
+
+ implementation(group: 'org.lwjglx', name: 'lwjgl3-awt', version: '0.1.7') {
+ exclude group: 'org.lwjgl', module: ''
+ }
+
+ // Test lib dependencies
+ implementation(platform("org.junit:junit-bom:5.8.1")) {
+ // junit-bom will set version numbers for the other org.junit dependencies.
+ }
+ implementation("org.junit.jupiter:junit-jupiter-api")
+ implementation("org.junit.jupiter:junit-jupiter-params")
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
+ testImplementation('org.mockito:mockito-inline:3.12.4')
+}
+
+test {
+// dependsOn copyResourcesToClasses
+ dependsOn rootProject.extractNatives
+
+ description("Runs all tests (slow)")
+
+ systemProperty("junit.jupiter.execution.timeout.default", "4m")
+}
+
+
+
+// Prep an IntelliJ module for the facade
+idea {
+ module {
+ // Change around the output a bit
+ inheritOutputDirs = false
+ outputDir = file('build/classes')
+ testOutputDir = file('build/testClasses')
+ }
+}
diff --git a/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/AssetTesting.java b/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/AssetTesting.java
new file mode 100644
index 00000000000..199b6457441
--- /dev/null
+++ b/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/AssetTesting.java
@@ -0,0 +1,166 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.workspace.validation;
+
+import com.google.common.io.ByteStreams;
+import org.junit.jupiter.api.DynamicContainer;
+import org.junit.jupiter.api.DynamicNode;
+import org.junit.jupiter.api.DynamicTest;
+import org.terasology.engine.core.TerasologyConstants;
+import org.terasology.engine.core.module.ModuleManager;
+import org.terasology.gestalt.assets.Asset;
+import org.terasology.gestalt.assets.AssetData;
+import org.terasology.gestalt.assets.ResourceUrn;
+import org.terasology.gestalt.assets.management.AssetManager;
+import org.terasology.gestalt.entitysystem.component.Component;
+import org.terasology.gestalt.module.Module;
+import org.terasology.gestalt.module.resources.ModuleFileSource;
+import org.terasology.gestalt.naming.Name;
+import org.terasology.moduletestingenvironment.Engines;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Set of functions for testing assets.
+ */
+public class AssetTesting {
+ /**
+ * Template for assets tests.
+ *
+ * Provide engine setup, engine tearup and {@code testsCretor}'s content as tests.
+ *
+ * @param moduleFilter - filter. which modules should be runs with {@code testsCreator}'s tests.
+ * @param testsCreator - function which provides asset tests.
+ * @return Stream of DynamicNodes. requred for junit-jupiter's dynamic tests.
+ */
+ public Stream template(
+ BiPredicate moduleFilter,
+ Function,
+ Stream> testsCreator) {
+ ModuleManager moduleManager = createModuleManager();
+ return moduleManager.getRegistry()
+ .getModuleIds()
+ .stream()
+ // use filter over modules
+ .filter(name -> moduleFilter.test(moduleManager, name))
+ // create tests
+ .map(moduleName -> {
+ // Create engine
+ EnginesAccessor engine = new EnginesAccessor(Set.of(moduleName.toString()), null);
+ // Provide `Lazy` reference for asset tests
+ AtomicReference assetManagerRef = new AtomicReference<>();
+ return DynamicContainer.dynamicContainer(String.format("Module - %s", moduleName),
+ Stream.of(
+ // engine test
+ DynamicTest.dynamicTest("setup", () -> {
+ engine.setup();
+ assetManagerRef.set(engine.getHostContext().get(AssetManager.class));
+ }),
+ // Container with asset tests
+ DynamicContainer.dynamicContainer("Assets tests", testsCreator.apply(assetManagerRef)),
+ // engine teardown test
+ DynamicTest.dynamicTest("tearDown", engine::tearDown)));
+ });
+ }
+
+ public ModuleManager createModuleManager() {
+ System.setProperty(ModuleManager.LOAD_CLASSPATH_MODULES_PROPERTY, "true");
+ return new ModuleManager("");
+ }
+
+ public BiPredicate haveAsset(String assetName) {
+ return (moduleManager, name) -> {
+ Module module = moduleManager.getRegistry().getLatestModuleVersion(name);
+ ModuleFileSource resources = module.getResources();
+
+ boolean haveAsset = !resources.getFilesInPath(true, "assets/" + assetName).isEmpty();
+
+ boolean haveAssetOverride =
+ resources.getFilesInPath(false, "overrides")
+ .stream()
+ .anyMatch(fr -> fr.getPath().contains(assetName));
+ boolean haveAssetDelta =
+ resources.getFilesInPath(false, "deltas")
+ .stream()
+ .anyMatch(fr -> fr.getPath().contains(assetName));
+
+ return haveAsset || haveAssetOverride || haveAssetDelta;
+ };
+ }
+
+ public > BiPredicate havePrefab(Class componentClass) {
+ return (moduleManager, name) -> {
+ Module module = moduleManager.getRegistry().getLatestModuleVersion(name);
+ ModuleFileSource resources = module.getResources();
+
+ return resources.getFiles().stream()
+ .filter(fr -> fr.toString().contains("prefabs") && fr.toString().contains(".prefab"))
+ .anyMatch(fr -> {
+ try (InputStream inputStream = fr.open()) {
+ byte[] bytes = ByteStreams.toByteArray(inputStream);
+ String content = new String(bytes, TerasologyConstants.CHARSET);
+ String componentName = componentClass.getSimpleName();
+ return content.contains(componentName);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ });
+ };
+ }
+
+ public > DynamicContainer asset(AtomicReference assetManager,
+ Class assetClazz,
+ Consumer validator) {
+ return asset(assetManager, assetClazz, (a) -> true, validator);
+
+ }
+
+ public > DynamicContainer asset(AtomicReference assetManager,
+ Class assetClazz,
+ Predicate filter,
+ Consumer validator) {
+
+ return DynamicContainer.dynamicContainer(String.format("AssetType: %s", assetClazz.getSimpleName()),
+ DynamicTest.stream(
+ // Special generator for 1 value for `lazy` getting.
+ Stream.generate(() -> assetManager.get().getAvailableAssets(assetClazz).stream())
+ .limit(1)
+ .flatMap(s -> s)
+ .filter(urn -> filter.test(assetManager.get().getAsset(urn, assetClazz).get())),
+ ResourceUrn::toString,
+ urn -> {
+ A asset = assetManager.get().getAsset(urn, assetClazz).get();
+ validator.accept(asset);
+ }
+ ));
+
+ }
+
+ // Just grant access to protected methods.
+ public static class EnginesAccessor extends Engines {
+
+ public EnginesAccessor(Set dependencies, String worldGeneratorUri) {
+ super(dependencies, worldGeneratorUri);
+ }
+
+ @Override
+ protected void setup() {
+ super.setup();
+ }
+
+ @Override
+ protected void tearDown() {
+ super.tearDown();
+ }
+ }
+}
diff --git a/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/WorkspaceBehaviorsTests.java b/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/WorkspaceBehaviorsTests.java
new file mode 100644
index 00000000000..40c8fb750cf
--- /dev/null
+++ b/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/WorkspaceBehaviorsTests.java
@@ -0,0 +1,39 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.workspace.validation;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DynamicNode;
+import org.junit.jupiter.api.TestFactory;
+import org.terasology.engine.entitySystem.prefab.Prefab;
+import org.terasology.engine.logic.behavior.BehaviorComponent;
+import org.terasology.engine.logic.behavior.asset.BehaviorTree;
+
+import java.util.stream.Stream;
+
+public class WorkspaceBehaviorsTests extends AssetTesting {
+
+ @TestFactory
+ Stream behaviours() {
+ return template(
+ // Filter modules
+ haveAsset("behaviors").or(havePrefab(BehaviorComponent.class)),
+ // Create assetTests
+ assetManagerRef -> Stream.of(
+ asset(
+ assetManagerRef,
+ BehaviorTree.class,
+ (b) -> Assertions.assertNotNull(b.getData())
+ ),
+ asset(
+ assetManagerRef,
+ Prefab.class,
+ prefab -> prefab.hasComponent(BehaviorComponent.class),
+ prefab -> Assertions.assertNotNull(prefab.getComponent(BehaviorComponent.class).tree)
+
+ ))
+ );
+ }
+
+}
diff --git a/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/WorkspaceModulesResolvingTests.java b/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/WorkspaceModulesResolvingTests.java
new file mode 100644
index 00000000000..0674dee51af
--- /dev/null
+++ b/facades/WorkspaceValidation/src/test/java/org/terasology/workspace/validation/WorkspaceModulesResolvingTests.java
@@ -0,0 +1,56 @@
+// Copyright 2021 The Terasology Foundation
+// SPDX-License-Identifier: Apache-2.0
+
+package org.terasology.workspace.validation;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.terasology.engine.core.module.ModuleManager;
+import org.terasology.gestalt.naming.Name;
+
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * Example test for check modules dependency validations.
+ */
+public class WorkspaceModulesResolvingTests {
+
+ public static Stream modulesAndModuleManager() {
+ System.setProperty(ModuleManager.LOAD_CLASSPATH_MODULES_PROPERTY, "true");
+ ModuleManager temporary = new ModuleManager("");
+ return temporary.getRegistry()
+ .getModuleIds()
+ .stream()
+ .map((name) -> Arguments.of(temporary, name));
+ }
+
+ public static Stream modulePairsAndModuleManager() {
+ System.setProperty(ModuleManager.LOAD_CLASSPATH_MODULES_PROPERTY, "true");
+ ModuleManager temporary = new ModuleManager("");
+ Set moduleIds = temporary.getRegistry()
+ .getModuleIds();
+ return moduleIds
+ .stream()
+ .flatMap((name) -> moduleIds.stream().map((name2) -> Arguments.of(temporary, name, name2)));
+ }
+
+ @DisplayName("Try to resolve and load module")
+ @ParameterizedTest(name = "{displayName} - {1}")
+ @MethodSource("modulesAndModuleManager")
+ void resolveAndLoadModule(ModuleManager moduleManager, Name moduleName) {
+ moduleManager.resolveAndLoadEnvironment(moduleName);
+ Assertions.assertNotNull(moduleManager.getEnvironment());
+ }
+
+ @DisplayName("Try to resolve and load pair modules")
+ @ParameterizedTest(name = "{displayName} - [{1}, {2}]")
+ @MethodSource("modulePairsAndModuleManager")
+ void resolveAndLoadPairModules(ModuleManager moduleManager, Name moduleName1, Name moduleName2) {
+ moduleManager.resolveAndLoadEnvironment(moduleName1, moduleName2);
+ Assertions.assertNotNull(moduleManager.getEnvironment());
+ }
+}