@@ -229,6 +229,168 @@ tasks.named('javadoc', Javadoc).configure {
229229
230230tasks. named(' test' , Test ). configure {
231231 useJUnitPlatform()
232+ // Unit tests inspect target files relative to the checkout, but they do
233+ // not need Forge's rolling runtime files. A console-only test logger keeps
234+ // them from contending with Eclipse/client logs in this working directory.
235+ systemProperty ' log4j.configurationFile' , file(' src/test/resources/log4j2-test.xml' ). absolutePath
236+ // Loaded only through an isolated URLClassLoader by the parity test. This
237+ // is deliberately not a Gradle dependency and cannot leak into Eclipse or
238+ // a published OreSpawn jar.
239+ File mineralogy5Oracle = file(' ../../MinecraftMineralogy 118/MinecraftMineralogy/build/libs/Mineralogy-1.18.2-5.4.0.jar' )
240+ if (mineralogy5Oracle. isFile()) {
241+ systemProperty ' orespawn.mineralogy5Oracle' , mineralogy5Oracle. absolutePath
242+ }
243+ }
244+
245+ // Several registry-focused tests initialize the real global config singleton.
246+ // Keep that target-native coverage without creating or changing a developer's
247+ // checkout config as a side effect of `test` or `build`.
248+ def unitTestWorldgenConfig = file(' config/orespawn-worldgen.json' )
249+ def unitTestWorldgenConfigWasPresent = false
250+ byte [] unitTestWorldgenConfigBytes = null
251+ tasks. named(' test' , Test ). configure {
252+ doFirst {
253+ unitTestWorldgenConfigWasPresent = unitTestWorldgenConfig. isFile()
254+ unitTestWorldgenConfigBytes = unitTestWorldgenConfigWasPresent
255+ ? unitTestWorldgenConfig. bytes : null
256+ }
257+ }
258+ def preserveDeveloperWorldgenConfig = tasks. register(' preserveDeveloperWorldgenConfig' ) {
259+ doLast {
260+ if (unitTestWorldgenConfigWasPresent) {
261+ byte [] after = unitTestWorldgenConfig. isFile() ? unitTestWorldgenConfig. bytes : null
262+ if (after == null || ! java.util.Arrays . equals(unitTestWorldgenConfigBytes, after)) {
263+ unitTestWorldgenConfig. parentFile. mkdirs()
264+ unitTestWorldgenConfig. bytes = unitTestWorldgenConfigBytes
265+ throw new GradleException (' Unit tests changed config/orespawn-worldgen.json; the original was restored' )
266+ }
267+ } else if (unitTestWorldgenConfig. isFile()) {
268+ delete unitTestWorldgenConfig
269+ }
270+ }
271+ }
272+ tasks. named(' test' ) {
273+ finalizedBy preserveDeveloperWorldgenConfig
274+ }
275+
276+ // A Forge process is not green merely because it returns exit code zero. The
277+ // loader can log a worldgen/linkage failure and still shut down normally.
278+ def acceptedForge40LogNoise = [
279+ ~/ FML appears to be missing any signature data/ ,
280+ ~/ Found multiple arguments for option fml\. mcVersion/ ,
281+ ~/ Found multiple arguments for option fml\. forgeVersion/ ,
282+ ~/ \/ FATAL\] \[ net\. minecraftforge\. common\. ForgeConfig\/ CORE\] : Forge config just got changed on the file system!$/ ,
283+ ~/ \/ FATAL\] \[ net\. minecraftforge\. fml\. packs\. ModFileResourcePack\/\] : Failed to clean up tempdir /
284+ ]
285+
286+ def runtimeCrashSnapshot = { File runDirectory ->
287+ File crashDirectory = new File (runDirectory, ' crash-reports' )
288+ if (! crashDirectory. isDirectory()) return [] as Set
289+ return fileTree(crashDirectory) { include ' **/*' }. files
290+ .findAll { it. isFile() }. collect { it. absolutePath } as Set
291+ }
292+
293+ def assertRuntimeLogsClean = { File runDirectory , String context , Set priorCrashes ->
294+ File crashDirectory = new File (runDirectory, ' crash-reports' )
295+ if (crashDirectory. isDirectory()) {
296+ def crashes = fileTree(crashDirectory) { include ' **/*' }. files
297+ .findAll { it. isFile() && ! priorCrashes. contains(it. absolutePath) }
298+ if (! crashes. isEmpty()) {
299+ throw new GradleException (" ${ context} produced crash report ${ crashes.first()} " )
300+ }
301+ }
302+
303+ File logsDirectory = new File (runDirectory, ' logs' )
304+ if (! logsDirectory. isDirectory()) return
305+ def failures = []
306+ [new File (logsDirectory, ' latest.log' ), new File (logsDirectory, ' debug.log' )]
307+ .findAll { it. isFile() }. each { File log ->
308+ int lineNumber = 0
309+ log. eachLine(' UTF-8' ) { String line ->
310+ lineNumber++
311+ boolean unexpectedSeverity = line ==~ / .*\/ (?:ERROR|FATAL)\] .*/
312+ boolean knownNoise = acceptedForge40LogNoise. any { line =~ it }
313+ boolean fatalText = line. contains(' Encountered an unexpected exception' ) ||
314+ line. contains(' Exception stopping the server' ) ||
315+ line. contains(' Migration audit failed' ) ||
316+ line. contains(' java.lang.Error:' ) ||
317+ line. contains(' NoSuchMethodError' ) ||
318+ line. contains(' NoClassDefFoundError' ) ||
319+ line. contains(' ExceptionInInitializerError' ) ||
320+ line. contains(' Tried to assign a mutable BlockPos' ) ||
321+ line. contains(' causing cascading worldgen lag' )
322+ if ((unexpectedSeverity && ! knownNoise) || fatalText) {
323+ failures. add(" ${ log.name} :${ lineNumber} : ${ line} " )
324+ }
325+ }
326+ }
327+ if (! failures. isEmpty()) {
328+ throw new GradleException (" ${ context} logged unexpected errors:\n "
329+ + failures. take(20 ). join(' \n ' ))
330+ }
331+ }
332+
333+ task runtimeLogScannerTest {
334+ group = ' verification'
335+ description = ' Proves runtime log validation accepts documented Forge noise and rejects real failures.'
336+ doLast {
337+ File probe = file(" ${ buildDir} /runtime-log-scanner-test" )
338+ delete probe
339+ File logs = new File (probe, ' logs' ); logs. mkdirs()
340+ new File (logs, ' latest.log' ). setText(
341+ ' [main/ERROR] [FML]: FML appears to be missing any signature data\n '
342+ + ' [Server thread/INFO] [FML]: Done\n ' , ' UTF-8' )
343+ assertRuntimeLogsClean(probe, ' scanner-accepted-noise-probe' , [] as Set )
344+ new File (logs, ' latest.log' ). setText(
345+ ' [Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n ' , ' UTF-8' )
346+ boolean rejected = false
347+ try { assertRuntimeLogsClean(probe, ' scanner-mutable-position-probe' , [] as Set ) }
348+ catch (GradleException expected) { rejected = true }
349+ if (! rejected) throw new GradleException (' Runtime log scanner accepted a mutable BlockPos leak' )
350+ new File (logs, ' latest.log' ). setText(
351+ ' [Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n ' , ' UTF-8' )
352+ rejected = false
353+ try { assertRuntimeLogsClean(probe, ' scanner-cascading-probe' , [] as Set ) }
354+ catch (GradleException expected) { rejected = true }
355+ if (! rejected) throw new GradleException (' Runtime log scanner accepted cascading worldgen' )
356+ new File (logs, ' latest.log' ). setText(
357+ ' [Server thread/ERROR] [example]: Unexpected fixture failure\n ' , ' UTF-8' )
358+ rejected = false
359+ try { assertRuntimeLogsClean(probe, ' scanner-severity-probe' , [] as Set ) }
360+ catch (GradleException expected) { rejected = true }
361+ if (! rejected) throw new GradleException (' Runtime log scanner accepted an unexpected ERROR line' )
362+ delete probe
363+ }
364+ }
365+
366+ check. dependsOn runtimeLogScannerTest
367+
368+ task verifyMineralogyOracleIsolation {
369+ group = ' verification'
370+ description = ' Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
371+ doLast {
372+ configurations. each { configuration ->
373+ if (configuration. canBeResolved &&
374+ configuration. files. any { it. name ==~ / Mineralogy-.*\. jar/ }) {
375+ throw new GradleException (" Mineralogy oracle leaked into Gradle configuration ${ configuration.name} " )
376+ }
377+ }
378+ }
379+ }
380+
381+ check. dependsOn verifyMineralogyOracleIsolation
382+
383+ [' runClient' , ' runServer' , ' runData' ]. each { String taskName ->
384+ tasks. matching { it. name == taskName }. all { JavaExec runTask ->
385+ doFirst {
386+ new File (runTask. workingDir, ' mods' ). mkdirs()
387+ runTask. ext. oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask. workingDir)
388+ }
389+ doLast {
390+ assertRuntimeLogsClean(runTask. workingDir, taskName,
391+ runTask. ext. oreSpawnCrashSnapshot as Set )
392+ }
393+ }
232394}
233395
234396def surfaceIntegrationClasses = layout. buildDirectory. dir(' surface-integration-fixture/classes' )
@@ -273,8 +435,16 @@ tasks.configureEach {
273435 } else if (name == ' runSurfaceIntegrationReload' ) {
274436 dependsOn ' runSurfaceIntegrationFresh'
275437 }
438+ if (name == ' runSurfaceIntegrationFresh' || name == ' runSurfaceIntegrationReload' ) {
439+ doFirst {
440+ ext. oreSpawnCrashSnapshot = runtimeCrashSnapshot(workingDir)
441+ }
442+ doLast {
443+ assertRuntimeLogsClean(workingDir, " Forge 64 ${ name} " ,
444+ ext. oreSpawnCrashSnapshot as Set )
445+ }
446+ }
276447}
277-
278448def surfaceIntegrationTest = tasks. register(' surfaceIntegrationTest' ) {
279449 group = ' verification'
280450 description = ' Verifies provider surfaces and dynamic-biome geology across fresh and reloaded normal terrain.'
0 commit comments