Skip to content

Commit 39ee554

Browse files
committed
Audit Forge 26.1.2 runtime logs and fluid writes
1 parent ac30b0a commit 39ee554

8 files changed

Lines changed: 425 additions & 19 deletions

File tree

build.gradle

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,168 @@ tasks.named('javadoc', Javadoc).configure {
229229

230230
tasks.named('test', Test).configure {
231231
useJUnitPlatform()
232+
// Unit tests inspect target files relative to the checkout, but they do
233+
// not need Forge's rolling runtime files. A console-only test logger keeps
234+
// them from contending with Eclipse/client logs in this working directory.
235+
systemProperty 'log4j.configurationFile', file('src/test/resources/log4j2-test.xml').absolutePath
236+
// Loaded only through an isolated URLClassLoader by the parity test. This
237+
// is deliberately not a Gradle dependency and cannot leak into Eclipse or
238+
// a published OreSpawn jar.
239+
File mineralogy5Oracle = file('../../MinecraftMineralogy 118/MinecraftMineralogy/build/libs/Mineralogy-1.18.2-5.4.0.jar')
240+
if (mineralogy5Oracle.isFile()) {
241+
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
242+
}
243+
}
244+
245+
// Several registry-focused tests initialize the real global config singleton.
246+
// Keep that target-native coverage without creating or changing a developer's
247+
// checkout config as a side effect of `test` or `build`.
248+
def unitTestWorldgenConfig = file('config/orespawn-worldgen.json')
249+
def unitTestWorldgenConfigWasPresent = false
250+
byte[] unitTestWorldgenConfigBytes = null
251+
tasks.named('test', Test).configure {
252+
doFirst {
253+
unitTestWorldgenConfigWasPresent = unitTestWorldgenConfig.isFile()
254+
unitTestWorldgenConfigBytes = unitTestWorldgenConfigWasPresent
255+
? unitTestWorldgenConfig.bytes : null
256+
}
257+
}
258+
def preserveDeveloperWorldgenConfig = tasks.register('preserveDeveloperWorldgenConfig') {
259+
doLast {
260+
if (unitTestWorldgenConfigWasPresent) {
261+
byte[] after = unitTestWorldgenConfig.isFile() ? unitTestWorldgenConfig.bytes : null
262+
if (after == null || !java.util.Arrays.equals(unitTestWorldgenConfigBytes, after)) {
263+
unitTestWorldgenConfig.parentFile.mkdirs()
264+
unitTestWorldgenConfig.bytes = unitTestWorldgenConfigBytes
265+
throw new GradleException('Unit tests changed config/orespawn-worldgen.json; the original was restored')
266+
}
267+
} else if (unitTestWorldgenConfig.isFile()) {
268+
delete unitTestWorldgenConfig
269+
}
270+
}
271+
}
272+
tasks.named('test') {
273+
finalizedBy preserveDeveloperWorldgenConfig
274+
}
275+
276+
// A Forge process is not green merely because it returns exit code zero. The
277+
// loader can log a worldgen/linkage failure and still shut down normally.
278+
def acceptedForge40LogNoise = [
279+
~/FML appears to be missing any signature data/,
280+
~/Found multiple arguments for option fml\.mcVersion/,
281+
~/Found multiple arguments for option fml\.forgeVersion/,
282+
~/\/FATAL\] \[net\.minecraftforge\.common\.ForgeConfig\/CORE\]: Forge config just got changed on the file system!$/,
283+
~/\/FATAL\] \[net\.minecraftforge\.fml\.packs\.ModFileResourcePack\/\]: Failed to clean up tempdir /
284+
]
285+
286+
def runtimeCrashSnapshot = { File runDirectory ->
287+
File crashDirectory = new File(runDirectory, 'crash-reports')
288+
if (!crashDirectory.isDirectory()) return [] as Set
289+
return fileTree(crashDirectory) { include '**/*' }.files
290+
.findAll { it.isFile() }.collect { it.absolutePath } as Set
291+
}
292+
293+
def assertRuntimeLogsClean = { File runDirectory, String context, Set priorCrashes ->
294+
File crashDirectory = new File(runDirectory, 'crash-reports')
295+
if (crashDirectory.isDirectory()) {
296+
def crashes = fileTree(crashDirectory) { include '**/*' }.files
297+
.findAll { it.isFile() && !priorCrashes.contains(it.absolutePath) }
298+
if (!crashes.isEmpty()) {
299+
throw new GradleException("${context} produced crash report ${crashes.first()}")
300+
}
301+
}
302+
303+
File logsDirectory = new File(runDirectory, 'logs')
304+
if (!logsDirectory.isDirectory()) return
305+
def failures = []
306+
[new File(logsDirectory, 'latest.log'), new File(logsDirectory, 'debug.log')]
307+
.findAll { it.isFile() }.each { File log ->
308+
int lineNumber = 0
309+
log.eachLine('UTF-8') { String line ->
310+
lineNumber++
311+
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
312+
boolean knownNoise = acceptedForge40LogNoise.any { line =~ it }
313+
boolean fatalText = line.contains('Encountered an unexpected exception') ||
314+
line.contains('Exception stopping the server') ||
315+
line.contains('Migration audit failed') ||
316+
line.contains('java.lang.Error:') ||
317+
line.contains('NoSuchMethodError') ||
318+
line.contains('NoClassDefFoundError') ||
319+
line.contains('ExceptionInInitializerError') ||
320+
line.contains('Tried to assign a mutable BlockPos') ||
321+
line.contains('causing cascading worldgen lag')
322+
if ((unexpectedSeverity && !knownNoise) || fatalText) {
323+
failures.add("${log.name}:${lineNumber}: ${line}")
324+
}
325+
}
326+
}
327+
if (!failures.isEmpty()) {
328+
throw new GradleException("${context} logged unexpected errors:\n"
329+
+ failures.take(20).join('\n'))
330+
}
331+
}
332+
333+
task runtimeLogScannerTest {
334+
group = 'verification'
335+
description = 'Proves runtime log validation accepts documented Forge noise and rejects real failures.'
336+
doLast {
337+
File probe = file("${buildDir}/runtime-log-scanner-test")
338+
delete probe
339+
File logs = new File(probe, 'logs'); logs.mkdirs()
340+
new File(logs, 'latest.log').setText(
341+
'[main/ERROR] [FML]: FML appears to be missing any signature data\n'
342+
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
343+
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe', [] as Set)
344+
new File(logs, 'latest.log').setText(
345+
'[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8')
346+
boolean rejected = false
347+
try { assertRuntimeLogsClean(probe, 'scanner-mutable-position-probe', [] as Set) }
348+
catch (GradleException expected) { rejected = true }
349+
if (!rejected) throw new GradleException('Runtime log scanner accepted a mutable BlockPos leak')
350+
new File(logs, 'latest.log').setText(
351+
'[Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n', 'UTF-8')
352+
rejected = false
353+
try { assertRuntimeLogsClean(probe, 'scanner-cascading-probe', [] as Set) }
354+
catch (GradleException expected) { rejected = true }
355+
if (!rejected) throw new GradleException('Runtime log scanner accepted cascading worldgen')
356+
new File(logs, 'latest.log').setText(
357+
'[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
358+
rejected = false
359+
try { assertRuntimeLogsClean(probe, 'scanner-severity-probe', [] as Set) }
360+
catch (GradleException expected) { rejected = true }
361+
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
362+
delete probe
363+
}
364+
}
365+
366+
check.dependsOn runtimeLogScannerTest
367+
368+
task verifyMineralogyOracleIsolation {
369+
group = 'verification'
370+
description = 'Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
371+
doLast {
372+
configurations.each { configuration ->
373+
if (configuration.canBeResolved &&
374+
configuration.files.any { it.name ==~ /Mineralogy-.*\.jar/ }) {
375+
throw new GradleException("Mineralogy oracle leaked into Gradle configuration ${configuration.name}")
376+
}
377+
}
378+
}
379+
}
380+
381+
check.dependsOn verifyMineralogyOracleIsolation
382+
383+
['runClient', 'runServer', 'runData'].each { String taskName ->
384+
tasks.matching { it.name == taskName }.all { JavaExec runTask ->
385+
doFirst {
386+
new File(runTask.workingDir, 'mods').mkdirs()
387+
runTask.ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask.workingDir)
388+
}
389+
doLast {
390+
assertRuntimeLogsClean(runTask.workingDir, taskName,
391+
runTask.ext.oreSpawnCrashSnapshot as Set)
392+
}
393+
}
232394
}
233395

234396
def surfaceIntegrationClasses = layout.buildDirectory.dir('surface-integration-fixture/classes')
@@ -273,8 +435,16 @@ tasks.configureEach {
273435
} else if (name == 'runSurfaceIntegrationReload') {
274436
dependsOn 'runSurfaceIntegrationFresh'
275437
}
438+
if (name == 'runSurfaceIntegrationFresh' || name == 'runSurfaceIntegrationReload') {
439+
doFirst {
440+
ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(workingDir)
441+
}
442+
doLast {
443+
assertRuntimeLogsClean(workingDir, "Forge 64 ${name}",
444+
ext.oreSpawnCrashSnapshot as Set)
445+
}
446+
}
276447
}
277-
278448
def surfaceIntegrationTest = tasks.register('surfaceIntegrationTest') {
279449
group = 'verification'
280450
description = 'Verifies provider surfaces and dynamic-biome geology across fresh and reloaded normal terrain.'

src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public final class SurfaceProbeTestMod {
7575
private static final Identifier BIOME_A = Identifier.parse(MODID + ":surface_a");
7676
private static final Identifier BIOME_B = Identifier.parse(MODID + ":surface_b");
7777
private static final Identifier PROBE_GEOME = Identifier.parse(MODID + ":dynamic_biome_geome");
78+
private static final Identifier DYNAMIC_FLUID = Identifier.parse(MODID + ":fluid/dynamic_water");
7879
private static final Identifier[] BUILT_IN_GEOMES = {
7980
Identifier.parse("orespawn:stable_craton"), Identifier.parse("orespawn:mountain_belt"),
8081
Identifier.parse("orespawn:volcanic_arc"), Identifier.parse("orespawn:sedimentary_basin"),
@@ -106,8 +107,20 @@ public SurfaceProbeTestMod(FMLJavaModLoadingContext context) {
106107
private void enqueueProvider(InterModEnqueueEvent event) {
107108
WorldgenProvider.Builder provider = WorldgenProvider.builder(MODID, 1);
108109
addDynamicBiomeGeology(provider);
110+
provider.fluidDeposit(DYNAMIC_FLUID, blockId(Blocks.WATER), deposit -> deposit
111+
.dimension(OPEN_ID, placement -> placement
112+
.yRange(16, 24)
113+
.attempts(12.0D)
114+
.radius(1, 1)
115+
.verticalRadius(1, 1)
116+
.maxLobes(1)
117+
.minSolidCover(1)
118+
.minSolidShell(1)
119+
.hostBlock(blockId(Blocks.CALCITE))));
109120
addPalette(provider, "open_palette", OPEN_ID, false);
110121
addPalette(provider, "roofed_palette", ROOFED_ID, true);
122+
provider.dimensionMaterials(Identifier.parse(MODID + ":materials/nether"), ROOFED_ID,
123+
materials -> materials.defaultFluid(blockId(Blocks.WATER)));
111124
if (!OreSpawnApi.enqueue(provider.build())) {
112125
throw new IllegalStateException("Could not enqueue surface probe provider");
113126
}
@@ -143,6 +156,7 @@ private void enableGeologyProbe(ServerAboutToStartEvent event) {
143156
throw new IllegalStateException("Could not read the test-owned End geology profile", exception);
144157
}
145158
try {
159+
root.addProperty("place_fluid_deposits", true);
146160
JsonObject terrain = root.getAsJsonObject("terrain_dimensions");
147161
if (terrain == null) {
148162
terrain = new JsonObject();
@@ -345,8 +359,31 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) {
345359
+ ", sentinels=" + sentinels + ", geology=" + geology
346360
+ ", ceiling=" + ceiling + ", roofTop=" + roofTop);
347361
}
362+
long aquiferFluid = roofed ? 0L : auditDynamicFluid(level);
348363
return new AuditResult(top, underwater, filler, geology, ceiling, roofTop,
349-
biomeA, biomeB, edgeChanges, sentinels);
364+
biomeA, biomeB, edgeChanges, sentinels, aquiferFluid);
365+
}
366+
367+
private static long auditDynamicFluid(ServerLevel level) {
368+
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
369+
long water = 0L;
370+
for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) {
371+
for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) {
372+
level.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true);
373+
LevelChunk chunk = level.getChunk(chunkX, chunkZ);
374+
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) {
375+
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
376+
for (int y = 12; y <= 30; y++) {
377+
if (chunk.getBlockState(pos.set(x, y, z)).is(Blocks.WATER)) water++;
378+
}
379+
}
380+
}
381+
}
382+
}
383+
if (water == 0L) {
384+
throw new IllegalStateException("Forge 64 dynamic fluid deposit produced no covered flowing-water blocks");
385+
}
386+
return water;
350387
}
351388

