This is an automated email from the ASF dual-hosted git repository.

ashishvijaywargiya pushed a commit to branch release24.09
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/release24.09 by this push:
     new ccdee9ee3f Fix webtools Run Suite/Run Case: missing prerequisites and 
shared ambient transaction (#1655) (#1656)
ccdee9ee3f is described below

commit ccdee9ee3f51793c9549d2be5a59a995fcb0d921
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Sat Aug 15 13:59:48 2026 +0530

    Fix webtools Run Suite/Run Case: missing prerequisites and shared ambient 
transaction (#1655) (#1656)
    
    Backport of #1655 (trunk commit b9dfe95aa1) to release24.09.
    
    ## Problem
    
    Webtools' "Run Suite" / "Run Case" (`TestSuiteInfo` ->
    `RunTest/{compName}/{suiteName}[/{caseName}]`) had two bugs that didn't
    show up via the CLI's `ofbiz --test`:
    
    1. Requesting a single case (`case=`) ran only that exact case, silently
    skipping sibling setup cases it depends on (e.g. `party-tests` needs the
    data `party-tests-data-load` creates), so a filtered run could fail for
    missing data that the full suite run never exposed.
    2. `entity`/`service`/`ecommerce` full-suite runs failed or misbehaved:
    `RunTestEvents` -> `TestRunContainer` runs a whole testdef suite's worth
    of otherwise-independent test-cases inside one
    `JavaEventHandler`-invoked event call, and `JavaEventHandler`
    unconditionally wraps that whole call in one ambient transaction. A
    `require-new-transaction` service invoked by one test-case couldn't see
    data a prior test-case's own entity-xml load had "committed" (that
    commit was a no-op participant in the still-open ambient transaction),
    and one test-case marking that shared transaction rollback-only poisoned
    every test-case after it in the same run. Neither symptom is possible
    via `ofbiz --test`, which runs `TestRunContainer` as a bare `Container`
    with no event/HTTP layer and no ambient transaction at all.
    
    ## Fix
    
    **`ModelTestSuite`**: `case=` filtering now auto-includes only genuine
    data-load prerequisites (`entity-xml`/`entity-xml-assert` with
    `action="load"`) declared before the requested case - not every
    preceding case. An earlier, broader version of this fix (running
    everything declared before the target) over-included independent sibling
    test classes, real `service-test` cases, and `entity-xml
    action="assert"` checks - `entitytests.xml` bundles five unrelated
    `jupiter-test-suite` classes in one file, and requesting one used to
    also run the others, including a deliberately slow one.
    
    **`JavaEventHandler`**: honors the existing `global-transaction="false"`
    `<event>` attribute (previously only read by `ServiceMultiEventHandler`,
    unused anywhere in the codebase until this change).
    **`controller.xml`**: declares `global-transaction="false"` on both
    `RunTest` request-maps in webtools, opting only that one event out of
    the default whole-invocation ambient transaction.
    
    ### Backport-specific notes
    
    - Omits trunk's new `ModelTestSuiteTest.java`: this branch's `test`
    Gradle task is JUnit4-only (`useJUnit()`, no `useJUnitPlatform()`), so a
    pure-Jupiter unit test can't be auto-executed here the way it is on
    trunk.
    - Also includes a one-line fix to `RunTestEvents.java`, unrelated to and
    predating #1655: an inverted null-check (`if (caseName == null)` instead
    of `if (caseName != null)`) silently dropped the `case=` filter on every
    webtools case-filtered run, so
    `RunTest/{compName}/{suiteName}/{caseName}` always ran the entire suite
    regardless of `{caseName}` - defeating half of this fix's benefit via
    the actual UI. Trunk already has the correct condition; found and fixed
    while verifying this backport live through webtools.
    
    ## Testing
    
    - `./gradlew compileJava compileTestJava checkstyleMain checkstyleTest
    codenarcMain codenarcTest` - all clean.
    - `./gradlew cleanAll loadAll testIntegration` (full CLI run, every
    testdef suite in the repo): 36 suites, 1089 tests, 0 failures, 0 errors
    - including the previously-known pre-existing
    `facilitytests`/`InventoryTests.testGetInventoryAvailableByFacility`
    failure noted on trunk, which did not reproduce here.
    - Manually exercised `party` (case-filtered), `entity` (full suite +
    case-filtered `entity-crypto-tests`), `service` (full suite),
    `ecommerce` (full suite) through the actual webtools UI (`RunTest/...`
    URLs) - all passing, and `entity-crypto-tests` case-filtering confirmed
    scoped to just its own 4 tests (0.56s, was ~32s/59 tests before the
    `RunTestEvents.java` fix).
    
    (cherry picked from commit b9dfe95aa10b2857110d6d7c0db2a689d0d1fb65)
