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