352389
private static int auditSentinels(ServerLevel level, LevelChunk chunk,
@@ -460,6 +497,7 @@ private static Properties properties(long seed, Map<String, AuditResult> results
460497
values.setProperty(prefix + "biome_b", Integer.toString(result.biomeB()));
461498
values.setProperty(prefix + "edge_changes", Integer.toString(result.edgeChanges()));
462499
values.setProperty(prefix + "sentinels", Integer.toString(result.sentinels()));
500+
values.setProperty(prefix + "aquifer_fluid", Long.toString(result.aquiferFluid()));
463501
}
464502
return values;
465503
}
@@ -532,7 +570,7 @@ private static boolean prepareTerrain(WorldGenLevel world, ChunkAccess chunk) {
532570
chunk.setBlockState(pos.set(x, groundY - depth, z), Blocks.DIRT.defaultBlockState(), 0);
533571
}
534572
if (!roofed) {
535-
for (int depth = 6; depth <= 8; depth++) {
573+
for (int depth = 6; depth <= 60 && groundY - depth >= 1; depth++) {
536574
chunk.setBlockState(pos.set(x, groundY - depth, z), Blocks.END_STONE.defaultBlockState(), 0);
537575
}
538576
}
@@ -605,5 +643,5 @@ private record Material(BlockState top, BlockState filler,
605643

606644
private record AuditResult(long top, long underwater, long filler, long geology,
607645
long ceiling, long roofTop, int biomeA, int biomeB,
608-
int edgeChanges, int sentinels) { }
646+
int edgeChanges, int sentinels, long aquiferFluid) { }
609647
}

