@@ -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 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
304466def 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
395568def 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+ }
396580def 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
398588def surfaceIntegrationTest = tasks. register(' surfaceIntegrationTest' ) {
399589 group = ' verification'
0 commit comments