Skip to content

Commit 3c5f08d

Browse files
committed
Audit Forge 1.14 runtime logs and fluid writes
1 parent 84a46e5 commit 3c5f08d

3 files changed

Lines changed: 349 additions & 6 deletions

File tree

‎build.gradle‎

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,133 @@ javadoc {
200200

201201
test {
202202
useJUnitPlatform()
203+
// Loaded only through an isolated URLClassLoader by the parity test. This
204+
// is deliberately not a Gradle dependency and cannot leak into Eclipse or
205+
// a published OreSpawn jar.
206+
File mineralogy5Oracle = file('../../MinecraftMineralogy 114-new/MinecraftMineralogy/build/libs/Mineralogy-1.14.4-5.0.1.jar')
207+
if (mineralogy5Oracle.isFile()) {
208+
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
209+
}
210+
}
211+
212+
// A Forge process is not green merely because it returns exit code zero. The
213+
// loader can log a worldgen/linkage failure and still shut down normally.
214+
def acceptedForge28LogNoise = [
215+
~/FML appears to be missing any signature data/,
216+
~/Found multiple arguments for option fml\.mcVersion/,
217+
~/Found multiple arguments for option fml\.forgeVersion/,
218+
~/\/FATAL\] \[net\.minecraftforge\.common\.ForgeConfig\/CORE\]: Forge config just got changed on the file system!$/,
219+
~/\/FATAL\] \[net\.minecraftforge\.fml\.packs\.ModFileResourcePack\/\]: Failed to clean up tempdir /
220+
]
221+
222+
def runtimeCrashSnapshot = { File runDirectory ->
223+
File crashDirectory = new File(runDirectory, 'crash-reports')
224+
if (!crashDirectory.isDirectory()) return [] as Set
225+
return fileTree(crashDirectory) { include '**/*' }.files
226+
.findAll { it.isFile() }.collect { it.absolutePath } as Set
227+
}
228+
229+
def assertRuntimeLogsClean = { File runDirectory, String context, Set priorCrashes ->
230+
File crashDirectory = new File(runDirectory, 'crash-reports')
231+
if (crashDirectory.isDirectory()) {
232+
def crashes = fileTree(crashDirectory) { include '**/*' }.files
233+
.findAll { it.isFile() && !priorCrashes.contains(it.absolutePath) }
234+
if (!crashes.isEmpty()) {
235+
throw new GradleException("${context} produced crash report ${crashes.first()}")
236+
}
237+
}
238+
239+
File logsDirectory = new File(runDirectory, 'logs')
240+
if (!logsDirectory.isDirectory()) return
241+
def failures = []
242+
[new File(logsDirectory, 'latest.log'), new File(logsDirectory, 'debug.log')]
243+
.findAll { it.isFile() }.each { File log ->
244+
int lineNumber = 0
245+
log.eachLine('UTF-8') { String line ->
246+
lineNumber++
247+
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
248+
boolean knownNoise = acceptedForge28LogNoise.any { line =~ it }
249+
boolean fatalText = line.contains('Encountered an unexpected exception') ||
250+
line.contains('Exception stopping the server') ||
251+
line.contains('Migration audit failed') ||
252+
line.contains('java.lang.Error:') ||
253+
line.contains('NoSuchMethodError') ||
254+
line.contains('NoClassDefFoundError') ||
255+
line.contains('ExceptionInInitializerError') ||
256+
line.contains('Tried to assign a mutable BlockPos') ||
257+
line.contains('causing cascading worldgen lag')
258+
if ((unexpectedSeverity && !knownNoise) || fatalText) {
259+
failures.add("${log.name}:${lineNumber}: ${line}")
260+
}
261+
}
262+
}
263+
if (!failures.isEmpty()) {
264+
throw new GradleException("${context} logged unexpected errors:\n"
265+
+ failures.take(20).join('\n'))
266+
}
267+
}
268+
269+
task runtimeLogScannerTest {
270+
group = 'verification'
271+
description = 'Proves runtime log validation accepts documented Forge noise and rejects real failures.'
272+
doLast {
273+
File probe = file("${buildDir}/runtime-log-scanner-test")
274+
delete probe
275+
File logs = new File(probe, 'logs'); logs.mkdirs()
276+
new File(logs, 'latest.log').setText(
277+
'[main/ERROR] [FML]: FML appears to be missing any signature data\n'
278+
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
279+
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe', [] as Set)
280+
new File(logs, 'latest.log').setText(
281+
'[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8')
282+
boolean rejected = false
283+
try { assertRuntimeLogsClean(probe, 'scanner-mutable-position-probe', [] as Set) }
284+
catch (GradleException expected) { rejected = true }
285+
if (!rejected) throw new GradleException('Runtime log scanner accepted a mutable BlockPos leak')
286+
new File(logs, 'latest.log').setText(
287+
'[Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n', 'UTF-8')
288+
rejected = false
289+
try { assertRuntimeLogsClean(probe, 'scanner-cascading-probe', [] as Set) }
290+
catch (GradleException expected) { rejected = true }
291+
if (!rejected) throw new GradleException('Runtime log scanner accepted cascading worldgen')
292+
new File(logs, 'latest.log').setText(
293+
'[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
294+
rejected = false
295+
try { assertRuntimeLogsClean(probe, 'scanner-severity-probe', [] as Set) }
296+
catch (GradleException expected) { rejected = true }
297+
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
298+
delete probe
299+
}
300+
}
301+
302+
check.dependsOn runtimeLogScannerTest
303+
304+
task verifyMineralogyOracleIsolation {
305+
group = 'verification'
306+
description = 'Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
307+
doLast {
308+
configurations.each { configuration ->
309+
if (configuration.canBeResolved &&
310+
configuration.files.any { it.name ==~ /Mineralogy-.*\.jar/ }) {
311+
throw new GradleException("Mineralogy oracle leaked into Gradle configuration ${configuration.name}")
312+
}
313+
}
314+
}
315+
}
316+
317+
check.dependsOn verifyMineralogyOracleIsolation
318+
319+
['runClient', 'runServer', 'runData'].each { String taskName ->
320+
tasks.matching { it.name == taskName }.all { JavaExec runTask ->
321+
doFirst {
322+
new File(runTask.workingDir, 'mods').mkdirs()
323+
runTask.ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask.workingDir)
324+
}
325+
doLast {
326+
assertRuntimeLogsClean(runTask.workingDir, taskName,
327+
runTask.ext.oreSpawnCrashSnapshot as Set)
328+
}
329+
}
203330
}
204331

205332
def surfaceIntegrationClasses = file("${buildDir}/surface-integration-fixture/classes")
@@ -291,8 +418,14 @@ surfaceIntegrationFreshProcess.doLast {
291418
if (!marker.isFile()) {
292419
throw new GradleException("Fresh surface integration completion marker is missing: ${marker}")
293420
}
421+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
422+
'surface integration fresh phase', [] as Set)
294423
}
295424
def surfaceIntegrationReloadProcess = createSurfaceProcess('Reload', surfaceIntegrationFreshProcess)
425+
surfaceIntegrationReloadProcess.doLast {
426+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
427+
'surface integration reload phase', [] as Set)
428+
}
296429