---
 build.gradle                                       |  23 ++++-
 .../org/apache/ofbiz/testtools/ModelTestSuite.java | 109 +++++++++++++++++++--
 .../ofbiz/webapp/event/JavaEventHandler.java       |  18 +++-
 .../ofbiz/webtools/artifactinfo/RunTestEvents.java |   2 +-
 .../webapp/webtools/WEB-INF/controller.xml         |  18 +++-
 5 files changed, 152 insertions(+), 18 deletions(-)

diff --git a/build.gradle b/build.gradle
index 9d2dd0823e..60dc5e0450 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1004,11 +1004,28 @@ tasks.addRule('Pattern: ofbizBackground <Commands>: 
Execute OFBiz startup comman
 def createOfbizCommandTask(taskName, arguments) {
     task(type: JavaExec, dependsOn: classes, taskName) {
         jvmArgs(application.applicationDefaultJvmArgs)
+        // Every ofbiz-launched dev server needs test classes on its 
classpath, not just one
+        // started with --test/-t: webtools' "Run Test" screen (RunTestEvents 
-> TestRunContainer)
+        // loads jupiter-test-suite/junit-test-suite classes by name inside 
whatever JVM is already
+        // running, regardless of how that JVM was started. OFBIZ-13402 moved 
those classes out of
+        // src/main/* (and so out of sourceSets.main) to keep them out of 
release artifacts; this
+        // task is a local dev-run launcher, never a release-packaging task, 
so widening its
+        // classpath here doesn't reintroduce test code into anything that 
gets distributed.
+        classpath = sourceSets.main.runtimeClasspath + 
sourceSets.test.runtimeClasspath
         if (taskName ==~ /^ofbiz.*(--test|-t).*/) {
-            classpath = sourceSets.main.runtimeClasspath + 
sourceSets.test.runtimeClasspath
+            // TestRunContainer.java writes one XML per suite as 
"<suite.getName()>.xml" and only
+            // 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 createTestReports below just globs 
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
+            // run's suites, nothing left over from before.
+            doFirst {
+                delete fileTree('./runtime/logs/test-results') { include 
'*.xml' }
+            }
             finalizedBy(createTestReports)
-        } else {
-            classpath = sourceSets.main.runtimeClasspath
         }
         mainClass = application.mainClass
         args arguments
diff --git 
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/ModelTestSuite.java
 
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/ModelTestSuite.java
index 547f9cc1ae..c941f6fff1 100644
--- 
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/ModelTestSuite.java
+++ 
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/ModelTestSuite.java
@@ -65,23 +65,112 @@ public class ModelTestSuite {
         this.delegator = 
DelegatorFactory.getDelegator(DELEGATOR_NAME).makeTestDelegator(DELEGATOR_NAME 
+ uniqueSuffix);
         this.dispatcher = ServiceContainer.getLocalDispatcher(DISPATCHER_NAME 
+ uniqueSuffix, delegator);
 
-        for (Element testCaseElement : UtilXml.childElementList(mainElement, 
UtilMisc.toSet("test-case", "test-group"))) {
+        List<Element> testCaseElements = 
List.copyOf(UtilXml.childElementList(mainElement, UtilMisc.toSet("test-case", 
"test-group")));
+        for (Element testCaseElement : 
selectTestCaseElements(testCaseElements, testCase)) {
             String caseName = testCaseElement.getAttribute("case-name");
             String nodeName = testCaseElement.getNodeName();
-            if (testCase == null || caseName.equals(testCase)) {
-                if ("test-case".equals(nodeName)) {
-                    parseTestElement(caseName, 
UtilXml.firstChildElement(testCaseElement));
-                } else if ("test-group".equals(nodeName)) {
-                    int i = 0;
-                    for (Element childElement: 
UtilXml.childElementList(testCaseElement)) {
-                        parseTestElement(caseName + '-' + i, childElement);
-                        i++;
-                    }
+            if ("test-case".equals(nodeName)) {
+                parseTestElement(caseName, 
UtilXml.firstChildElement(testCaseElement));
+            } else if ("test-group".equals(nodeName)) {
+                int i = 0;
+                for (Element childElement: 
UtilXml.childElementList(testCaseElement)) {
+                    parseTestElement(caseName + '-' + i, childElement);
+                    i++;
                 }
             }
         }
     }
 
+    /**
+     * Selects which of a suite's ordered test-case/test-group elements should 
run for a given
+     * {@code case=} filter.
+     *
+     * <p>A single case is rarely self-contained: every testdef file in this 
repo follows the
+     * convention of a data-load case (an {@code entity-xml action="load"}, 
typically named
+     * {@code *-data-load}/{@code load*TestData}) declared immediately before 
the case(s) that consume
+     * it - see e.g. applications/party/testdef/PartyTests.xml's {@code 
party-tests-data-load} feeding
+     * {@code party-tests}. Filtering down to only the exact requested 
case-name, the way this method
+     * used to work, silently drops that prerequisite and leaves the requested 
case failing for lack of
+     * data it never asked to be responsible for loading itself.
+     *
+     * <p>Only that one shape - an {@code entity-xml}/{@code 
entity-xml-assert} case whose
+     * {@code action} is {@code "load"} - is auto-included as a prerequisite; 
every other preceding
+     * case is skipped unless it is itself the requested case. That is 
deliberately narrower than
+     * "run everything declared before the requested case": a {@code load} is 
additive and
+     * side-effect-free relative to every other case in the suite, so running 
it unconditionally is
+     * safe, but nothing else in a testdef file has that property -
+     * <ul>
+     * <li>A {@code jupiter-test-suite}/{@code junit-test-suite} case, or a 
{@code test-group} (a
+     * repo-wide scan found 14 of 15 {@code test-group} children are themselves
+     * {@code jupiter-test-suite}), is a whole independent test class in its 
own right. Several
+     * testdef files (e.g. framework/entity/testdef/entitytests.xml) bundle 
multiple such classes into
+     * one {@code <test-suite>} purely for organization, with no data 
relationship between them -
+     * requesting entity-crypto-tests (declared third) used to also run the 
unrelated, much larger and
+     * deliberately slow entity-tests/entity-util-tests classes ahead of it, 
turning a single-case
+     * request into most of the suite's runtime.</li>
+     * <li>A {@code service-test} is always a real functional test in every 
testdef file in this repo,
+     * never data loading (see framework/service/testdef/servicetests.xml's
+     * service-dead-lock-retry-test and friends).</li>
+     * <li>An {@code entity-xml} whose action is {@code "assert"} (the default 
when unset) is a check,
+     * not a load - in servicetests.xml, 
service-eca-global-event-exec-assert-data verifies the side
+     * effects of the service-test case declared immediately before it. 
Auto-including an assert case
+     * without the real test whose effects it checks would assert against data 
that was never
+     * created - trading one bug (a missing prerequisite) for another (a 
spurious failure) rather than
+     * fixing it.</li>
+     * </ul>
+     *
+     * <p>Nothing declared after the requested case runs, so this is not 
simply "ignore the filter":
+     * a case still can't see data set up by a case that only runs after it in 
the suite's own
+     * declared order.
+     *
+     * <p>When {@code testCase} is {@code null} (no filter - run the whole 
suite), every element is
+     * selected unchanged. When {@code testCase} names a case-name that isn't 
present in this
+     * particular document at all, nothing is selected - unchanged from the 
old exact-match
+     * behavior, and still what lets JunitSuiteWrapper's suite-name filtering 
keep a testdef file
+     * that simply doesn't define the requested case from contributing 
anything.
+     * @param testCaseElements the suite's test-case/test-group elements, in 
declared order
+     * @param testCase the requested case-name, or {@code null} to select 
every element
+     * @return the elements to run, in the same order
+     */
+    static List<Element> selectTestCaseElements(List<Element> 
testCaseElements, String testCase) {
+        if (testCase == null) {
+            return testCaseElements;
+        }
+        List<Element> selected = new ArrayList<>();
+        for (Element element : testCaseElements) {
+            boolean isTarget = 
testCase.equals(element.getAttribute("case-name"));
+            if (isTarget) {
+                selected.add(element);
+                return selected;
+            }
+            if (isDataLoadCase(element)) {
+                selected.add(element);
+            }
+        }
+        return List.of();
+    }
+
+    /**
+     * True for a {@code <test-case>} whose sole child is an {@code 
entity-xml}/{@code entity-xml-assert}
+     * element with {@code action="load"}: the one shape safe to auto-include 
as a prerequisite ahead
+     * of a requested case. See selectTestCaseElements()'s javadoc for why 
every other shape - including
+     * the same elements with a different action - is excluded instead.
+     * @param testCaseElement one of a suite's test-case/test-group elements
+     * @return true if this element is a genuine data-load case
+     */
+    private static boolean isDataLoadCase(Element testCaseElement) {
+        if (!"test-case".equals(testCaseElement.getNodeName())) {
+            return false;
+        }
+        Element child = UtilXml.firstChildElement(testCaseElement);
+        if (child == null) {
+            return false;
+        }
+        String nodeName = child.getNodeName();
+        boolean isEntityXml = "entity-xml".equals(nodeName) || 
"entity-xml-assert".equals(nodeName);
+        return isEntityXml && "load".equals(child.getAttribute("action"));
+    }
+
     private void parseTestElement(String caseName, Element testElement) {
         String nodeName = testElement.getNodeName();
         if ("junit-test-suite".equals(nodeName)) {
diff --git 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/event/JavaEventHandler.java
 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/event/JavaEventHandler.java
index f11f3fbb2c..03ba636131 100644
--- 
a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/event/JavaEventHandler.java
+++ 
b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/event/JavaEventHandler.java
@@ -76,10 +76,24 @@ public class JavaEventHandler implements EventHandler {
         if (Debug.verboseOn()) {
             Debug.logVerbose("[Processing]: Java Event", MODULE);
         }
+        // A type="java" event defaults to running inside one transaction 
spanning the whole
+        // invoke() call - the same global-transaction="false" opt-out 
ServiceMultiEventHandler
+        // already honors for type="service-multi" events. Without it, an 
event whose own method
+        // deliberately manages several independent units of work (e.g. 
RunTestEvents running a
+        // whole testdef suite's worth of otherwise-independent test-cases 
through TestRunContainer)
+        // has every one of those units silently folded into this one ambient 
transaction instead:
+        // an entity-xml load's own begin()/commit() inside that event becomes 
a no-op participant
+        // rather than a real commit, so a require-new-transaction service 
called by a later,
+        // unrelated test-case in the same suite can't see data an earlier one 
just "committed",
+        // and one test-case marking the transaction rollback-only poisons 
every test-case after it
+        // in the same event invocation - see the 2026-08-15 
ecommerce/entity/service RunTest
+        // investigation this comment was added for.
         boolean began = false;
         try {
-            int timeout = Integer.max(event.getTransactionTimeout(), 0);
-            began = TransactionUtil.begin(timeout);
+            if (event.isGlobalTransaction()) {
+                int timeout = Integer.max(event.getTransactionTimeout(), 0);
+                began = TransactionUtil.begin(timeout);
+            }
             Method m = k.getMethod(event.getInvoke(), HttpServletRequest.class,
                                    HttpServletResponse.class);
             String ret = (String) m.invoke(null, request, response);
diff --git 
a/framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/RunTestEvents.java
 
b/framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/RunTestEvents.java
index 7ec49474a0..741e17dbd2 100644
--- 
a/framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/RunTestEvents.java
+++ 
b/framework/webtools/src/main/java/org/apache/ofbiz/webtools/artifactinfo/RunTestEvents.java
@@ -43,7 +43,7 @@ public class RunTestEvents {
 
         List<StartupCommand> ofbizCommands = new ArrayList<>();
         Map<String, String> cmdArgs = UtilMisc.toMap("component", component, 
"suitename", suiteName);
-        if (caseName == null) {
+        if (caseName != null) {
             cmdArgs.put("case", caseName);
         }
         ofbizCommands.add(new 
StartupCommand.Builder("test").properties(cmdArgs).build());
diff --git a/framework/webtools/webapp/webtools/WEB-INF/controller.xml 
b/framework/webtools/webapp/webtools/WEB-INF/controller.xml
index 783acb468a..f60a9de123 100644
--- a/framework/webtools/webapp/webtools/WEB-INF/controller.xml
+++ b/framework/webtools/webapp/webtools/WEB-INF/controller.xml
@@ -426,13 +426,27 @@ under the License.
 
     <request-map uri="RunTest/{compName}/{suiteName}">
         <security https="true" auth="true"/>
-        <event type="java" 
path="org.apache.ofbiz.webtools.artifactinfo.RunTestEvents" invoke="runTest"/>
+        <!-- TestRunContainer runs a whole testdef suite's worth of 
otherwise-independent
+             test-cases; each manages its own transaction the same way it does 
when run via
+             the ofbiz command line's own test option, which never runs inside 
an event's
+             ambient transaction to begin with. Without 
global-transaction="false" here,
+             JavaEventHandler's default whole-invoke() transaction folds every 
one of those
+             test-cases into a single ambient transaction instead, breaking 
both data
+             visibility between test-cases and per-suite rollback-only 
isolation. -->
+        <event type="java" 
path="org.apache.ofbiz.webtools.artifactinfo.RunTestEvents" invoke="runTest" 
global-transaction="false"/>
         <response name="success" type="request" value="TestSuiteInfo"/>
         <response name="error" type="request" value="TestSuiteInfo"/>
     </request-map>
     <request-map uri="RunTest/{compName}/{suiteName}/{caseName}">
         <security https="true" auth="true"/>
-        <event type="java" 
path="org.apache.ofbiz.webtools.artifactinfo.RunTestEvents" invoke="runTest"/>
+        <!-- TestRunContainer runs a whole testdef suite's worth of 
otherwise-independent
+             test-cases; each manages its own transaction the same way it does 
when run via
+             the ofbiz command line's own test option, which never runs inside 
an event's
+             ambient transaction to begin with. Without 
global-transaction="false" here,
+             JavaEventHandler's default whole-invoke() transaction folds every 
one of those
+             test-cases into a single ambient transaction instead, breaking 
both data
+             visibility between test-cases and per-suite rollback-only 
isolation. -->
+        <event type="java" 
path="org.apache.ofbiz.webtools.artifactinfo.RunTestEvents" invoke="runTest" 
global-transaction="false"/>
         <response name="success" type="request" value="TestSuiteInfo"/>
         <response name="error" type="request" value="TestSuiteInfo"/>
     </request-map>

Reply via email to