This is an automated email from the ASF dual-hosted git repository.
ashishvijaywargiya pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new 82db69805a Retiring the legacy Ant-based JUnit Integration Tests
reports (#1603)
82db69805a is described below
commit 82db69805ae8623da9bb6f9af59808b05d175a4a
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Sat Aug 8 17:17:59 2026 +0530
Retiring the legacy Ant-based JUnit Integration Tests reports (#1603)
Retiring the legacy Ant-based createTestReports task and replacing it
with the new framed test report as the single source of truth for unit
test results.
Renamed the report tasks and output paths for clarity, and brought the
new report to full parity with the old one by adding global
fails/errors/skipped pages plus per-suite fails/errors pages.
Fixed stat cards on the per-suite status pages and tidied up leftover
report task cleanup logic.
Updated unit-tests.adoc to reflect the renamed test report paths.
Verified JUnit3 passthrough still works correctly alongside the new
report generation.
---
build.gradle | 13 +-
.../testtools/src/docs/asciidoc/unit-tests.adoc | 10 +-
test-reports.gradle | 216 +++++++++++++++------
3 files changed, 161 insertions(+), 78 deletions(-)
diff --git a/build.gradle b/build.gradle
index 55cf7fb0c9..9142a731e5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -253,9 +253,6 @@ subprojects {
}
configurations {
- junitReport {
- description = 'libraries needed to run junitreport for OFBiz unit
tests'
- }
ofbizPlugins {
description = 'ofbiz plugin dependencies configuration'
transitive = true
@@ -281,9 +278,6 @@ dependencies {
compileOnly project(path: subProject.path, configuration:
'pluginLibsCompileOnly')
}
- junitReport 'junit:junit:4.13.2'
- junitReport 'org.apache.ant:ant-junit:1.10.15'
-
// Libraries downloaded manually
implementation fileTree(dir: file("${rootDir}/lib"), include: '**/*.jar')
getDirectoryInActiveComponentsIfExists('lib').each { libDir ->
@@ -925,7 +919,7 @@ task generateAllPluginsDocumentation(group: docsGroup,
// ========== System Administration tasks ==========
-// createTestReports/createModernTestReport/createFramedTestReport moved to
test-reports.gradle
+// createTestReport/createFramedTestReport moved to test-reports.gradle
// (applied above) - kept out of this file since they're self-contained and
sizable.
task gitInfoFooter(group: sysadminGroup, description: 'Update the Git
Branch-revision info in the footer if Git is used') {
@@ -1269,7 +1263,7 @@ def createOfbizCommandTask(taskName, arguments) {
// ever overwrites the suites a run actually re-executes - nothing
here deletes the
// rest first. Left alone, a narrower run (suitename=... filtering
to one component) or
// a renamed/removed suite leaves other suites' XML sitting in
this directory from
- // whenever they last ran, and all three report tasks below just
glob whatever *.xml
+ // whenever they last ran, and the two report tasks below just
glob whatever *.xml
// files exist at generation time - so the report would silently
blend fresh results
// with stale ones from an earlier run into what looks like one
coherent run. Clearing
// the directory before the run starts guarantees every report
reflects exactly this
@@ -1277,8 +1271,7 @@ def createOfbizCommandTask(taskName, arguments) {
doFirst {
delete fileTree('./runtime/logs/test-results') { include
'*.xml' }
}
- finalizedBy(createTestReports)
- finalizedBy(createModernTestReport)
+ finalizedBy(createTestReport)
finalizedBy(createFramedTestReport)
} else {
classpath = sourceSets.main.runtimeClasspath
diff --git a/framework/testtools/src/docs/asciidoc/unit-tests.adoc
b/framework/testtools/src/docs/asciidoc/unit-tests.adoc
index 5977c897f1..803276673c 100644
--- a/framework/testtools/src/docs/asciidoc/unit-tests.adoc
+++ b/framework/testtools/src/docs/asciidoc/unit-tests.adoc
@@ -194,7 +194,9 @@ This error message will be in between starting and finished
test cass line like
----
== Test result
-After you run unit test, you can see the result of the testing. +
-If you use run-tests target, you can see test result web page by view
runtime/logs/test-results/html/index.html
-file in web browser and see JUnit Test Result files for each test suite in
runtime/logs/test-results directory. +
-If you use other target you only see JUnit Test Result file in
runtime/logs/test-results.
+After you run the tests, you can see the results in
`runtime/logs/test-results`: +
+Open `runtime/logs/test-results/html/index.html` in a web browser for a
navigable, multi-page
+report, or `runtime/logs/test-results/test-report.html` for the same results
as a single
+portable file. +
+The raw JUnit Test Result XML file for each test suite is also available
directly in
+`runtime/logs/test-results`.
diff --git a/test-reports.gradle b/test-reports.gradle
index 8c0ff004c1..c8b874c2fa 100644
--- a/test-reports.gradle
+++ b/test-reports.gradle
@@ -19,37 +19,15 @@
import groovy.xml.MarkupBuilder
import groovy.xml.XmlSlurper
-// createTestReports (classic Ant-based), createModernTestReport (single-page)
and
-// createFramedTestReport (navigable multi-page) all turn the JUnit suite XML
written to
-// runtime/logs/test-results into an HTML report - split out of build.gradle
into its own file
-// since it's self-contained and sizable, applied below so it still runs as
part of every Gradle
-// invocation exactly as if it were declared inline.
+// createTestReport (single-page) and createFramedTestReport (navigable
multi-page) turn the
+// JUnit suite XML written to runtime/logs/test-results into an HTML report -
split out of
+// build.gradle into its own file since it's self-contained and sizable,
applied below so it
+// still runs as part of every Gradle invocation exactly as if it were
declared inline.
def sysadminGroup = 'System Administration'
-task createTestReports(group: sysadminGroup, description: 'Generate HTML
reports from junit XML output') {
- doLast {
- ant.taskdef(name: 'junitreport',
- classname:
'org.apache.tools.ant.taskdefs.optional.junit.XMLResultAggregator',
- classpath: configurations.junitReport.asPath)
- ant.junitreport(todir: './runtime/logs/test-results') {
- fileset(dir: './runtime/logs/test-results') {
- include(name: '*.xml')
- }
- report(format:'frames', todir:'./runtime/logs/test-results/html')
- report(format:'noframes', todir:'./runtime/logs/test-results/html')
- }
- // Ant's stock junit-frames.xsl/junit-noframes.xsl label this column
"Type", but it actually
- // renders the failure/error message and stack trace, not the bare
exception type - relabel
- // it post-generation rather than vendoring and maintaining a forked
copy of Ant's templates.
- fileTree('./runtime/logs/test-results/html') {
- include '**/*.html'
- }.each { file -> file.text = file.text.replace('>Type</th>', '>Failure
Reason</th>') }
- }
-}
-
def modernReportCss() {
// Hand-rolled, not a real Bootstrap import: same default
palette/spacing/component shapes.
- // Used two ways: inlined into modern-report.html so that report stays one
portable file, and
+ // Used two ways: inlined into test-report.html so that report stays one
portable file, and
// written once as a shared junit-report.css alongside the framed report's
many pages (referenced via
// a same-directory relative <link>, so it's still a
zero-external-network-request file:// doc -
// see the 2026-08-07 "CSS/JS duplication" note in
junit5-improvements-august2026.md).
@@ -145,6 +123,7 @@ table.bs-table tbody tr.row-skipped { color:
var(--bs-secondary); }
pre { white-space: pre-wrap; margin: 0; font-size: 0.85em; }
.parse-error { color: var(--bs-danger); font-weight: bold; }
+.suite-status-links { margin: 0 0 1rem; font-size: 0.9rem; }
.nav-pane { padding: 0.75rem; }
.nav-section-label { font-size: 0.75rem; text-transform: uppercase;
letter-spacing: 0.03em; color: var(--bs-secondary); margin: 1rem 0 0.4rem; }
@@ -229,22 +208,22 @@ def renderStatCards(Map counts, Closure statusHrefFn,
List extraCards = [], Stri
html.div(wrapperAttrs) {
// Tests/Failures/Errors/Skipped are links (matching the classic
report's
// all-tests.html/alltests-fails.html/etc.) that filter the results
below down to just
- // that status, in-page or on a dedicated page depending on
statusHrefFn.
- div('class': 'stat-card') {
- a('class': 'stat-value', href: statusHrefFn('tests'), title: 'Show
all tests', "${counts.tests}")
- div('class': 'stat-label', 'Tests')
- }
- div('class': 'stat-card') {
- a('class': 'stat-value', href: statusHrefFn('failures'), title:
'Show only failures', "${counts.failures}")
- div('class': 'stat-label', 'Failures')
- }
- div('class': 'stat-card') {
- a('class': 'stat-value', href: statusHrefFn('errors'), title:
'Show only errors', "${counts.errors}")
- div('class': 'stat-label', 'Errors')
- }
- div('class': 'stat-card') {
- a('class': 'stat-value', href: statusHrefFn('skipped'), title:
'Show only skipped', "${counts.skipped}")
- div('class': 'stat-label', 'Skipped')
+ // that status, in-page or on a dedicated page depending on
statusHrefFn. A null
+ // statusHrefFn means "this page can't be filtered" - it renders the
same four cards as
+ // plain values instead, for the already-pre-filtered static status
pages, which carry
+ // neither the #filters block nor junit-report.js a "#status-*" href
would need.
+ [[status: 'tests', label: 'Tests', value: counts.tests, title: 'Show
all tests'],
+ [status: 'failures', label: 'Failures', value: counts.failures,
title: 'Show only failures'],
+ [status: 'errors', label: 'Errors', value: counts.errors, title:
'Show only errors'],
+ [status: 'skipped', label: 'Skipped', value: counts.skipped, title:
'Show only skipped']].each { card ->
+ div('class': 'stat-card') {
+ if (statusHrefFn) {
+ a('class': 'stat-value', href: statusHrefFn(card.status),
title: card.title, "${card.value}")
+ } else {
+ div('class': 'stat-value', "${card.value}")
+ }
+ div('class': 'stat-label', card.label)
+ }
}
div('class': 'stat-card') {
div('class': 'stat-value', counts.successRate)
@@ -269,7 +248,7 @@ def buildOverallSummaryTable(List suites, Closure
statusHrefFn) {
def toSeconds = { String s -> (s ==~ /-?[0-9]*\.?[0-9]+/) ? s.toDouble() :
0.0 }
// List.sum(Closure) returns null (not 0) on an empty list, unlike the
no-arg sum() - discovered
// live when a malformed CLI invocation purged runtime/logs/test-results/
(item 3's own fix) and
- // then failed before any suite ran, leaving
createModernTestReport/createFramedTestReport to
+ // then failed before any suite ran, leaving
createTestReport/createFramedTestReport to
// parse zero suites and crash on "null as int" instead of just rendering
"0 tests".
def totalTests = (suites.sum { toInt(it.tests) } ?: 0) as int
def totalFailures = (suites.sum { toInt(it.failures) } ?: 0) as int
@@ -313,7 +292,13 @@ def buildSummaryTable(List suites, Closure hrefFn) {
writer.toString()
}
-def buildSuiteDetailFragment(Map suite) {
+def buildSuiteDetailFragment(Map suite, boolean linked = true) {
+ // linked = true (the default) is the live, JS-filterable suite page: stat
cards link to
+ // "#status-*" and report the suite's own totals. linked = false is a
static status page
+ // (renderFramedSuiteStatusPage), whose `suite` has already had its
testcases filtered down to
+ // one status and which loads neither the #filters block nor
junit-report.js - there the cards
+ // must be plain values (a "#status-*" href would be a dead link) counted
off the filtered
+ // testcases actually rendered in the table below, not off the whole
suite's attributes.
def writer = new StringWriter()
def html = new MarkupBuilder(writer)
html.div {
@@ -326,16 +311,24 @@ def buildSuiteDetailFragment(Map suite) {
// summing needed) plus Time Stamp/Host as extraCards, meaningful
for one suite but not
// for a sum across many.
def toInt = { String s -> (s ==~ /\d+/) ? s.toInteger() : 0 }
- def tests = toInt(suite.tests)
- def failures = toInt(suite.failures)
- def errors = toInt(suite.errors)
+ def toSeconds = { String s -> (s ==~ /-?[0-9]*\.?[0-9]+/) ?
s.toDouble() : 0.0 }
+ def tests = linked ? toInt(suite.tests) : suite.testcases.size()
+ def failures = linked ? toInt(suite.failures) :
suite.testcases.count { it.status == 'fail' }
+ def errors = linked ? toInt(suite.errors) : suite.testcases.count
{ it.status == 'error' }
def successRate = tests > 0
? String.format('%.2f%%', ((tests - failures - errors) /
(double) tests) * 100)
: 'N/A'
- mkp.yieldUnescaped(renderStatCards(
- [tests: suite.tests, failures: suite.failures, errors:
suite.errors,
- skipped: suite.skipped, successRate: successRate, time:
suite.time],
- { status -> "#status-${status}" },
+ // Success rate and Time are derived from the same set of
testcases as the four counts
+ // above, so a filtered page's cards stay internally consistent
with its own table
+ // rather than mixing filtered counts with whole-suite figures.
+ def counts = linked
+ ? [tests: suite.tests, failures: suite.failures, errors:
suite.errors,
+ skipped: suite.skipped, successRate: successRate, time:
suite.time]
+ : [tests: tests, failures: failures, errors: errors,
+ skipped: suite.testcases.count { it.status == 'skipped'
}, successRate: successRate,
+ time: String.format('%.3f', (suite.testcases.sum {
toSeconds(it.time) } ?: 0.0) as double)]
+ def statusHrefFn = linked ? { String status -> "#status-${status}"
} : null
+ mkp.yieldUnescaped(renderStatCards(counts, statusHrefFn,
[[label: 'Time Stamp', value: suite.timestamp ?: 'N/A'],
[label: 'Host', value: suite.hostname ?: 'N/A']]))
table('class': 'detail bs-table') {
@@ -411,7 +404,7 @@ def renderFramedIndexPage() {
<body>
<p>This report is designed to be viewed using the frames feature. If you see
this
message, you are using a non-frame-capable web client — use
-<a href="../modern-report.html">modern-report.html</a> instead.</p>
+<a href="../test-report.html">test-report.html</a> instead.</p>
</body>
</noframes>
</frameset>
@@ -432,6 +425,9 @@ def renderFramedNavPage(List suites) {
ul('class': 'nav-list') {
li { a('class': 'nav-link', href: 'overview.html', target:
'detail', 'Overview') }
li { a('class': 'nav-link', href: 'all-tests.html', target:
'detail', 'All Tests') }
+ li { a('class': 'nav-link', href: 'all-tests-fails.html',
target: 'detail', 'All Failures') }
+ li { a('class': 'nav-link', href: 'all-tests-errors.html',
target: 'detail', 'All Errors') }
+ li { a('class': 'nav-link', href: 'all-tests-skipped.html',
target: 'detail', 'All Skipped') }
}
div('class': 'nav-section-label', 'Suites')
ul('class': 'nav-list') {
@@ -478,9 +474,21 @@ def filterControlsHtml() {
</div>'''
}
-def renderFramedSuitePage(Map suite) {
+def renderFramedSuiteStatusLinksHtml(boolean hasFailsPage, boolean
hasErrorsPage, String slug) {
+ def links = []
+ if (hasFailsPage) {
+ links << "<a href=\"suite-${slug}-fails.html\">Failures only</a>"
+ }
+ if (hasErrorsPage) {
+ links << "<a href=\"suite-${slug}-errors.html\">Errors only</a>"
+ }
+ links ? "<p class=\"suite-status-links\">${links.join(' | ')}</p>" : ''
+}
+
+def renderFramedSuitePage(Map suite, boolean hasFailsPage, boolean
hasErrorsPage) {
def detail = buildSuiteDetailFragment(suite)
def suiteNameHtml = escapeHtmlText(suite.name)
+ def statusLinks = renderFramedSuiteStatusLinksHtml(hasFailsPage,
hasErrorsPage, suiteSlug(suite.name))
"""<!DOCTYPE html>
<html>
<head>
@@ -491,6 +499,7 @@ def renderFramedSuitePage(Map suite) {
<body>
<div class="navbar-report"><h1>${suiteNameHtml}</h1></div>
<div class="container">
+${statusLinks}
${filterControlsHtml()}
${detail}
</div>
@@ -500,6 +509,33 @@ ${detail}
"""
}
+def renderFramedSuiteStatusPage(Map suite, String status, String statusLabel) {
+ // Per-suite equivalent of renderFramedStatusPage, generated only when the
suite actually has
+ // a test in that status - mirrors the old Ant report's per-class
-fails/-errors pages, which
+ // only existed for classes with a failure/error.
+ def filteredSuite = suite + [testcases: suite.testcases.findAll {
it.status == status }]
+ // linked = false: this page is already filtered and has no filter
controls or JS, so its stat
+ // cards render as plain values counted off filteredSuite.testcases - see
buildSuiteDetailFragment.
+ def detail = buildSuiteDetailFragment(filteredSuite, false)
+ def suiteNameHtml = escapeHtmlText(suite.name)
+ """<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Apache OFBiz Test Report: ${suiteNameHtml} - ${statusLabel}</title>
+<link rel="stylesheet" href="junit-report.css">
+</head>
+<body>
+<div class="navbar-report"><h1>${suiteNameHtml} - ${statusLabel}</h1></div>
+<div class="container">
+<p class="suite-status-links"><a
href="suite-${suiteSlug(suite.name)}.html">← Back to suite</a></p>
+${detail}
+</div>
+</body>
+</html>
+"""
+}
+
def buildAllTestsFragment(List suites) {
def writer = new StringWriter()
def html = new MarkupBuilder(writer)
@@ -524,6 +560,36 @@ def buildAllTestsFragment(List suites) {
writer.toString()
}
+def suitesFilteredByStatus(List suites, String status) {
+ // Keeps every suite (even ones with zero matches) so
buildAllTestsFragment's normal
+ // iteration just emits no rows for them - simpler than special-casing an
empty suite list,
+ // and matches how the unfiltered all-tests.html already handles a suite
with a parseError
+ // (skipped, not removed from the list).
+ suites.collect { s -> s.parseError ? s : s + [testcases:
s.testcases.findAll { it.status == status }] }
+}
+
+def renderFramedStatusPage(String status, String title, List suites) {
+ // Pre-filtered, static equivalent of all-tests.html's JS checkbox filter
- unlike that page,
+ // this has a stable URL and needs no JavaScript, matching the old Ant
report's
+ // alltests-fails.html/alltests-errors.html/alltests-skipped.html.
+ def detail = buildAllTestsFragment(suitesFilteredByStatus(suites, status))
+ """<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Apache OFBiz Test Report: ${title}</title>
+<link rel="stylesheet" href="junit-report.css">
+</head>
+<body>
+<div class="navbar-report"><h1>${title}</h1></div>
+<div class="container">
+${detail}
+</div>
+</body>
+</html>
+"""
+}
+
def renderFramedAllTestsPage(List suites) {
def detail = buildAllTestsFragment(suites)
"""<!DOCTYPE html>
@@ -549,16 +615,30 @@ def buildFramedReportFiles(List suites) {
def files = [
// Written once and referenced by every page below via a
same-directory relative
// <link>/<script src> instead of each page inlining its own copy
- see modernReportCss()'s
- // comment. modern-report.html (the single-page report) still
inlines both directly, since
+ // comment. test-report.html (the single-page report) still
inlines both directly, since
// staying one portable file is the point there.
'junit-report.css': modernReportCss(),
'junit-report.js': modernReportJs(),
'index.html': renderFramedIndexPage(),
'nav.html': renderFramedNavPage(suites),
'overview.html': renderFramedOverviewPage(suites),
- 'all-tests.html': renderFramedAllTestsPage(suites)
+ 'all-tests.html': renderFramedAllTestsPage(suites),
+ 'all-tests-fails.html': renderFramedStatusPage('fail', 'All
Failures', suites),
+ 'all-tests-errors.html': renderFramedStatusPage('error', 'All
Errors', suites),
+ 'all-tests-skipped.html': renderFramedStatusPage('skipped', 'All
Skipped', suites)
]
- suites.each { s -> files["suite-${suiteSlug(s.name)}.html"] =
renderFramedSuitePage(s) }
+ suites.each { s ->
+ def slug = suiteSlug(s.name)
+ def hasFails = !s.parseError && s.testcases.any { it.status == 'fail' }
+ def hasErrors = !s.parseError && s.testcases.any { it.status ==
'error' }
+ files["suite-${slug}.html"] = renderFramedSuitePage(s, hasFails,
hasErrors)
+ if (hasFails) {
+ files["suite-${slug}-fails.html"] = renderFramedSuiteStatusPage(s,
'fail', 'Failures')
+ }
+ if (hasErrors) {
+ files["suite-${slug}-errors.html"] =
renderFramedSuiteStatusPage(s, 'error', 'Errors')
+ }
+ }
files
}
@@ -608,8 +688,8 @@ def parseSuiteXml(File xmlFile) {
}
}
-task createModernTestReport(group: sysadminGroup,
- description: 'Generate a single-page modern HTML test report alongside
the classic Ant-based one') {
+task createTestReport(group: sysadminGroup,
+ description: 'Generate a single-page HTML test report') {
doLast {
def suiteFiles = fileTree('./runtime/logs/test-results') {
include '*.xml'
@@ -624,28 +704,36 @@ task createModernTestReport(group: sysadminGroup,
def suites = suiteFiles.collect { xmlFile -> parseSuiteXml(xmlFile) }
.sort { it.name?.toLowerCase() }
- def reportFile = file('./runtime/logs/test-results/modern-report.html')
+ def reportFile = file('./runtime/logs/test-results/test-report.html')
reportFile.parentFile.mkdirs()
+ // A checkout upgrading from before the rename still has the previous
run's
+ // modern-report.html sitting here with nothing to mark it stale -
drop it so only the
+ // report this run just generated is left behind (the framed task
self-cleans its whole
+ // output directory, and clears the old modern-report-framed/ the same
way).
+ delete file('./runtime/logs/test-results/modern-report.html')
reportFile.text = renderModernReport(suites)
- println "Modern test report written to ${reportFile}"
+ println "Test report written to ${reportFile}"
}
}
task createFramedTestReport(group: sysadminGroup,
- description: 'Generate a navigable, frameset-based HTML test report
alongside the classic Ant-based and single-page modern reports') {
+ description: 'Generate a navigable, frameset-based HTML test report') {
doLast {
def suiteFiles = fileTree('./runtime/logs/test-results') {
include '*.xml'
exclude 'TESTS-TestSuites.xml'
}
- // See createModernTestReport's matching comment: sort by parsed suite
name, not filename.
+ // See createTestReport's matching comment: sort by parsed suite name,
not filename.
def suites = suiteFiles.collect { xmlFile -> parseSuiteXml(xmlFile) }
.sort { it.name?.toLowerCase() }
- def outDir = file('./runtime/logs/test-results/modern-report-framed')
+ def outDir = file('./runtime/logs/test-results/html')
if (outDir.exists()) {
outDir.deleteDir()
}
+ // Same stale-output cleanup createTestReport does for
modern-report.html: a checkout
+ // upgrading from before the rename still has the pre-rename framed
output directory here.
+ delete file('./runtime/logs/test-results/modern-report-framed')
outDir.mkdirs()
buildFramedReportFiles(suites).each { name, content ->
file("${outDir}/${name}").text = content }
println "Framed test report written to ${outDir}/index.html"