@@ -200,6 +200,133 @@ javadoc {
200200
201201test {
202202 useJUnitPlatform()
203+ // Loaded only through an isolated URLClassLoader by the parity test. This
204+ // is deliberately not a Gradle dependency and cannot leak into Eclipse or
205+ // a published OreSpawn jar.
206+ File mineralogy5Oracle = file(' ../../MinecraftMineralogy 114-new/MinecraftMineralogy/build/libs/Mineralogy-1.14.4-5.0.1.jar' )
207+ if (mineralogy5Oracle. isFile()) {
208+ systemProperty ' orespawn.mineralogy5Oracle' , mineralogy5Oracle. absolutePath
209+ }
210+ }
211+
212+ // A Forge process is not green merely because it returns exit code zero. The
213+ // loader can log a worldgen/linkage failure and still shut down normally.
214+ def acceptedForge28LogNoise = [
215+ ~/ FML appears to be missing any signature data/ ,
216+ ~/ Found multiple arguments for option fml\. mcVersion/ ,
217+ ~/ Found multiple arguments for option fml\. forgeVersion/ ,
218+ ~/ \/ FATAL\] \[ net\. minecraftforge\. common\. ForgeConfig\/ CORE\] : Forge config just got changed on the file system!$/ ,
219+ ~/ \/ FATAL\] \[ net\. minecraftforge\. fml\. packs\. ModFileResourcePack\/\] : Failed to clean up tempdir /
220+ ]
221+
222+ def runtimeCrashSnapshot = { File runDirectory ->
223+ File crashDirectory = new File (runDirectory, ' crash-reports' )
224+ if (! crashDirectory. isDirectory()) return [] as Set
225+ return fileTree(crashDirectory) { include ' **/*' }. files
226+ .findAll { it. isFile() }. collect { it. absolutePath } as Set
227+ }
228+
229+ def assertRuntimeLogsClean = { File runDirectory , String context , Set priorCrashes ->
230+ File crashDirectory = new File (runDirectory, ' crash-reports' )
231+ if (crashDirectory. isDirectory()) {
232+ def crashes = fileTree(crashDirectory) { include ' **/*' }. files
233+ .findAll { it. isFile() && ! priorCrashes. contains(it. absolutePath) }
234+ if (! crashes. isEmpty()) {
235+ throw new GradleException (" ${ context} produced crash report ${ crashes.first()} " )
236+ }
237+ }
238+
239+ File logsDirectory = new File (runDirectory, ' logs' )
240+ if (! logsDirectory. isDirectory()) return
241+ def failures = []
242+ [new File (logsDirectory, ' latest.log' ), new File (logsDirectory, ' debug.log' )]
243+ .findAll { it. isFile() }. each { File log ->
244+ int lineNumber = 0
245+ log. eachLine(' UTF-8' ) { String line ->
246+ lineNumber++
247+ boolean unexpectedSeverity = line ==~ / .*\/ (?:ERROR|FATAL)\] .*/
248+ boolean knownNoise = acceptedForge28LogNoise. any { line =~ it }
249+ boolean fatalText = line. contains(' Encountered an unexpected exception' ) ||
250+ line. contains(' Exception stopping the server' ) ||
251+ line. contains(' Migration audit failed' ) ||
252+ line. contains(' java.lang.Error:' ) ||
253+ line. contains(' NoSuchMethodError' ) ||
254+ line. contains(' NoClassDefFoundError' ) ||
255+ line. contains(' ExceptionInInitializerError' ) ||
256+ line. contains(' Tried to assign a mutable BlockPos' ) ||
257+ line. contains(' causing cascading worldgen lag' )
258+ if ((unexpectedSeverity && ! knownNoise) || fatalText) {
259+ failures. add(" ${ log.name} :${ lineNumber} : ${ line} " )
260+ }
261+ }
262+ }
263+ if (! failures. isEmpty()) {
264+ throw new GradleException (" ${ context} logged unexpected errors:\n "
265+ + failures. take(20 ). join(' \n ' ))
266+ }
267+ }
268+
269+ task runtimeLogScannerTest {
270+ group = ' verification'
271+ description = ' Proves runtime log validation accepts documented Forge noise and rejects real failures.'
272+ doLast {
273+ File probe = file(" ${ buildDir} /runtime-log-scanner-test" )
274+ delete probe
275+ File logs = new File (probe, ' logs' ); logs. mkdirs()
276+ new File (logs, ' latest.log' ). setText(
277+ ' [main/ERROR] [FML]: FML appears to be missing any signature data\n '
278+ + ' [Server thread/INFO] [FML]: Done\n ' , ' UTF-8' )
279+ assertRuntimeLogsClean(probe, ' scanner-accepted-noise-probe' , [] as Set )
280+ new File (logs, ' latest.log' ). setText(
281+ ' [Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n ' , ' UTF-8' )
282+ boolean rejected = false
283+ try { assertRuntimeLogsClean(probe, ' scanner-mutable-position-probe' , [] as Set ) }
284+ catch (GradleException expected) { rejected = true }
285+ if (! rejected) throw new GradleException (' Runtime log scanner accepted a mutable BlockPos leak' )
286+ new File (logs, ' latest.log' ). setText(
287+ ' [Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n ' , ' UTF-8' )
288+ rejected = false
289+ try { assertRuntimeLogsClean(probe, ' scanner-cascading-probe' , [] as Set ) }
290+ catch (GradleException expected) { rejected = true }
291+ if (! rejected) throw new GradleException (' Runtime log scanner accepted cascading worldgen' )
292+ new File (logs, ' latest.log' ). setText(
293+ ' [Server thread/ERROR] [example]: Unexpected fixture failure\n ' , ' UTF-8' )
294+ rejected = false
295+ try { assertRuntimeLogsClean(probe, ' scanner-severity-probe' , [] as Set ) }
296+ catch (GradleException expected) { rejected = true }
297+ if (! rejected) throw new GradleException (' Runtime log scanner accepted an unexpected ERROR line' )
298+ delete probe
299+ }
300+ }
301+
302+ check. dependsOn runtimeLogScannerTest
303+
304+ task verifyMineralogyOracleIsolation {
305+ group = ' verification'
306+ description = ' Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
307+ doLast {
308+ configurations. each { configuration ->
309+ if (configuration. canBeResolved &&
310+ configuration. files. any { it. name ==~ / Mineralogy-.*\. jar/ }) {
311+ throw new GradleException (" Mineralogy oracle leaked into Gradle configuration ${ configuration.name} " )
312+ }
313+ }
314+ }
315+ }
316+
317+ check. dependsOn verifyMineralogyOracleIsolation
318+
319+ [' runClient' , ' runServer' , ' runData' ]. each { String taskName ->
320+ tasks. matching { it. name == taskName }. all { JavaExec runTask ->
321+ doFirst {
322+ new File (runTask. workingDir, ' mods' ). mkdirs()
323+ runTask. ext. oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask. workingDir)
324+ }
325+ doLast {
326+ assertRuntimeLogsClean(runTask. workingDir, taskName,
327+ runTask. ext. oreSpawnCrashSnapshot as Set )
328+ }
329+ }
203330}
204331
205332def surfaceIntegrationClasses = file(" ${ buildDir} /surface-integration-fixture/classes" )
@@ -291,8 +418,14 @@ surfaceIntegrationFreshProcess.doLast {
291418 if (! marker. isFile()) {
292419 throw new GradleException (" Fresh surface integration completion marker is missing: ${ marker} " )
293420 }
421+ assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
422+ ' surface integration fresh phase' , [] as Set )
294423}
295424def surfaceIntegrationReloadProcess = createSurfaceProcess(' Reload' , surfaceIntegrationFreshProcess)
425+ surfaceIntegrationReloadProcess. doLast {
426+ assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
427+ ' surface integration reload phase' , [] as Set )
428+ }
296429
297430task surfaceIntegrationTest (dependsOn : surfaceIntegrationReloadProcess) {
298431 group = ' verification'
@@ -341,6 +474,11 @@ task syncForge28EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
341474 / < mapEntry key= " MOD_CLASSES" value= " [^" ]* " \/ >/,
342475 java.util.regex.Matcher.quoteReplacement(
343476 " < mapEntry key= \" MOD_CLASSES\" value=\" ${ ordinaryModClasses} \" />" ))
477+ if (! text. contains(' org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' )) {
478+ text = text. replace(' </launchConfiguration>' ,
479+ ' <booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>\r\n ' +
480+ ' </launchConfiguration>' )
481+ }
344482 launch. setText(text, ' UTF-8' )
345483 }
346484 [' Fresh' , ' Reload' ]. each { String phase ->
@@ -365,6 +503,22 @@ task syncForge28EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
365503 }
366504}
367505
506+ task verifyForge28EclipseLaunchIsolation {
507+ group = ' verification'
508+ doLast {
509+ [' runClient.launch' , ' runServer.launch' , ' runData.launch' ]. each { String launchName ->
510+ File launch = file(launchName)
511+ String launchText = launch. getText(' UTF-8' )
512+ if (launchText. contains(' Mineralogy-' ) ||
513+ ! launchText. contains(' org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE' )) {
514+ throw new GradleException (" Ordinary Eclipse launch is not isolated from test oracles: ${ launch} " )
515+ }
516+ }
517+ }
518+ }
519+
520+ syncForge28EclipseLaunches. finalizedBy verifyForge28EclipseLaunchIsolation
521+
368522tasks. matching { it. name == ' genEclipseRuns' }. all {
369523 finalizedBy syncForge28EclipseLaunches
370524}
0 commit comments