297430
task surfaceIntegrationTest(dependsOn: surfaceIntegrationReloadProcess) {
298431
group = 'verification'
@@ -341,6 +474,11 @@ task syncForge28EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
341474
/<mapEntry key="MOD_CLASSES" value="[^"]*"\/>/,
342475
java.util.regex.Matcher.quoteReplacement(
343476
"<mapEntry key=\"MOD_CLASSES\" value=\"${ordinaryModClasses}\"/>"))
477+
if (!text.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
478+
text = text.replace('</launchConfiguration>',
479+
' <booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>\r\n' +
480+
'</launchConfiguration>')
481+
}
344482
launch.setText(text, 'UTF-8')
345483
}
346484
['Fresh', 'Reload'].each { String phase ->
@@ -365,6 +503,22 @@ task syncForge28EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
365503
}
366504
}
367505

506+
task verifyForge28EclipseLaunchIsolation {
507+
group = 'verification'
508+
doLast {
509+
['runClient.launch', 'runServer.launch', 'runData.launch'].each { String launchName ->
510+
File launch = file(launchName)
511+
String launchText = launch.getText('UTF-8')
512+
if (launchText.contains('Mineralogy-') ||
513+
!launchText.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
514+
throw new GradleException("Ordinary Eclipse launch is not isolated from test oracles: ${launch}")
515+
}
516+
}
517+
}
518+
}
519+
520+
syncForge28EclipseLaunches.finalizedBy verifyForge28EclipseLaunchIsolation
521+
368522
tasks.matching { it.name == 'genEclipseRuns' }.all {
369523
finalizedBy syncForge28EclipseLaunches
370524
}

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public final class SurfaceProbeTestMod {
118118
private static final ResourceLocation BIOME_B = new ResourceLocation(MODID + ":surface_b");
119119
private static final ResourceLocation PROBE_GEOME = new ResourceLocation(MODID + ":dynamic_biome_geome");
120120
private static final ResourceLocation SPRING_ROCK = new ResourceLocation(MODID + ":rock/spring_host");
121+
private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water");
121122
private static final BlockPos SPRING_POS = new BlockPos(1128, 32, 1128);
122123
private static final ResourceLocation[] BUILT_IN_GEOMES = {
123124
new ResourceLocation("orespawn:stable_craton"), new ResourceLocation("orespawn:mountain_belt"),
@@ -192,6 +193,16 @@ private static void addUnique(java.util.List<ConfiguredFeature<?>> features,
192193
private void enqueueProvider(InterModEnqueueEvent event) {
193194
WorldgenProvider.Builder provider = WorldgenProvider.builder(MODID, 1);
194195
addDynamicBiomeGeology(provider);
196+
provider.fluidDeposit(DYNAMIC_FLUID, blockId(Blocks.WATER), deposit -> deposit
197+
.dimension(OPEN_ID, placement -> placement
198+
.yRange(16, 24)
199+
.attempts(12.0D)
200+
.radius(1, 1)
201+
.verticalRadius(1, 1)
202+
.maxLobes(1)
203+
.minSolidCover(1)
204+
.minSolidShell(1)
205+
.hostBlock(blockId(Blocks.DIORITE))));
195206
addPalette(provider, "open_palette_0", OPEN_ID, false);
196207
addPalette(provider, "roofed_palette_0", ROOFED_ID, true);
197208
provider.dimensionMaterials(new ResourceLocation(MODID + ":materials/nether"), ROOFED_ID,
@@ -237,6 +248,7 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent event) {
237248
throw new IllegalStateException("Could not read the test-owned End geology profile", exception);
238249
}
239250
try {
251+
root.addProperty("place_fluid_deposits", true);
240252
JsonObject terrain = root.getAsJsonObject("terrain_dimensions");
241253
if (terrain == null) {
242254
terrain = new JsonObject();
@@ -502,29 +514,29 @@ private static AuditResult auditDimension(ServerWorld level, boolean roofed) {
502514
+ ", sentinels=" + sentinels + ", geology=" + geology
503515
+ ", ceiling=" + ceiling + ", roofTop=" + roofTop);
504516
}
505-
long aquiferFluid = 0L;
517+
long aquiferFluid = roofed ? 0L : auditDynamicFluid(level);
506518
return new AuditResult(top, underwater, filler, geology, ceiling, roofTop,
507519
biomeA, biomeB, edgeChanges, sentinels, aquiferFluid);
508520
}
509521

510-
private static long auditDefaultFluid(ServerWorld level) {
522+
private static long auditDynamicFluid(ServerWorld level) {
511523
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
512524
long water = 0L;
513525
for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) {
514-
for (int chunkX = FLUID_PROBE_MIN_CHUNK_X; chunkX <= FLUID_PROBE_MAX_CHUNK_X; chunkX++) {
526+
for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) {
515527
level.getChunk(chunkX, chunkZ, ChunkStatus.FULL, true);
516528
Chunk chunk = level.getChunk(chunkX, chunkZ);
517529
for (int x = chunk.getPos().getXStart(); x <= chunk.getPos().getXEnd(); x++) {
518530
for (int z = chunk.getPos().getZStart(); z <= chunk.getPos().getZEnd(); z++) {
519-
for (int y = 0; y <= 63; y++) {
531+
for (int y = 12; y <= 30; y++) {
520532
if (chunk.getBlockState(pos.setPos(x, y, z)).getBlock() == Blocks.WATER) water++;
521533
}
522534
}
523535
}
524536
}
525537
}
526538
if (water == 0L) {
527-
throw new IllegalStateException("Minecraft 1.17.1 default-fluid override produced no water in the fixed untouched Nether probe strip");
539+
throw new IllegalStateException("Forge 28 dynamic fluid deposit produced no covered flowing-water blocks");
528540
}
529541
return water;
530542
}
@@ -720,7 +732,7 @@ private static boolean prepareTerrain(IWorld world, IChunk chunk) {
720732
chunk.setBlockState(pos.setPos(x, groundY - depth, z), Blocks.DIRT.getDefaultState(), false);
721733
}
722734
if (!roofed) {
723-
for (int depth = 6; depth <= 8; depth++) {
735+
for (int depth = 6; depth <= 60 && groundY - depth >= 1; depth++) {
724736
chunk.setBlockState(pos.setPos(x, groundY - depth, z), Blocks.END_STONE.getDefaultState(), false);
725737
}
726738
}

0 commit comments

Comments
 (0)