Skip to content

REST API Support for Running and Monitoring OFBiz Test Cases - #1690

Merged
ashishvijaywargiya merged 29 commits into
apache:trunkfrom
ashishvijaywargiya:test-cases-api-support
Aug 20, 2026
Merged

REST API Support for Running and Monitoring OFBiz Test Cases#1690
ashishvijaywargiya merged 29 commits into
apache:trunkfrom
ashishvijaywargiya:test-cases-api-support

Conversation

@ashishvijaywargiya

Copy link
Copy Markdown
Contributor

Today, running OFBiz's tests means logging into the server and typing a command by hand. This feature lets tests be started and checked over a simple web request instead, so tools like CI pipelines can run them automatically, and no one needs direct server access just to run a test.

I took care of the following work items in this pass:

  1. Adds TestRunServices.runTestSuite/getTestRunStatus, a REST/service-triggered way to kick off an existing testdef test-suite (optionally one case-name within it) asynchronously and poll it via a runId, reusing the exact same JunitSuiteWrapper/TestRunContainer engine that ofbiz --test and gradlew testIntegration already use.

  2. Gates that API behind two independent fail-closed checks enforced inside the service itself, not just at the REST layer: the test.api.enabled config flag (off by default) and a new TESTEXEC_ADMIN permission.

  3. Tracks each run's lifecycle (QUEUED -> RUNNING -> PASSED/FAILED/ERROR) in an in-memory TestRunTracker so getTestRunStatus can be polled while the run executes on a dedicated single-threaded executor, and archives finished API-triggered runs into the same manifest.json test-history store gradlew test/testIntegration already write to, tagged trigger="api".

  4. Adds runScopedTestSuite/getScopedTestRunStatus, which force-lock componentName server-side so a component-branded REST endpoint can never trigger or poll another component's tests regardless of what a caller supplies; plugins/example's ExampleTestRunServices (a Groovy service script) is the reference implementation any other component can copy.

  5. Adds a testParams map that lets a caller override field values inside the Jupiter test methods for that one run, delivered through a ThreadLocal bridge and JupiterTestHelper.getTestParams(), later extended to be namespaced per test method so two different methods in the same run can each receive their own value for the same field name.

  6. Adds ofbiz --test method= on the CLI and the equivalent testMethodName on runTestSuite/runExampleTestSuite, both scoping a resolved case's class down to one @test or @ParameterizedTest method instead of running the whole class, sharing the identical fail-closed validators and reflection-based method discovery (needed because JUnit Platform's plain selectMethod(Class,String) cannot match a method that takes parameters).

  7. Documents and partially mitigates a POC-scope dispatcher/delegator resource leak from running tests inside a long-lived server process, deliberately using ServiceContainer.removeFromCache rather than deregister after a fix for a critical regression where deregister was found to shut down the live server's real JMS listeners.

  8. Adds substantial new unit-test coverage across both repos (TestRunServicesTest, TestRunContainerTest, JupiterClassRunnerTest, TestRunTrackerTest, and more) and grows plugins/example's ExampleJupiterTests with testParams-driven and parameterized test methods used to exercise every feature above end-to-end.

  9. A final round of live testing against a running dev server -- unit tests, checkstyle, and real REST/CLI calls covering valid runs, parameterized methods, every validation-failure path, permission/auth checks, and componentName-tampering attempts -- found zero regressions across both the test-run-triggering feature and the method=/testMethodName feature.

Add a new 5-arg constructor to JupiterClassRunner that accepts a testParams
parameter map, with the 4-arg constructor delegating to it with Map.of().
Arm and clear CURRENT_TEST_PARAMS in the run() method's try/finally block.

Add a corresponding 5-arg overload to TestRunContainer.runSuiteEntries()
that accepts testParams, with the 4-arg version delegating to it.

Include tests verifying that CURRENT_TEST_PARAMS is armed during execution
and cleared after, and that it defaults to an empty Map when omitted.
…suite resolution

- executeRun: wrap SuiteXmlReportWriter usage per suite so endSuite() (the
  only place that flushes/writes/closes the underlying FileOutputStream)
  always runs via finally, even if runSuiteEntries()/rollback() throws.
  Prevents a file-descriptor leak and a 0-byte XML being left on disk.
- executeRun: widen the outer catch from Exception to Throwable, matching
  TestRunContainer.start()'s established precedent, so an Error escaping a
  test class (NoClassDefFoundError, StackOverflowError, OOM) still reaches
  TRACKER.markError(...) instead of leaving the run stuck at RUNNING forever.
- runTestSuite: wrap JunitSuiteWrapper construction and the empty-list check
  in a try/catch returning ServiceUtil.returnError(...), so malformed testdef
  XML during suite resolution no longer propagates as a raw 500 to the REST
  caller.
… limitations

runTestSuite reuses ModelTestSuite's constructor (the same construction the
ofbiz --test CLI already uses once per process), which permanently registers
a new test Delegator/LocalDispatcher in ServiceContainer's static dispatcher
cache and re-runs startup services. Harmless for the one-shot CLI, but a real
resource leak for this feature's long-lived server process handling many
API-triggered runs with no cleanup. This is an accepted POC limitation, not
an oversight - a proper fix would touch shared static server-engine
internals (ServiceContainer deregistration) that can't be safely validated
without a live running instance.

Also documents that API-triggered runs execute against the live server's
database with only best-effort rollback (modelSuite.getDelegator().rollback()),
not a true isolated sandbox - previously undocumented anywhere in the shipped
code and a material fact for anyone deciding whether to enable
test.api.enabled.

