mirror of
https://github.com/GTNewHorizons/ForgeMultipart.git
synced 2026-10-26 14:24:01 +00:00
ci: report test counts and keep the run log clean
Upload functional test reports, JUnit XML and server logs on every run so a CI failure has something to read beyond aggregate counts. Print unit and functional test summaries, naming each failed functional test. Repackage the JUnit stack into the test mod jar without module descriptors so FML stops logging a LoaderException per jar while scanning the classpath, and silence the two ERROR lines that passing tests drive on purpose. Refresh the handoff counts and record which migration documents are deleted when the port closes.
This commit is contained in:
1 parent
6a28fff946
commit
78be94b0b3
8 files changed
+132
-7
No files matched your search
@@ -46,3 +46,16 @@ jobs:
|
||||
|
||||
- name: Run Forge functional tests
|
||||
run: ./gradlew --build-cache --stacktrace runFunctionalTestServer
|
||||
|
||||
# The server runs out of process; without this a failure leaves only the counts.
|
||||
- name: Upload functional test reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: forge-functional-test-reports
|
||||
path: |
|
||||
run/server/junit-out/
|
||||
run/server/logs/
|
||||
run/server/crash-reports/
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
+39
-3
@@ -26,8 +26,25 @@ configurations {
|
||||
}
|
||||
}
|
||||
|
||||
// FML scans classpath jars for @Mod with an ASM that predates Java 9, so every JUnit jar logs a LoaderException
|
||||
// stack trace for its module-info. Repackage them into the test mod jar and keep the originals off the classpath.
|
||||
def junitArtifactPrefixes = ["junit-jupiter", "junit-platform", "opentest4j", "apiguardian"]
|
||||
def isJunitArtifact = { File f -> junitArtifactPrefixes.any { f.name.startsWith(it) } }
|
||||
def functionalTestRuntime = configurations.named(functionalTestSet.runtimeClasspathConfigurationName)
|
||||
|
||||
tasks.register(functionalTestSet.jarTaskName, Jar) {
|
||||
from(functionalTestSet.output)
|
||||
from(functionalTestRuntime.map { cfg -> cfg.files.findAll(isJunitArtifact).collect { zipTree(it) } }) {
|
||||
// META-INF/versions is the Java 9+ half of the multi-release jars, never loaded on this Java 8 server.
|
||||
// Signatures do not survive repackaging; the engine is still found through META-INF/services.
|
||||
exclude(
|
||||
"module-info.class",
|
||||
"META-INF/versions/**",
|
||||
"META-INF/*.SF",
|
||||
"META-INF/*.DSA",
|
||||
"META-INF/*.RSA")
|
||||
}
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
archiveClassifier.set("functionalTests")
|
||||
archiveVersion.set("1.0")
|
||||
destinationDirectory.set(layout.buildDirectory.dir("tmp"))
|
||||
@@ -64,7 +81,8 @@ def functionalTestProfileResults = layout.projectDirectory.file("run/server/forg
|
||||
def addFunctionalTestsToRun = { JavaExec task ->
|
||||
task.dependsOn(functionalTestSet.jarTaskName)
|
||||
task.classpath(
|
||||
configurations.named(functionalTestSet.runtimeClasspathConfigurationName),
|
||||
// JUnit is repackaged into the test mod jar above.
|
||||
functionalTestRuntime.map { cfg -> cfg.files.findAll { !isJunitArtifact(it) } },
|
||||
tasks.named(functionalTestSet.jarTaskName))
|
||||
}
|
||||
|
||||
@@ -92,15 +110,33 @@ tasks.register("runFunctionalTestServer", GradleBuild) {
|
||||
int tests = 0
|
||||
int failures = 0
|
||||
int errors = 0
|
||||
int skipped = 0
|
||||
def failed = []
|
||||
reports.each { report ->
|
||||
def suite = new XmlSlurper(false, false).parse(report)
|
||||
tests += suite.@tests.toInteger()
|
||||
failures += suite.@failures.toInteger()
|
||||
errors += suite.@errors.toInteger()
|
||||
skipped += suite.@skipped.toString().isEmpty() ? 0 : suite.@skipped.toInteger()
|
||||
suite.testcase.each { testcase ->
|
||||
if (!testcase.failure.isEmpty() || !testcase.error.isEmpty()) {
|
||||
failed << "${testcase.@classname}.${testcase.@name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def summary = "Forge functional tests: ${tests} tests, ${tests - failures - errors - skipped} passed, " +
|
||||
"${failures} failures, ${errors} errors, ${skipped} skipped"
|
||||
logger.lifecycle(summary)
|
||||
failed.each { logger.lifecycle(" FAILED ${it}") }
|
||||
|
||||
def stepSummary = System.getenv("GITHUB_STEP_SUMMARY")
|
||||
if (stepSummary) {
|
||||
new File(stepSummary) << "### ${summary}\n" + failed.collect { "- FAILED `${it}`\n" }.join()
|
||||
}
|
||||
|
||||
if (tests == 0 || failures > 0 || errors > 0) {
|
||||
throw new GradleException(
|
||||
"ForgeMultipart functional tests failed: ${tests} tests, ${failures} failures, ${errors} errors")
|
||||
throw new GradleException("ForgeMultipart functional tests failed: ${summary}")
|
||||
}
|
||||
if (profileFunctionalTests
|
||||
&& (!functionalTestProfileRecording.asFile.isFile() || !functionalTestProfileResults.asFile.isFile())) {
|
||||
|
||||
@@ -6,6 +6,23 @@ plugins {
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("failed")
|
||||
exceptionFormat = "full"
|
||||
}
|
||||
// Gradle prints nothing on success, so a green run never says how much ran.
|
||||
afterSuite { desc, result ->
|
||||
if (desc.parent != null) {
|
||||
return
|
||||
}
|
||||
def summary = "Unit tests: ${result.testCount} tests, ${result.successfulTestCount} passed, " +
|
||||
"${result.failedTestCount} failed, ${result.skippedTestCount} skipped"
|
||||
logger.lifecycle(summary)
|
||||
def stepSummary = System.getenv("GITHUB_STEP_SUMMARY")
|
||||
if (stepSummary) {
|
||||
new File(stepSummary) << "### ${summary}\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scope modern syntax to these helpers; retained Scala and joint Java stay on Java 8.
|
||||
|
||||
@@ -16,8 +16,8 @@ migration checkout; `codex/tile-compatibility-fixes` was deleted after its fixes
|
||||
|
||||
## Current state and next target
|
||||
|
||||
**577 plain-JVM tests and 290 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **230 Java
|
||||
files and 9 Scala files / 747 nonblank Scala lines**. The packaged inventory has 451 classes.
|
||||
**581 plain-JVM tests and 291 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **231 Java
|
||||
files and 9 Scala files / 747 nonblank Scala lines**. The packaged inventory has 452 classes.
|
||||
|
||||
The agreed milestone order is the documented consumer-facing Java API, then consumer release and adoption, then final
|
||||
Scala removal; see the plan's API migration design and Phases 8-10. Per-change fixes, their regression cases and their
|
||||
|
||||
@@ -4,6 +4,23 @@ Status: living document
|
||||
Initial audit: 2026-08-14
|
||||
Audited revision: `f10595d` (`master`)
|
||||
|
||||
## Document disposition
|
||||
|
||||
`docs/migration/` is working material for the port, not permanent documentation. Phase language here describes the
|
||||
migration in progress; do not read it as the current state of the mod. These files are deleted once the migration
|
||||
closes:
|
||||
|
||||
| File | Disposition |
|
||||
| --- | --- |
|
||||
| `README.md` (this file) | Delete. Phase gates stop meaning anything once the phases close. |
|
||||
| `HANDOFF.md` | Delete. Working handoff, including branch and checkout instructions. |
|
||||
| `HISTORY.md` | Delete. Dated port diary; the narrative stays in git history. |
|
||||
| `COMPATIBILITY.md` | **Keep.** ABI inventory and consumer audit; `DIVERGENCES.md` requires checking it. |
|
||||
| `DIVERGENCES.md` | **Keep.** The durable compatibility ledger. |
|
||||
| `MANUAL_CHECKS.md` | **Keep.** Becomes the recurring client release checklist. |
|
||||
|
||||
`docs/api/`, `docs/API.md` and `docs/RELEASE_NOTES.md` are consumer-facing and unaffected.
|
||||
|
||||
## Decision summary
|
||||
|
||||
Converting ForgeMultipart to Java is realistic. It should be treated as a staged compatibility migration, not as an automatic source conversion or a ground-up rewrite.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package codechicken.multipart.test;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import codechicken.multipart.handler.MultipartProxy;
|
||||
|
||||
/**
|
||||
* Runs an action with ForgeMultipart's logger detached, for tests that deliberately drive a path which logs at ERROR.
|
||||
* Those lines are correct behaviour, but in a green run they read as failures.
|
||||
*
|
||||
* <p>
|
||||
* The swap is global and not thread safe; callers run on the server thread inside one synchronous call.
|
||||
*/
|
||||
final class ExpectedErrorLog {
|
||||
|
||||
/** A reflective proxy rather than a subclass, so it does not depend on Forge's log4j-api version. */
|
||||
private static final Logger SILENT = (Logger) Proxy
|
||||
.newProxyInstance(Logger.class.getClassLoader(), new Class<?>[] { Logger.class }, (proxy, method, args) -> {
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType == String.class) return "";
|
||||
if (returnType == void.class || !returnType.isPrimitive()) return null;
|
||||
// Boxed zero/false for any primitive return, without enumerating them.
|
||||
return Array.get(Array.newInstance(returnType, 1), 0);
|
||||
});
|
||||
|
||||
private ExpectedErrorLog() {}
|
||||
|
||||
static void silence(Runnable action) {
|
||||
Logger original = MultipartProxy.logger();
|
||||
MultipartProxy.logger_$eq(SILENT);
|
||||
try {
|
||||
action.run();
|
||||
} finally {
|
||||
MultipartProxy.logger_$eq(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -43,7 +43,8 @@ class MultipartCompatiblityFunctionalTest {
|
||||
Function4<World, Object, Object, Object, Object> replacement = new DenyPlacement();
|
||||
try {
|
||||
MultipartCompatiblity.canAddPart_$eq(replacement);
|
||||
assertDoesNotThrow(MCPCCompatModule::load);
|
||||
// The absent hook is reported at ERROR; that is the behaviour under test.
|
||||
ExpectedErrorLog.silence(() -> assertDoesNotThrow(MCPCCompatModule::load));
|
||||
assertSame(replacement, MultipartCompatiblity.canAddPart());
|
||||
} finally {
|
||||
MultipartCompatiblity.canAddPart_$eq(original);
|
||||
|
||||
+2
-1
@@ -59,7 +59,8 @@ public class MultipartGeneratorFunctionalTest {
|
||||
server,
|
||||
generator.codechicken$multipart$MultipartGenerator$$traitsForPart(new HierarchyPart(), false));
|
||||
|
||||
MultipartGenerator.registerTrait(root, slotted, redstone);
|
||||
// Deliberate duplicate: both sides log at ERROR and keep the first registration.
|
||||
ExpectedErrorLog.silence(() -> MultipartGenerator.registerTrait(root, slotted, redstone));
|
||||
assertEquals(slotted.replace('.', '/'), map(false).apply(root.replace('.', '/')));
|
||||
assertEquals(redstone.replace('.', '/'), map(true).apply(root.replace('.', '/')));
|
||||
MultipartGenerator.registerTrait(LateMarker.class.getName(), tileChange, null);
|
||||
|
||||
Reference in new issue
Block a user