Skip to content

Commit b986ad0

Browse files
committed
Audit Forge 1.17 runtime logs and fluid writes
1 parent b6b94a9 commit b986ad0

9 files changed

Lines changed: 443 additions & 43 deletions

File tree

‎build.gradle‎

Lines changed: 198 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,168 @@ tasks.named('javadoc', Javadoc).configure {
299299

300300
tasks.named('test', Test).configure {
301301
useJUnitPlatform()
302+
// Unit tests inspect target files relative to the checkout, but they do
303+
// not need Forge's rolling runtime files. A console-only test logger keeps
304+
// them from contending with Eclipse/client logs in this working directory.
305+
systemProperty 'log4j.configurationFile', file('src/test/resources/log4j2-test.xml').absolutePath
306+
// Loaded only through an isolated URLClassLoader by the parity test. This
307+
// is deliberately not a Gradle dependency and cannot leak into Eclipse or
308+
// a published OreSpawn jar.
309+
File mineralogy5Oracle = file('../../MinecraftMineralogy 117/MinecraftMineralogy/build/libs/Mineralogy-1.17.1-5.3.0.jar')
310+
if (mineralogy5Oracle.isFile()) {
311+
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
312+
}
313+
}
314+
315+
// Several registry-focused tests initialize the real global config singleton.
316+
// Keep that target-native coverage without creating or changing a developer's
317+
// checkout config as a side effect of `test` or `build`.
318+
def unitTestWorldgenConfig = file('config/orespawn-worldgen.json')
319+
def unitTestWorldgenConfigWasPresent = false
320+
byte[] unitTestWorldgenConfigBytes = null
321+
tasks.named('test', Test).configure {
322+
doFirst {
323+
unitTestWorldgenConfigWasPresent = unitTestWorldgenConfig.isFile()
324+
unitTestWorldgenConfigBytes = unitTestWorldgenConfigWasPresent
325+
? unitTestWorldgenConfig.bytes : null
326+
}
327+
}
328+
def preserveDeveloperWorldgenConfig = tasks.register('preserveDeveloperWorldgenConfig') {
329+
doLast {
330+
if (unitTestWorldgenConfigWasPresent) {
331+
byte[] after = unitTestWorldgenConfig.isFile() ? unitTestWorldgenConfig.bytes : null
332+
if (after == null || !java.util.Arrays.equals(unitTestWorldgenConfigBytes, after)) {
333+
unitTestWorldgenConfig.parentFile.mkdirs()
334+
unitTestWorldgenConfig.bytes = unitTestWorldgenConfigBytes
335+
throw new GradleException('Unit tests changed config/orespawn-worldgen.json; the original was restored')
336+
}
337+
} else if (unitTestWorldgenConfig.isFile()) {
338+
delete unitTestWorldgenConfig
339+
}
340+
}
341+
}
342+
tasks.named('test') {
343+
finalizedBy preserveDeveloperWorldgenConfig
344+
}
345+
346+
// A Forge process is not green merely because it returns exit code zero. The
347+
// loader can log a worldgen/linkage failure and still shut down normally.
348+
def acceptedForge37LogNoise = [
349+
~/FML appears to be missing any signature data/,
350+
~/Found multiple arguments for option fml\.mcVersion/,
351+
~/Found multiple arguments for option fml\.forgeVersion/,
352+
~/\/FATAL\] \[net\.minecraftforge\.common\.ForgeConfig\/CORE\]: Forge config just got changed on the file system!$/,
353+
~/\/FATAL\] \[net\.minecraftforge\.fml\.packs\.ModFileResourcePack\/\]: Failed to clean up tempdir /
354+
]
355+
356+
def runtimeCrashSnapshot = { File runDirectory ->
357+
File crashDirectory = new File(runDirectory, 'crash-reports')
358+
if (!crashDirectory.isDirectory()) return [] as Set
359+
return fileTree(crashDirectory) { include '**/*' }.files
360+
.findAll { it.isFile() }.collect { it.absolutePath } as Set
361+
}
362+
363+
def assertRuntimeLogsClean = { File runDirectory, String context, Set priorCrashes ->
364+
File crashDirectory = new File(runDirectory, 'crash-reports')
365+
if (crashDirectory.isDirectory()) {
366+
def crashes = fileTree(crashDirectory) { include '**/*' }.files
367+
.findAll { it.isFile() && !priorCrashes.contains(it.absolutePath) }
368+
if (!crashes.isEmpty()) {
369+
throw new GradleException("${context} produced crash report ${crashes.first()}")
370+
}
371+
}
372+
373+
File logsDirectory = new File(runDirectory, 'logs')
374+
if (!logsDirectory.isDirectory()) return
375+
def failures = []
376+
[new File(logsDirectory, 'latest.log'), new File(logsDirectory, 'debug.log')]
377+
.findAll { it.isFile() }.each { File log ->
378+
int lineNumber = 0
379+
log.eachLine('UTF-8') { String line ->
380+
lineNumber++
381+
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
382+
boolean knownNoise = acceptedForge37LogNoise.any { line =~ it }
383+
boolean fatalText = line.contains('Encountered an unexpected exception') ||
384+
line.contains('Exception stopping the server') ||
385+
line.contains('Migration audit failed') ||
386+
line.contains('java.lang.Error:') ||
387+
line.contains('NoSuchMethodError') ||
388+
line.contains('NoClassDefFoundError') ||
389+
line.contains('ExceptionInInitializerError') ||
390+
line.contains('Tried to assign a mutable BlockPos') ||
391+
line.contains('causing cascading worldgen lag')
392+
if ((unexpectedSeverity && !knownNoise) || fatalText) {
393+
failures.add("${log.name}:${lineNumber}: ${line}")
394+
}
395+
}
396+
}
397+
if (!failures.isEmpty()) {
398+
throw new GradleException("${context} logged unexpected errors:\n"
399+
+ failures.take(20).join('\n'))
400+
}
401+
}
402+
403+
task runtimeLogScannerTest {
404+
group = 'verification'
405+
description = 'Proves runtime log validation accepts documented Forge noise and rejects real failures.'
406+
doLast {
407+
File probe = file("${buildDir}/runtime-log-scanner-test")
408+
delete probe
409+
File logs = new File(probe, 'logs'); logs.mkdirs()
410+
new File(logs, 'latest.log').setText(
411+
'[main/ERROR] [FML]: FML appears to be missing any signature data\n'
412+
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
413+
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe', [] as Set)
414+
new File(logs, 'latest.log').setText(
415+
'[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8')
416+
boolean rejected = false
417+
try { assertRuntimeLogsClean(probe, 'scanner-mutable-position-probe', [] as Set) }
418+
catch (GradleException expected) { rejected = true }
419+
if (!rejected) throw new GradleException('Runtime log scanner accepted a mutable BlockPos leak')
420+
new File(logs, 'latest.log').setText(
421+
'[Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n', 'UTF-8')
422+
rejected = false
423+
try { assertRuntimeLogsClean(probe, 'scanner-cascading-probe', [] as Set) }
424+
catch (GradleException expected) { rejected = true }
425+
if (!rejected) throw new GradleException('Runtime log scanner accepted cascading worldgen')
426+
new File(logs, 'latest.log').setText(
427+
'[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
428+
rejected = false
429+
try { assertRuntimeLogsClean(probe, 'scanner-severity-probe', [] as Set) }
430+
catch (GradleException expected) { rejected = true }
431+
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
432+
delete probe
433+
}
434+
}
435+
436+
check.dependsOn runtimeLogScannerTest
437+
438+
task verifyMineralogyOracleIsolation {
439+
group = 'verification'
440+
description = 'Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
441+
doLast {
442+
configurations.each { configuration ->
443+
if (configuration.canBeResolved &&
444+
configuration.files.any { it.name ==~ /Mineralogy-.*\.jar/ }) {
445+
throw new GradleException("Mineralogy oracle leaked into Gradle configuration ${configuration.name}")
446+
}
447+
}
448+
}
449+
}
450+
451+
check.dependsOn verifyMineralogyOracleIsolation
452+
453+
['runClient', 'runServer', 'runData'].each { String taskName ->
454+
tasks.matching { it.name == taskName }.all { JavaExec runTask ->
455+
doFirst {
456+
new File(runTask.workingDir, 'mods').mkdirs()
457+
runTask.ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask.workingDir)
458+
}
459+
doLast {
460+
assertRuntimeLogsClean(runTask.workingDir, taskName,
461+
runTask.ext.oreSpawnCrashSnapshot as Set)
462+
}
463+
}
302464
}
303465

304466
def surfaceIntegrationClasses = layout.buildDirectory.dir('surface-integration-fixture/classes')
@@ -360,14 +522,25 @@ def surfaceIntegrationProcess = { String phase, dependency ->
360522
group = 'verification'
361523
description = "Runs the ${phase.toLowerCase(Locale.ROOT)} surface-probe server behind a process boundary."
362524
dependsOn dependency, "prepareRunSurfaceIntegration${phase}"
363-
doFirst {
364-
JavaExec runTask = tasks.getByName("runSurfaceIntegration${phase}") as JavaExec
365-
File argumentFile = layout.buildDirectory.file(
366-
"surface-integration-fixture/${phase.toLowerCase(Locale.ROOT)}.args").get().asFile
367-
argumentFile.parentFile.mkdirs()
368-
List<String> arguments = []
369-
arguments.addAll(runTask.allJvmArgs)
370-
arguments.add('-cp')
525+
doFirst {
526+
JavaExec runTask = tasks.getByName("runSurfaceIntegration${phase}") as JavaExec
527+
File argumentFile = layout.buildDirectory.file(
528+
"surface-integration-fixture/${phase.toLowerCase(Locale.ROOT)}.args").get().asFile
529+
argumentFile.parentFile.mkdirs()
530+
List<String> arguments = []
531+
arguments.addAll(runTask.allJvmArgs)
532+
String legacyClassPathArgument = arguments.find {
533+
it.startsWith('-DlegacyClassPath.file=')
534+
}
535+
if (legacyClassPathArgument != null) {
536+
File legacyClassPath = file(legacyClassPathArgument.substring(
537+
'-DlegacyClassPath.file='.length()))
538+
legacyClassPath.parentFile.mkdirs()
539+
legacyClassPath.setText(runTask.classpath.files.findAll { it.isFile() }
540+
.collect { it.absolutePath }
541+
.join(System.lineSeparator()) + System.lineSeparator(), 'UTF-8')
542+
}
543+
arguments.add('-cp')
371544
arguments.add(runTask.classpath.asPath)
372545
arguments.add(runTask.main)
373546
arguments.addAll(runTask.args)
@@ -393,7 +566,24 @@ def surfaceIntegrationProcess = { String phase, dependency ->
393566
}
394567

395568
def surfaceIntegrationFreshProcess = surfaceIntegrationProcess('Fresh', prepareSurfaceIntegrationTest)
569+
surfaceIntegrationFreshProcess.configure {
570+
doLast {
571+
File marker = surfaceIntegrationRunDirectory.get().file(
572+
'surface-integration-world/surfaceprobe-integration.properties').asFile
573+
if (!marker.isFile()) {
574+
throw new GradleException("Fresh surface integration completion marker is missing: ${marker}")
575+
}
576+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory.get().asFile,
577+
'Forge 37 surface integration fresh phase', [] as Set)
578+
}
579+
}
396580
def surfaceIntegrationReloadProcess = surfaceIntegrationProcess('Reload', surfaceIntegrationFreshProcess)
581+
surfaceIntegrationReloadProcess.configure {
582+
doLast {
583+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory.get().asFile,
584+
'Forge 37 surface integration reload phase', [] as Set)
585+
}
586+
}
397587

398588
def surfaceIntegrationTest = tasks.register('surfaceIntegrationTest') {
399589
group = 'verification'

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public final class SurfaceProbeTestMod {
9898
private static final ResourceLocation BIOME_A = new ResourceLocation(MODID + ":surface_a");
9999
private static final ResourceLocation BIOME_B = new ResourceLocation(MODID + ":surface_b");
100100
private static final ResourceLocation PROBE_GEOME = new ResourceLocation(MODID + ":dynamic_biome_geome");
101+
private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water");
101102
private static final ResourceLocation[] BUILT_IN_GEOMES = {
102103
new ResourceLocation("orespawn:stable_craton"), new ResourceLocation("orespawn:mountain_belt"),
103104
new ResourceLocation("orespawn:volcanic_arc"), new ResourceLocation("orespawn:sedimentary_basin"),
@@ -171,6 +172,16 @@ private static void addUnique(java.util.List<Supplier<ConfiguredFeature<?, ?>>>
171172
private void enqueueProvider(InterModEnqueueEvent event) {
172173
WorldgenProvider.Builder provider = WorldgenProvider.builder(MODID, 1);
173174
addDynamicBiomeGeology(provider);
175+
provider.fluidDeposit(DYNAMIC_FLUID, blockId(Blocks.WATER), deposit -> deposit
176+
.dimension(OPEN_ID, placement -> placement
177+
.yRange(16, 24)
178+
.attempts(12.0D)
179+
.radius(1, 1)
180+
.verticalRadius(1, 1)
181+
.maxLobes(1)
182+
.minSolidCover(1)
183+
.minSolidShell(1)
184+
.hostBlock(blockId(Blocks.CALCITE))));
174185
addPalette(provider, "open_palette_0", OPEN_ID, false);
175186
addPalette(provider, "roofed_palette_0", ROOFED_ID, true);
176187
provider.dimensionMaterials(new ResourceLocation(MODID + ":materials/nether"), ROOFED_ID,
@@ -210,6 +221,7 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent event) {
210221
throw new IllegalStateException("Could not read the test-owned End geology profile", exception);
211222
}
212223
try {
224+
root.addProperty("place_fluid_deposits", true);
213225
JsonObject terrain = root.getAsJsonObject("terrain_dimensions");
214226
if (terrain == null) {
215227
terrain = new JsonObject();
@@ -423,29 +435,29 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) {
423435
+ ", sentinels=" + sentinels + ", geology=" + geology
424436
+ ", ceiling=" + ceiling + ", roofTop=" + roofTop);
425437
}
426-
long aquiferFluid = roofed ? auditDefaultFluid(level) : 0L;
438+
long aquiferFluid = roofed ? 0L : auditDynamicFluid(level);
427439
return new AuditResult(top, underwater, filler, geology, ceiling, roofTop,
428440
biomeA, biomeB, edgeChanges, sentinels, aquiferFluid);
429441
}
430442

431-
private static long auditDefaultFluid(ServerLevel level) {
443+
private static long auditDynamicFluid(ServerLevel level) {
432444
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
433445
long water = 0L;
434446
for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) {
435-
for (int chunkX = FLUID_PROBE_MIN_CHUNK_X; chunkX <= FLUID_PROBE_MAX_CHUNK_X; chunkX++) {
447+
for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) {
436448
level.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true);
437449
LevelChunk chunk = level.getChunk(chunkX, chunkZ);
438450
for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) {
439451
for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) {
440-
for (int y = level.getMinBuildHeight(); y <= Math.min(63, level.getMaxBuildHeight() - 1); y++) {
452+
for (int y = 12; y <= 30; y++) {
441453
if (chunk.getBlockState(pos.set(x, y, z)).is(Blocks.WATER)) water++;
442454
}
443455
}
444456
}
445457
}
446458
}
447459
if (water == 0L) {
448-
throw new IllegalStateException("Minecraft 1.17.1 default-fluid override produced no water in the fixed untouched Nether probe strip");
460+
throw new IllegalStateException("Forge 37 dynamic fluid deposit produced no covered flowing-water blocks");
449461
}
450462
return water;
451463
}
@@ -637,7 +649,7 @@ private static boolean prepareTerrain(WorldGenLevel world, ChunkAccess chunk) {
637649
chunk.setBlockState(pos.set(x, groundY - depth, z), Blocks.DIRT.defaultBlockState(), false);
638650
}
639651
if (!roofed) {
640-
for (int depth = 6; depth <= 8; depth++) {
652+
for (int depth = 6; depth <= 60 && groundY - depth >= 1; depth++) {
641653
chunk.setBlockState(pos.set(x, groundY - depth, z), Blocks.END_STONE.defaultBlockState(), false);
642654
}
643655
}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public void replaceStoneInChunk(LevelAccessor world, ChunkAccess chunk, BakedTer
116116
if (terrain.isReplaceable(current)
117117
|| (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) {
118118
BlockState replacement = pickReplacement(baseRockVal, geomeBase, y);
119-
if (!GeomeGeology.changes(current, replacement)) continue;
119+
if (current.equals(replacement)) continue;
120120
chunk.setBlockState(cursor, replacement, false);
121121
changed = true;
122122
}
@@ -172,10 +172,9 @@ static BlockState[] resolveRockOrder(WorldGeologyProfile profile, String key,
172172
for (String idText : profile.cyanoRockOrder(key)) {
173173
try {
174174
ResourceLocation id = new ResourceLocation(idText);
175-
Block block = ForgeRegistries.BLOCKS.containsKey(id)
176-
? ForgeRegistries.BLOCKS.getValue(id) : null;
175+
Block block = ForgeRegistries.BLOCKS.getValue(id);
177176
if (block != null && block != Blocks.AIR) {
178-
states.add(block.getDefaultState());
177+
states.add(block.defaultBlockState());
179178
} else {
180179
LOGGER.warn("Legacy Mineralogy rock '{}' is not registered and will be omitted", id);
181180
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.nio.file.Path;
1313
import java.nio.file.StandardCopyOption;
1414
import java.util.ArrayList;
15+
import java.util.Arrays;
1516
import java.util.Comparator;
1617
import java.util.List;
1718
import java.util.Locale;

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
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.NbtIo;
27+
import net.minecraft.nbt.CompoundTag;
28+
import net.minecraft.nbt.ListTag;
29+
import net.minecraft.resources.ResourceLocation;
3030
import net.minecraftforge.registries.ForgeRegistries;
3131

3232
import org.apache.logging.log4j.LogManager;
@@ -308,7 +308,7 @@ private static MineralogyIdentity legacyMineralogyIdentity(Path worldRoot) {
308308
Path levelDat = worldRoot.resolve(fileName);
309309
if (!Files.isRegularFile(levelDat)) continue;
310310
try (FileInputStream input = new FileInputStream(levelDat.toFile())) {
311-
CompoundNBT root = CompressedStreamTools.readCompressed(input);
311+
CompoundTag root = NbtIo.readCompressed(input);
312312
MineralogyIdentity identity = identity(root, fileName);
313313
if (identity != null) return identity.legacy ? identity : null;
314314
} catch (IOException | RuntimeException e) {
@@ -318,12 +318,12 @@ private static MineralogyIdentity legacyMineralogyIdentity(Path worldRoot) {
318318
return null;
319319
}
320320

321-
private static MineralogyIdentity identity(CompoundNBT root, String sourceFile) {
321+
private static MineralogyIdentity identity(CompoundTag root, String sourceFile) {
322322
for (ModListPath path : MOD_LIST_PATHS) {
323-
CompoundNBT container = root.getCompound(path.compound);
324-
ListNBT mods = container.getList(path.list, 10);
323+
CompoundTag container = root.getCompound(path.compound);
324+
ListTag mods = container.getList(path.list, 10);
325325
for (int i = 0; i < mods.size(); i++) {
326-
CompoundNBT mod = mods.getCompound(i);
326+
CompoundTag mod = mods.getCompound(i);
327327
String id = firstNonBlank(mod.getString("ModId"), mod.getString("modid"));
328328
if (!"mineralogy".equalsIgnoreCase(id)) continue;
329329
String version = firstNonBlank(mod.getString("ModVersion"), mod.getString("version")).trim();

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

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

0 commit comments

Comments
 (0)