Adds a class-level javadoc paragraph on TestRunServices and a matching note
in testtools.properties' test.api.enabled comment block.
register() stored the caller's paramsUsed map by reference; that same map
instance later gets armed into JupiterTestExtension.CURRENT_TEST_PARAMS and
is directly readable/mutable inside the running Jupiter test class (as
testParameters). A test mutating testParameters could corrupt both the
tracker's stored record and, eventually, the archived manifest.json's
paramsUsed.

Wraps the incoming map with Map.copyOf(...) before storing it - the existing
call site (TestRunServices.runTestSuite) always passes a non-null map, so no
null handling is needed. Adds a test proving that mutating the original map
after calling register() does not affect what tracker.get(runId).paramsUsed()
returns.
- Remove the dead 'Delegator delegator = null;' local that existed only to
  be passed as null; pass null directly to readStringProperty(...) instead.
- Replace the inline fully-qualified org.apache.ofbiz.base.util.UtilProperties
  call with a proper import and unqualified call, matching the rest of the
  file's style.
- Document, at the point a reader would notice it, that test.history is read
  file-only here (delegator is null on this background thread) unlike
  test.api.enabled, which is delegator-aware - so a SystemProperty override
  of test.history will NOT apply to API-triggered runs. Intentional
  (see the existing 'Note for the implementer' comment) but previously
  undocumented at this specific point.
M1: Move defensive parameterization to runTestSuite before passing to
    executor/test/archiver, ensuring all downstream consumers receive an
    immutable, null-safe copy from that point on.

M2: Replace Map.copyOf with LinkedHashMap+unmodifiableMap pattern in both
    runTestSuite and TestRunTracker.register to tolerate null values,
    fixing the NullPointerException when testParams contained null values.

Add test verifying null values are handled without NPE.
Update comments to reflect the layered defense-in-depth approach.
Deregister each ModelTestSuite's test dispatcher after its suite
finishes, via ServiceContainer.deregister(name). This removes the
per-run entry from ServiceContainer's static dispatcher cache and
closes its JMS listeners, so those no longer accumulate forever on
a long-lived server. Best-effort: failures are logged, not thrown,
so cleanup can never turn a passing run into a reported failure.

Still open, and documented as such in the class javadoc and in
testtools.properties: the underlying ServiceDispatcher instance
itself stays in ServiceDispatcher's own separate static cache
indefinitely, since no public API removes an entry from that map
(would require changes to ServiceDispatcher.java, out of scope
here). Startup services also still re-run on every call, since
each run's delegator gets a unique name and so never hits the
ServiceDispatcher-level cache regardless of deregistration.
…eanup

The prior leak-mitigation commit called ServiceContainer.deregister(name)
after each API-triggered test suite. That call shuts down the backing
ServiceDispatcher once its localContext empties out - always true
immediately for a dispatcher used by exactly one test run - and shutdown
closes JMS listeners through JmsListenerFactory, a process-wide static
singleton shared by every dispatcher in the JVM, including the live
server's real one. Every API-triggered test run would have permanently
killed the server's actual JMS listeners in any deployment with JMS
listening enabled. Switched to ServiceContainer.removeFromCache(name),
which only removes the ServiceContainer dispatcher-cache entry and
touches nothing else. Corrected the javadoc/properties-comment claims
that described the removed call as closing JMS listeners.

Also, related to the same cleanup path:

- JunitSuiteWrapper now tracks ModelTestSuite instances it discards for
  having no matching tests (previously silently dropped, each still
  holding a live dispatcher/delegator pair from its constructor).
  TestRunServices deregisters these before returning its "no tests
  found" error, closing a leak that previously had no mitigation at all.
- Moved the dispatcher-name lookup inside the try block in
  deregisterTestDispatcher and widened its catch from Exception to
  Throwable, so a cleanup-path Error can no longer escape into the
  outer handler and turn a passing run into a reported error.
- Reworded the class javadoc so it no longer implies the test Delegator
  is mitigated by this fix - it stays pinned by the ServiceDispatcher
  instance for as long as that instance remains cached, same as before.
- Widened the per-suite try in executeRun to cover report-stream
  creation, and added a fallback in the outer finally to deregister any
  suites the loop didn't reach, so one suite's exception can no longer
  abort the loop and leave every later suite's dispatcher registered.

Verified via ./gradlew test --tests "org.apache.ofbiz.testtools.*" (all
passing). ServiceDispatcher.java and ModelTestSuite.java are untouched.
…adopters away from the raw services

- runScopedTestSuite/getScopedTestRunStatus now reject an empty/null fixed/expected
  componentName up front with a clean ServiceUtil error, instead of one failing open
  (ComponentConfig treats a null cname as match-all) and the other throwing a raw NPE.
- Added services.xml description text and TestRunServices class javadoc pointing a
  reader from the raw runTestSuite/getTestRunStatus services to the scoped-wrapper
  pattern, so a future component doesn't reproduce the cross-component-reach bug by
  exposing them directly in its own *.rest.xml.
- Added tests covering both new guards and the null-componentName masking branch.
Matches the wire-level runTestSuite service attribute name (testParams)
instead of a second name for the same concept. Groovy test classes now
read the bare testParams property instead of testParameters - see the
companion commit in plugins/ (ExampleJupiterTests.groovy).
@ashishvijaywargiya
ashishvijaywargiya merged commit fac1ba4 into apache:trunk Aug 20, 2026
7 checks passed
ashishvijaywargiya added a commit to apache/ofbiz-plugins that referenced this pull request Aug 20, 2026
…component sample code) (#370)

Pushing the sample code from plugins->example component, this will help
others to see the patterns like how we can enable rest support for other
component's test cases.

ofbiz-framework PR:
apache/ofbiz-framework#1690
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant