@@ -200,6 +200,137 @@ javadoc {
200200
201201test {
202202 useJUnitPlatform()
203+ // Unit tests inspect target files relative to the checkout, but they do
204+ // not need Forge's rolling runtime files. A console-only test logger keeps
205+ // them from contending with Eclipse/client logs in this working directory.
206+ systemProperty ' log4j.configurationFile' , file(' src/test/resources/log4j2-test.xml' ). absolutePath
207+ // Loaded only through an isolated URLClassLoader by the parity test. This
208+ // is deliberately not a Gradle dependency and cannot leak into Eclipse or
209+ // a published OreSpawn jar.
210+ File mineralogy5Oracle = file(' ../../MinecraftMineralogy 115/MinecraftMineralogy/build/libs/Mineralogy-1.15.2-5.1.1.jar' )
211+ if (mineralogy5Oracle. isFile()) {
212+ systemProperty ' orespawn.mineralogy5Oracle' , mineralogy5Oracle. absolutePath
213+ }
214+ }
215+
216+ // A Forge process is not green merely because it returns exit code zero. The
217+ // loader can log a worldgen/linkage failure and still shut down normally.
218+ def acceptedForge31LogNoise = [
219+ ~/ FML appears to be missing any signature data/ ,
220+ ~/ Found multiple arguments for option fml\. mcVersion/ ,
221+ ~/ Found multiple arguments for option fml\. forgeVersion/ ,
222+ ~/ \/ FATAL\] \[ net\. minecraftforge\. common\. ForgeConfig\/ CORE\] : Forge config just got changed on the file system!$/ ,
223+ ~/ \/ FATAL\] \[ net\. minecraftforge\. fml\. packs\. ModFileResourcePack\/\] : Failed to clean up tempdir /
224+ ]
225+
226+ def runtimeCrashSnapshot = { File runDirectory ->
227+ File crashDirectory = new File (runDirectory, ' crash-reports' )
228+ if (! crashDirectory. isDirectory()) return [] as Set
229+ return fileTree(crashDirectory) { include ' **/*' }. files
230+ .findAll { it. isFile() }. collect { it. absolutePath } as Set
231+ }
232+
233+ def assertRuntimeLogsClean = { File runDirectory , String context , Set priorCrashes ->
234+ File crashDirectory = new File (runDirectory, ' crash-reports' )
235+ if (crashDirectory. isDirectory()) {
236+ def crashes = fileTree(crashDirectory) { include ' **/*' }. files
237+ .findAll { it. isFile() && ! priorCrashes. contains(it. absolutePath) }
238+ if (! crashes. isEmpty()) {
239+ throw new GradleException (" ${ context} produced crash report ${ crashes.first()} " )
240+ }
241+ }
242+
243+ File logsDirectory = new File (runDirectory, ' logs' )
244+ if (! logsDirectory. isDirectory()) return
245+ def failures = []
246+ [new File (logsDirectory, ' latest.log' ), new File (logsDirectory, ' debug.log' )]
247+ .findAll { it. isFile() }. each { File log ->
248+ int lineNumber = 0
249+ log. eachLine(' UTF-8' ) { String line ->
250+ lineNumber++
251+ boolean unexpectedSeverity = line ==~ / .*\/ (?:ERROR|FATAL)\] .*/
252+ boolean knownNoise = acceptedForge31LogNoise. any { line =~ it }
253+ boolean fatalText = line. contains(' Encountered an unexpected exception' ) ||
254+ line. contains(' Exception stopping the server' ) ||
255+ line. contains(' Migration audit failed' ) ||
256+ line. contains(' java.lang.Error:' ) ||
257+ line. contains(' NoSuchMethodError' ) ||
258+ line. contains(' NoClassDefFoundError' ) ||
259+ line. contains(' ExceptionInInitializerError' ) ||
260+ line. contains(' Tried to assign a mutable BlockPos' ) ||
261+ line. contains(' causing cascading worldgen lag' )
262+ if ((unexpectedSeverity && ! knownNoise) || fatalText) {
263+ failures. add(" ${ log.name} :${ lineNumber} : ${ line} " )
264+ }
265+ }
266+ }
267+ if (! failures. isEmpty()) {
268+ throw new GradleException (" ${ context} logged unexpected errors:\n "
269+ + failures. take(20 ). join(' \n ' ))
270+ }
271+ }
272+
273+ task runtimeLogScannerTest {
274+ group = ' verification'
275+ description = ' Proves runtime log validation accepts documented Forge noise and rejects real failures.'
276+ doLast {
277+ File probe = file(" ${ buildDir} /runtime-log-scanner-test" )
278+ delete probe
279+ File logs = new File (probe, ' logs' ); logs. mkdirs()
280+ new File (logs, ' latest.log' ). setText(
281+ ' [main/ERROR] [FML]: FML appears to be missing any signature data\n '
282+ + ' [Server thread/INFO] [FML]: Done\n ' , ' UTF-8' )
283+ assertRuntimeLogsClean(probe, ' scanner-accepted-noise-probe' , [] as Set )
284+ new File (logs, ' latest.log' ). setText(
285+ ' [Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n ' , ' UTF-8' )
286+ boolean rejected = false
287+ try { assertRuntimeLogsClean(probe, ' scanner-mutable-position-probe' , [] as Set ) }
288+ catch (GradleException expected) { rejected = true }
289+ if (! rejected) throw new GradleException (' Runtime log scanner accepted a mutable BlockPos leak' )
290+ new File (logs, ' latest.log' ). setText(
291+ ' [Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n ' , ' UTF-8' )
292+ rejected = false
293+ try { assertRuntimeLogsClean(probe, ' scanner-cascading-probe' , [] as Set ) }
294+ catch (GradleException expected) { rejected = true }
295+ if (! rejected) throw new GradleException (' Runtime log scanner accepted cascading worldgen' )
296+ new File (logs, ' latest.log' ). setText(
297+ ' [Server thread/ERROR] [example]: Unexpected fixture failure\n ' , ' UTF-8' )
298+ rejected = false
299+ try { assertRuntimeLogsClean(probe, ' scanner-severity-probe' , [] as Set ) }
300+ catch (GradleException expected) { rejected = true }
301+ if (! rejected) throw new GradleException (' Runtime log scanner accepted an unexpected ERROR line' )
302+ delete probe
303+ }
304+ }
305+
306+ check. dependsOn runtimeLogScannerTest
307+
308+ task verifyMineralogyOracleIsolation {
309+ group = ' verification'
310+ description = ' Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
311+ doLast {
312+ configurations. each { configuration ->
313+ if (configuration. canBeResolved &&
314+ configuration. files. any { it. name ==~ / Mineralogy-.*\. jar/ }) {
315+ throw new GradleException (" Mineralogy oracle leaked into Gradle configuration ${ configuration.name} " )
316+ }
317+ }
318+ }
319+ }
320+
321+ check. dependsOn verifyMineralogyOracleIsolation
322+
323+ [' runClient' , ' runServer' , ' runData' ]. each { String taskName ->
324+ tasks. matching { it. name == taskName }. all { JavaExec runTask ->
325+ doFirst {
326+ new File (runTask. workingDir, ' mods' ). mkdirs()
327+ runTask. ext. oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask. workingDir)
328+ }
329+ doLast {
330+ assertRuntimeLogsClean(runTask. workingDir, taskName,
331+ runTask. ext. oreSpawnCrashSnapshot as Set )
332+ }
333+ }
203334}
204335
205336def surfaceIntegrationClasses = file(" ${ buildDir} /surface-integration-fixture/classes" )
@@ -291,8 +422,14 @@ surfaceIntegrationFreshProcess.doLast {
291422 if (! marker. isFile()) {
292423 throw new GradleException (" Fresh surface integration completion marker is missing: ${ marker} " )
293424 }
425+ assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
426+ ' surface integration fresh phase' , [] as Set )
294427}
295428def surfaceIntegrationReloadProcess = createSurfaceProcess(' Reload' , surfaceIntegrationFreshProcess)
429+ surfaceIntegrationReloadProcess. doLast {
430+ assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
431+ ' surface integration reload phase' , [] as Set )
432+ }
296433
297434task surfaceIntegrationTest (dependsOn : surfaceIntegrationReloadProcess) {
298435 group = ' verification'
@@ -327,6 +464,19 @@ task syncForge31EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
327464 " <mapEntry key=\" FORGE_GROUP\" value=\" net.minecraftforge\" />\r\n " +
328465 " <mapEntry key=\" target\" value=\" fmluserdevserver\" />\r\n " +
329466 " <mapEntry key=\" MC_VERSION\" value=\" ${ minecraft_version} \" />\r\n "
467+ [' Client' , ' Server' , ' Data' ]. each { String runName ->
468+ File launch = file(" run${ runName} .launch" )
469+ if (! launch. isFile()) {
470+ throw new GradleException (" Missing generated Eclipse launch: ${ launch} " )
471+ }
472+ String text = launch. getText(' UTF-8' )
473+ if (! text. contains(' org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' )) {
474+ text = text. replace(' </launchConfiguration>' ,
475+ ' <booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>\r\n ' +
476+ ' </launchConfiguration>' )
477+ }
478+ launch. setText(text, ' UTF-8' )
479+ }
330480 [' Fresh' , ' Reload' ]. each { String phase ->
331481 File launch = file(" runSurfaceIntegration${ phase} .launch" )
332482 if (! launch. isFile()) {
@@ -349,6 +499,22 @@ task syncForge31EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
349499 }
350500}
351501
502+ task verifyForge31EclipseLaunchIsolation {
503+ group = ' verification'
504+ doLast {
505+ [' runClient.launch' , ' runServer.launch' , ' runData.launch' ]. each { String launchName ->
506+ File launch = file(launchName)
507+ String launchText = launch. getText(' UTF-8' )
508+ if (launchText. contains(' Mineralogy-' ) || launchText. contains(' biomeIntegrationTest' ) ||
509+ ! launchText. contains(' org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' )) {
510+ throw new GradleException (" Ordinary Eclipse launch is not isolated from test oracles: ${ launch} " )
511+ }
512+ }
513+ }
514+ }
515+
516+ syncForge31EclipseLaunches. finalizedBy verifyForge31EclipseLaunchIsolation
517+
352518tasks. matching { it. name == ' genEclipseRuns' }. all {
353519 finalizedBy syncForge31EclipseLaunches
354520}
0 commit comments