@@ -299,6 +299,168 @@ tasks.named('javadoc', Javadoc).configure {
299299
300300tasks. 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 116/MinecraftMineralogy/build/libs/Mineralogy-1.16.5-5.2.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 acceptedForge36LogNoise = [
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 = acceptedForge36LogNoise. 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
304466def surfaceIntegrationClasses = layout. buildDirectory. dir(' surface-integration-fixture/classes' )
@@ -403,9 +565,17 @@ surfaceIntegrationFreshProcess.configure {
403565 if (! marker. isFile()) {
404566 throw new GradleException (" Fresh surface integration completion marker is missing: ${ marker} " )
405567 }
568+ assertRuntimeLogsClean(surfaceIntegrationRunDirectory. get(). asFile,
569+ ' Forge 36 surface integration fresh phase' , [] as Set )
406570 }
407571}
408572def surfaceIntegrationReloadProcess = surfaceIntegrationProcess(' Reload' , surfaceIntegrationFreshProcess)
573+ surfaceIntegrationReloadProcess. configure {
574+ doLast {
575+ assertRuntimeLogsClean(surfaceIntegrationRunDirectory. get(). asFile,
576+ ' Forge 36 surface integration reload phase' , [] as Set )
577+ }
578+ }
409579
410580def surfaceIntegrationTest = tasks. register(' surfaceIntegrationTest' ) {
411581 group = ' verification'
0 commit comments