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