src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,11 @@ static BlockState[] resolveRockOrder(WorldGeologyProfile profile, String key,
170170
List<BlockState> states = new ArrayList<>();
171171
for (String idText : profile.cyanoRockOrder(key)) {
172172
try {
173-
ResourceLocation id = ResourceLocation.parse(idText);
173+
Identifier id = Identifier.parse(idText);
174174
Block block = ForgeRegistries.BLOCKS.containsKey(id)
175175
? ForgeRegistries.BLOCKS.getValue(id) : null;
176176
if (block != null && block != Blocks.AIR) {
177-
states.add(block.getDefaultState());
177+
states.add(block.defaultBlockState());
178178
} else {
179179
LOGGER.warn("Legacy Mineralogy rock '{}' is not registered and will be omitted", id);
180180
}

src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.nio.file.Path;
1111
import java.nio.file.StandardCopyOption;
1212
import java.util.ArrayList;
13+
import java.util.Arrays;
1314
import java.util.Comparator;
1415
import java.util.List;
1516
import java.util.Locale;

src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
2323
import com.google.gson.JsonObject;
2424
import com.google.gson.JsonPrimitive;
2525

26-
import net.minecraft.nbt.CompressedStreamTools;
27-
import net.minecraft.nbt.CompoundNBT;
28-
import net.minecraft.nbt.ListNBT;
29-
import net.minecraft.util.ResourceLocation;
26+
import net.minecraft.nbt.CompoundTag;
27+
import net.minecraft.nbt.ListTag;
28+
import net.minecraft.nbt.NbtAccounter;
29+
import net.minecraft.nbt.NbtIo;
30+
import net.minecraft.resources.Identifier;
3031
import net.minecraftforge.registries.ForgeRegistries;
3132

3233
import org.apache.logging.log4j.LogManager;
@@ -267,7 +268,7 @@ private static List<String> missingBlocks(List<String>... families) {
267268
for (List<String> family : families) {
268269
for (String idText : family) {
269270
try {
270-
ResourceLocation id = ResourceLocation.parse(idText);
271+
Identifier id = Identifier.parse(idText);
271272
if (!ForgeRegistries.BLOCKS.containsKey(id)) missing.add(id.toString());
272273
} catch (RuntimeException e) {
273274
missing.add(idText + " (invalid registry name)");
@@ -313,7 +314,7 @@ private static MineralogyIdentity legacyMineralogyIdentity(Path worldRoot) {
313314
Path levelDat = worldRoot.resolve(fileName);
314315
if (!Files.isRegularFile(levelDat)) continue;
315316
try (FileInputStream input = new FileInputStream(levelDat.toFile())) {
316-
CompoundNBT root = CompressedStreamTools.readCompressed(input);
317+
CompoundTag root = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap());
317318
MineralogyIdentity identity = identity(root, fileName);
318319
if (identity != null) return identity.legacy ? identity : null;
319320
} catch (IOException | RuntimeException e) {
@@ -323,15 +324,15 @@ private static MineralogyIdentity legacyMineralogyIdentity(Path worldRoot) {
323324
return null;
324325
}
325326

326-
private static MineralogyIdentity identity(CompoundNBT root, String sourceFile) {
327+
private static MineralogyIdentity identity(CompoundTag root, String sourceFile) {
327328
for (ModListPath path : MOD_LIST_PATHS) {
328-
CompoundNBT container = root.getCompound(path.compound);
329-
ListNBT mods = container.getList(path.list, 10);
329+
CompoundTag container = root.getCompoundOrEmpty(path.compound);
330+
ListTag mods = container.getListOrEmpty(path.list);
330331
for (int i = 0; i < mods.size(); i++) {
331-
CompoundNBT mod = mods.getCompound(i);
332-
String id = firstNonBlank(mod.getString("ModId"), mod.getString("modid"));
332+
CompoundTag mod = mods.getCompoundOrEmpty(i);
333+
String id = firstNonBlank(mod.getStringOr("ModId", ""), mod.getStringOr("modid", ""));
333334
if (!"mineralogy".equalsIgnoreCase(id)) continue;
334-
String version = firstNonBlank(mod.getString("ModVersion"), mod.getString("version")).trim();
335+
String version = firstNonBlank(mod.getStringOr("ModVersion", ""), mod.getStringOr("version", "")).trim();
335336
return new MineralogyIdentity(version.isEmpty() ? "legacy" : version,
336337
sourceFile + " (" + path.compound + "/" + path.list + ")",
337338
isLegacyVersion(version));
@@ -479,7 +480,7 @@ private static List<String> parseDelimitedList(String value, String delimiter) {
479480
private static void addConfiguredId(List<String> result, String raw) {
480481
String value = unquote(raw.trim());
481482
if (value.isEmpty()) return;
482-
try { result.add(ResourceLocation.parse(value).toString()); }
483+
try { result.add(Identifier.parse(value).toString()); }
483484
catch (RuntimeException e) {
484485
LOGGER.warn("Ignoring invalid legacy Mineralogy rock registry name '{}'", value);
485486
}

src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigratorTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,8 @@ private static JsonObject rule(JsonObject ore) {
216216
return ore.getAsJsonObject("dimensions").entrySet().iterator().next()
217217
.getValue().getAsJsonObject();
218218
}
219+
220+
private static String read(Path path) throws IOException {
221+
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
222+
}
219223
}

0 commit comments

Comments
 (0)