From 8779326e78bb0afc755dc1ac8b001bd2ed194b4a Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 4 Jun 2026 13:36:44 -0400 Subject: [PATCH] Unit tests --- core/src/mindustry/mod/ContentParser.java | 8 +- core/src/mindustry/mod/DataManager.java | 4 - core/src/mindustry/mod/DataPatcher.java | 26 ++++- core/src/mindustry/mod/data/ContentAsset.java | 9 ++ gradle.properties | 2 +- tests/src/test/java/DataAssetTests.java | 110 ++++++++++++++++++ 6 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 tests/src/test/java/DataAssetTests.java diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index b8192b40ea..ed7357c283 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -589,7 +589,7 @@ public class ContentParser{ block = locate(ContentType.block, name); } }else{ - block = make(resolve(value.getString("type", ""), allowPatching ? Block.class : null), mod + "-" + name); + block = make(resolve(value.getString("type", "Block"), allowPatching ? Block.class : null), mod + "-" + name); } currentContent = block; @@ -1253,7 +1253,7 @@ public class ContentParser{ if(!field.field.isAnnotationPresent(Nullable.class) && field.field.get(object) == null && !implicitNullable.contains(field.field.getType())){ throw new RuntimeException("'" + field.field.getName() + "' in " + ((object.getClass().isAnonymousClass() ? object.getClass().getSuperclass() : object.getClass()).getSimpleName()) + - " is missing! Object: " + object); + " is missing! " + object + "." + field.field.getName() + " cannot be null."); } }catch(Exception e){ throw new RuntimeException(e); @@ -1459,6 +1459,10 @@ public class ContentParser{ } void warn(String string, Object... format){ + warnContext(currentContent, currentFile, string, format); + } + + void warnContext(@Nullable Content currentContent, @Nullable Fi currentFile, String string, Object... format){ Log.warn(string, format); } diff --git a/core/src/mindustry/mod/DataManager.java b/core/src/mindustry/mod/DataManager.java index e6ce81217f..ffc126ec91 100644 --- a/core/src/mindustry/mod/DataManager.java +++ b/core/src/mindustry/mod/DataManager.java @@ -4,10 +4,6 @@ import arc.struct.*; import mindustry.*; import mindustry.mod.data.*; -/** - * TODO: - * - Unit tests - */ public class DataManager{ private DataPatcher patcher = new DataPatcher(); private DataImagePacker packer = new DataImagePacker(); diff --git a/core/src/mindustry/mod/DataPatcher.java b/core/src/mindustry/mod/DataPatcher.java index d58e72be71..1e797e0798 100644 --- a/core/src/mindustry/mod/DataPatcher.java +++ b/core/src/mindustry/mod/DataPatcher.java @@ -59,10 +59,10 @@ public class DataPatcher{ static ContentParser createParser(){ ContentParser cont = new ContentParser(){ @Override - void warn(String string, Object... format){ + void warnContext(@Nullable Content currentContent, @Nullable Fi currentFile, String string, Object... format){ //forward warnings to the current patcher - this is a bit hacky, but I do not want to re-initialize the parser every time if(currentDataPatcher!= null){ - currentDataPatcher.warn(string, format); + currentDataPatcher.warnContext(currentContent, currentFile, string, format); } } }; @@ -183,8 +183,16 @@ public class DataPatcher{ } } + currentlyApplyingContent = null; + parser.finishParsing(); + for(var errored : dpMod.erroredContent){ + if(errored.minfo.error != null && errored.minfo.asset != null){ + errored.minfo.asset.warnings.add(errored.minfo.error); + } + } + Seq all = new Seq<>(); for(var arr : Vars.content.getContentMap()){ all.addAll(arr.select(c -> c.minfo.mod == dpMod)); @@ -750,9 +758,19 @@ public class DataPatcher{ } void warn(String error, Object... fmt){ + warnContext(null, null, error, fmt); + } + + void warnContext(@Nullable Content currentContent, @Nullable Fi currentFile, String error, Object... fmt){ String formatted = Strings.format(error, fmt); - if(currentlyApplyingPatch != null) currentlyApplyingPatch.warnings.add(formatted); - else if(currentlyApplyingContent != null) currentlyApplyingContent.warnings.add(formatted); + + if(currentlyApplyingPatch != null){ + currentlyApplyingPatch.warnings.add(formatted); + }else if(currentlyApplyingContent != null && (currentlyApplyingContent.content == null || currentlyApplyingContent.content.minfo.asset == null)){ + currentlyApplyingContent.warnings.add(formatted); + }else if(currentContent != null && currentContent.minfo.asset != null){ + currentContent.minfo.asset.warnings.add(formatted); + } Log.warn("[ContentPatcher] " + formatted); } diff --git a/core/src/mindustry/mod/data/ContentAsset.java b/core/src/mindustry/mod/data/ContentAsset.java index 953ab5087e..afcc4d4671 100644 --- a/core/src/mindustry/mod/data/ContentAsset.java +++ b/core/src/mindustry/mod/data/ContentAsset.java @@ -22,6 +22,15 @@ public class ContentAsset extends DataAsset{ /** If true, this asset failed to completely, and cannot be used. */ public boolean errored; + public ContentAsset(String path, ContentType type, String data){ + setPath(path); + this.type = type; + this.data = data; + } + + public ContentAsset(){ + } + public void readOverride(String path, Fi file, ContentType type) throws IOException{ this.type = type; setPath(path); diff --git a/gradle.properties b/gradle.properties index 477b1e9cab..acb5f700dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,4 +26,4 @@ org.gradle.caching=true org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 android.enableR8.fullMode=false -archash=3893c9b8f2 +archash=7c7c92f0ba diff --git a/tests/src/test/java/DataAssetTests.java b/tests/src/test/java/DataAssetTests.java new file mode 100644 index 0000000000..be75f6adbc --- /dev/null +++ b/tests/src/test/java/DataAssetTests.java @@ -0,0 +1,110 @@ +import arc.struct.*; +import mindustry.*; +import mindustry.ctype.*; +import mindustry.entities.bullet.*; +import mindustry.game.*; +import mindustry.gen.*; +import mindustry.mod.data.*; +import mindustry.type.*; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class DataAssetTests{ + + @BeforeAll + static void init(){ + ApplicationTests.launchApplication(false); + } + + @AfterEach + void resetAfter(){ + Vars.logic.reset(); + } + + @BeforeEach + void resetBefore(){ + Vars.logic.reset(); + } + + @Test + void basicItem(){ + int totalItems = Vars.content.items().size; + + loadContent(ContentType.item, "testitem", """ + name: 'Test Item' + hardness: 10 + """); + + Item it = find(ContentType.item, "testitem"); + assertNotNull(it); + assertEquals(10f, it.hardness, 0.001f); + assertEquals("Test Item", it.localizedName); + + resetAfter(); + + assertEquals(totalItems, Vars.content.items().size, "Item content must be properly reset"); + assertNull(find(ContentType.item, "testitem"), "Item must be properly removed from map"); + } + + @Test + void basicUnit(){ + + loadContent(ContentType.unit, "testunit", """ + name: 'Test Unit' + type: tank + weapons: [ + { + mirror: true + bullet: { + damage: 10 + type: Laser + length: 1000 + } + } + ] + """); + + UnitType it = find(ContentType.unit, "testunit"); + + assertNotNull(it); + assertTrue(it.create(Team.sharded) instanceof TankUnit); + assertEquals("Test Unit", it.localizedName); + assertEquals(2, it.weapons.size); + assertEquals(LaserBulletType.class, it.weapons.get(0).bullet.getClass()); + assertEquals(1000f, ((LaserBulletType)it.weapons.get(0).bullet).length, 0.001f); + } + + @Test + void noContentAddedWithError(){ + + loadContent(ContentType.block, "badblock", """ + name: 'This will explode' + type: Bad + """); + + assertNull(find(ContentType.block, "badblock"), "Content should not be loaded when an error occurs"); + assertEquals(1, Vars.state.data.getContent().first().warnings.size); + } + + @Test + void noNullFieldsAllowed(){ + + loadContent(ContentType.block, "badblock", """ + name: 'This will explode' + flags: null + """); + + assertNull(find(ContentType.block, "badblock"), "Content should not be loaded when an error occurs"); + assertEquals(1, Vars.state.data.getContent().first().warnings.size); + assertTrue(Vars.state.data.getContent().first().warnings.toString().contains("null"), "Warnings must contain mention of field being null: " + Vars.state.data.getContent().first().warnings); + } + + static T find(ContentType type, String name){ + return (T)Vars.content.getByName(type, "dp-" + name); + } + + static void loadContent(ContentType type, String name, String data){ + Vars.state.data.load(Seq.with(new ContentAsset(name + ".json", type, data))); + } +}