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 39fb3d0984 Add batch test-run REST endpoint (#1701)
39fb3d0984 is described below
commit 39fb3d09848514e990f34e6b0b77538f23eb5b8c
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Sun Aug 23 19:20:50 2026 +0530
Add batch test-run REST endpoint (#1701)
---
framework/testtools/api/testruns.rest.xml | 9 +
framework/testtools/servicedef/services.xml | 35 +-
.../apache/ofbiz/testtools/BatchRunServices.java | 276 ++++++++
.../apache/ofbiz/testtools/BatchRunTracker.java | 61 ++
.../apache/ofbiz/testtools/TestRunServices.java | 49 +-
.../ofbiz/testtools/BatchRunServicesTest.java | 711 +++++++++++++++++++++
.../ofbiz/testtools/BatchRunTrackerTest.java | 74 +++
.../ofbiz/testtools/TestRunServicesTest.java | 73 +++
8 files changed, 1282 insertions(+), 6 deletions(-)
diff --git a/framework/testtools/api/testruns.rest.xml
b/framework/testtools/api/testruns.rest.xml
index be2ed9e30b..a5e3be2e5e 100644
--- a/framework/testtools/api/testruns.rest.xml
+++ b/framework/testtools/api/testruns.rest.xml
@@ -41,5 +41,14 @@ under the License.
<operation verb="get" description="Get a test run's status"
path="{runId}">
<service name="getTestRunStatus"/>
</operation>
+
+ <operation verb="post" description="Trigger a batch test-suite run
across multiple components"
+ path="batch" consumes="application/json">
+ <service name="runBatchTestSuite"/>
+ </operation>
+
+ <operation verb="get" description="Get a batch run's status"
path="batch/{batchId}">
+ <service name="getBatchTestRunStatus"/>
+ </operation>
</resource>
</api>
diff --git a/framework/testtools/servicedef/services.xml
b/framework/testtools/servicedef/services.xml
index 3435c2c0cb..894ea27c21 100644
--- a/framework/testtools/servicedef/services.xml
+++ b/framework/testtools/servicedef/services.xml
@@ -78,9 +78,42 @@ under the License.
Exposed directly via the generic, framework-owned
framework/testtools/api/testruns.rest.xml
endpoint (GET /rest/testtools/testruns/{runId}) - same caution as
runTestSuite above applies to
any component-owned *.rest.xml.</description>
- <attribute name="runId" type="String" mode="IN" optional="false"/>
+ <attribute name="runId" type="String" mode="INOUT" optional="false"/>
<attribute name="status" type="String" mode="OUT" optional="true"/>
<attribute name="componentName" type="String" mode="OUT"
optional="true"/>
<attribute name="resultSummary" type="Map" mode="OUT" optional="true"/>
</service>
+
+ <service name="runBatchTestSuite" engine="java" auth="true"
+ location="org.apache.ofbiz.testtools.BatchRunServices"
invoke="runBatchTestSuite">
+ <description>Fans out a full-suite runTestSuite call to multiple
components in one call, tracked
+ under one batchId; poll getBatchTestRunStatus for the aggregate
and per-component results.
+ With no components list, auto-discovers every component that has a
testdef and has both the
+ global test.api.enabled and its own
test.api.enabled.<componentName> on - if none
+ qualify, the call is rejected rather than returning a batchId with
nothing queued. With an
+ explicit components list, an empty list is rejected outright, and
every named component must
+ itself be known, have a testdef, and be enabled, or the whole call
is rejected up front naming
+ which entries were invalid - no batch is ever queued from an
invalid or empty list. A
+ component that passes this upfront check but still fails its
actual test run (e.g. a testdef
+ that resolves to zero tests) is omitted from the batch rather than
failing the whole call, the
+ same way an auto-discovered component would be. Each queued
component always runs its whole
+ suite (no per-component suiteName/testCaseName/testMethodName
scoping in this endpoint).
+ Exposed directly via the generic, framework-owned
framework/testtools/api/testruns.rest.xml
+ endpoint (POST /rest/testtools/testruns/batch).</description>
+ <attribute name="components" type="List" mode="IN" optional="true"/>
+ <attribute name="batchId" type="String" mode="OUT" optional="true"/>
+ </service>
+
+ <service name="getBatchTestRunStatus" engine="java" auth="true"
+ location="org.apache.ofbiz.testtools.BatchRunServices"
invoke="getBatchTestRunStatus">
+ <description>Reads a runBatchTestSuite-triggered batch's aggregate
status (QUEUED/RUNNING/PASSED/
+ FAILED/ERROR, derived live from its children's own tracked
statuses) plus each component's own
+ runId/status/resultSummary. Exposed directly via the generic,
framework-owned
+ framework/testtools/api/testruns.rest.xml endpoint
+ (GET /rest/testtools/testruns/batch/{batchId}).</description>
+ <attribute name="batchId" type="String" mode="IN" optional="false"/>
+ <attribute name="status" type="String" mode="OUT" optional="true"/>
+ <attribute name="summary" type="Map" mode="OUT" optional="true"/>
+ <attribute name="components" type="List" mode="OUT" optional="true"/>
+ </service>
</services>
diff --git
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/BatchRunServices.java
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/BatchRunServices.java
new file mode 100644
index 0000000000..d54b2587c4
--- /dev/null
+++
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/BatchRunServices.java
@@ -0,0 +1,276 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.testtools;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.apache.ofbiz.base.component.ComponentConfig;
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.UtilGenerics;
+import org.apache.ofbiz.base.util.UtilValidate;
+import org.apache.ofbiz.entity.Delegator;
+import org.apache.ofbiz.entity.GenericValue;
+import org.apache.ofbiz.service.DispatchContext;
+import org.apache.ofbiz.service.ServiceUtil;
+
+/**
+ * REST-triggered batch test execution: runBatchTestSuite fans a full-suite
+ * {@link TestRunServices#runTestSuite} call out to multiple components in one
call, tracked under
+ * one batchId; getBatchTestRunStatus polls the aggregate. Layered entirely on
top of
+ * TestRunServices/TestRunTracker - no change to the existing single-component
+ * runTestSuite/getTestRunStatus contract.
+ *
+ * <p>Two modes: with no {@code components} list, every component with a
testdef where both the
+ * global {@code test.api.enabled} and its own {@code
test.api.enabled.<componentName>} (default
+ * {@code true}) resolve {@code true} is queued (see {@link
#discoverEligibleComponents}); with an
+ * explicit {@code components} list, an empty list is rejected outright, and
every named component
+ * must itself pass that same check or the whole call is rejected before
anything is queued - no
+ * batch is ever queued from an invalid or empty list. This upfront rejection
is distinct from a
+ * component that passes it but still fails its actual {@link
TestRunServices#runTestSuite} call
+ * (e.g. a testdef that resolves to zero tests): that component is simply
omitted from the batch,
+ * the same as an auto-discovered one would be - see {@link
#runBatchTestSuite}.
+ *
+ * <p>Each queued component always runs its whole suite - {@code runTestSuite}
is called with only
+ * {@code componentName} set, no {@code suiteName}, which {@link
JunitSuiteWrapper} already treats
+ * as "every testdef suite in this component" (see its {@code suiteName !=
null} filter). No
+ * per-component {@code testCaseName}/{@code testMethodName}/{@code
testParams} scoping in this
+ * endpoint.
+ *
+ * <p>{@code runBatchTestSuite} fans a component list out with a plain
sequential loop, each
+ * iteration calling {@code runTestSuite} synchronously before moving to the
next - per the "no new
+ * concurrency" constraint, nothing here runs in parallel or off the caller's
thread. Each
+ * {@code runTestSuite} call itself constructs a fresh test {@code
Delegator}/{@code
+ * LocalDispatcher} and re-runs startup services (see {@link TestRunServices}'
own javadoc), so a
+ * large auto-discovered component set can make the POST block for a while
before a {@code batchId}
+ * comes back - this is a known characteristic of the current POC-level
implementation, not a bug.
+ *
+ * <p>Exposed directly via the generic, framework-owned
+ * {@code framework/testtools/api/testruns.rest.xml} endpoint
+ * ({@code POST /rest/testtools/testruns/batch}, {@code GET
/rest/testtools/testruns/batch/{batchId}}).
+ * See {@link TestRunServices}' javadoc for the same "do not expose from a
component's own
+ * *.rest.xml" caution - it applies here identically.
+ */
+public final class BatchRunServices {
+
+ private static final String MODULE = BatchRunServices.class.getName();
+ private static final String TESTEXEC_PERMISSION = "TESTEXEC_ADMIN";
+
+ static final BatchRunTracker BATCH_TRACKER = new BatchRunTracker();
+
+ private BatchRunServices() {
+ }
+
+ public static Map<String, Object> runBatchTestSuite(DispatchContext dctx,
Map<String, ?> context) {
+ GenericValue userLogin = (GenericValue) context.get("userLogin");
+ String userLoginId = userLogin == null ? "unknown" :
userLogin.getString("userLoginId");
+
+ if (!dctx.getSecurity().hasPermission(TESTEXEC_PERMISSION, userLogin))
{
+ Debug.logWarning("runBatchTestSuite: DENIED for user '" +
userLoginId + "' - missing "
+ + TESTEXEC_PERMISSION, MODULE);
+ return ServiceUtil.returnError("You do not have permission to
trigger test runs (" + TESTEXEC_PERMISSION + ")");
+ }
+
+ List<String> requestedComponents =
UtilGenerics.cast(context.get("components"));
+ List<String> componentNames;
+ if (requestedComponents == null) {
+ componentNames = discoverEligibleComponents(dctx.getDelegator());
+ if (componentNames.isEmpty()) {
+ Debug.logWarning("runBatchTestSuite: rejected for user '" +
userLoginId
+ + "' - no components are eligible", MODULE);
+ return ServiceUtil.returnError("No components are eligible for
a batch test run - none have "
+ + "the test execution API enabled, or none have a
testdef.");
+ }
+ } else if (requestedComponents.isEmpty()) {
+ Debug.logWarning("runBatchTestSuite: rejected for user '" +
userLoginId
+ + "' - components list was explicitly empty", MODULE);
+ return ServiceUtil.returnError("The components list cannot be
empty - omit the field entirely to "
+ + "auto-discover eligible components, or name at least one
component.");
+ } else {
+ List<String> invalid =
validateRequestedComponents(requestedComponents, dctx.getDelegator());
+ if (!invalid.isEmpty()) {
+ Debug.logWarning("runBatchTestSuite: rejected for user '" +
userLoginId + "' - invalid components: "
+ + invalid, MODULE);
+ return ServiceUtil.returnError("The following components
cannot be included in this batch run: "
+ + String.join("; ", invalid));
+ }
+ componentNames = requestedComponents.stream().distinct().toList();
+ }
+
+ String batchId = UUID.randomUUID().toString();
+ List<BatchRunTracker.BatchChildRef> children = new ArrayList<>();
+ for (String componentName : componentNames) {
+ Map<String, Object> childContext = new LinkedHashMap<>();
+ childContext.put("componentName", componentName);
+ childContext.put("userLogin", userLogin);
+ Map<String, Object> result = TestRunServices.runTestSuite(dctx,
childContext);
+ if (ServiceUtil.isError(result)) {
+ // Silently omitted, matching the "auto-discovered component
has no testdef tests"
+ // edge case this same rule already covers - the only
realistic ways an
+ // already-eligibility-checked component can still fail here
are a genuinely empty
+ // testdef (registered but resolves to zero tests) or a live
config change racing
+ // between this batch's own eligibility check above and this
call, both of which
+ // mean there is no runId to show for this component either
way.
+ Debug.logWarning("runBatchTestSuite: batchId=" + batchId + "
skipped component '" + componentName
+ + "' - " + ServiceUtil.getErrorMessage(result),
MODULE);
+ continue;
+ }
+ children.add(new BatchRunTracker.BatchChildRef(componentName,
(String) result.get("runId")));
+ }
+ if (children.isEmpty()) {
+ Debug.logWarning("runBatchTestSuite: rejected for user '" +
userLoginId
+ + "' - every requested component's runTestSuite call
failed, batchId=" + batchId, MODULE);
+ return ServiceUtil.returnError("No components were successfully
queued for this batch - every "
+ + "requested component's runTestSuite call failed.");
+ }
+ BATCH_TRACKER.register(batchId, children);
+
+ List<String> queuedComponentNames =
children.stream().map(BatchRunTracker.BatchChildRef::componentName).toList();
+ Debug.logInfo("runBatchTestSuite: STARTED batchId=" + batchId + "
user='" + userLoginId + "' components="
+ + queuedComponentNames, MODULE);
+
+ Map<String, Object> response = ServiceUtil.returnSuccess();
+ response.put("batchId", batchId);
+ return response;
+ }
+
+ public static Map<String, Object> getBatchTestRunStatus(DispatchContext
dctx, Map<String, ?> context) {
+ GenericValue userLogin = (GenericValue) context.get("userLogin");
+ String userLoginId = userLogin == null ? "unknown" :
userLogin.getString("userLoginId");
+ if (!dctx.getSecurity().hasPermission(TESTEXEC_PERMISSION, userLogin))
{
+ Debug.logWarning("getBatchTestRunStatus: DENIED for user '" +
userLoginId + "' - missing "
+ + TESTEXEC_PERMISSION, MODULE);
+ return ServiceUtil.returnError("You do not have permission to view
test run status (" + TESTEXEC_PERMISSION + ")");
+ }
+
+ String batchId = (String) context.get("batchId");
+ List<BatchRunTracker.BatchChildRef> children =
BATCH_TRACKER.get(batchId);
+ if (children == null) {
+ return ServiceUtil.returnError("No such batchId: " + batchId);
+ }
+
+ List<Map<String, Object>> componentResults = new ArrayList<>();
+ int passed = 0;
+ int failed = 0;
+ int running = 0;
+ int queued = 0;
+ int errored = 0;
+ for (BatchRunTracker.BatchChildRef child : children) {
+ // TestRunServices.TRACKER always has an entry for this runId by
construction: it was
+ // registered synchronously (TestRunServices.runTestSuite's own
TRACKER.register call)
+ // before that call ever returned the runId this child was built
from, and nothing ever
+ // removes an entry from that tracker.
+ TestRunRecord record = TestRunServices.TRACKER.get(child.runId());
+ componentResults.add(TestRunServices.describeRun(record));
+ switch (record.status()) {
+ case PASSED -> passed++;
+ case FAILED -> failed++;
+ case ERROR -> errored++;
+ case RUNNING -> running++;
+ case QUEUED -> queued++;
+ }
+ }
+
+ String batchStatus;
+ if (errored > 0) {
+ batchStatus = "ERROR";
+ } else if (failed > 0) {
+ batchStatus = "FAILED";
+ } else if (running > 0 || queued > 0) {
+ batchStatus = "RUNNING";
+ } else {
+ batchStatus = "PASSED";
+ }
+
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("total", children.size());
+ summary.put("passed", passed);
+ summary.put("failed", failed);
+ summary.put("running", running);
+ summary.put("queued", queued);
+ summary.put("error", errored);
+
+ Map<String, Object> result = ServiceUtil.returnSuccess();
+ result.put("status", batchStatus);
+ result.put("summary", summary);
+ result.put("components", componentResults);
+ return result;
+ }
+
+ /**
+ * Every component with a testdef where both the global {@code
test.api.enabled} and its own
+ * {@code test.api.enabled.<componentName>} (default {@code true}) resolve
{@code true} -
+ * short-circuits to an empty list without even consulting {@link
ComponentConfig} when the
+ * global flag alone is off.
+ * @param delegator the calling request's Delegator, for the same
live-overridable property
+ * read {@link TestRunServices#readStringProperty} already gives a
single-component call
+ * @return every eligible component's name, in {@link ComponentConfig}'s
own declared order,
+ * each name appearing at most once even if it registers more than one
testdef file
+ */
+ static List<String> discoverEligibleComponents(Delegator delegator) {
+ if (!TestRunServices.isTestApiGloballyEnabled(delegator)) {
+ return List.of();
+ }
+ return ComponentConfig.getAllTestSuiteInfos(null).stream()
+ .map(info -> info.getComponentConfig().getComponentName())
+ .distinct()
+ .filter(name ->
TestRunServices.isTestApiEnabledForComponent(delegator, name))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Validates every caller-named component up front, before any of them is
queued - a bad entry
+ * must reject the whole call rather than silently running only the good
ones (see this class's
+ * javadoc).
+ * @param requested the caller-supplied {@code components} list, as given
(not yet validated)
+ * @param delegator the calling request's Delegator
+ * @return one human-readable reason string per invalid entry, empty if
every entry is valid
+ */
+ static List<String> validateRequestedComponents(List<String> requested,
Delegator delegator) {
+ boolean apiEnabled =
TestRunServices.isTestApiGloballyEnabled(delegator);
+ List<String> invalid = new ArrayList<>();
+ for (String name : requested) {
+ if (UtilValidate.isEmpty(name)) {
+ invalid.add(name + " (blank component name)");
+ continue;
+ }
+ if (!apiEnabled) {
+ invalid.add(name + " (test execution API is disabled)");
+ continue;
+ }
+ if (!Boolean.TRUE.equals(ComponentConfig.componentExists(name))) {
+ invalid.add(name + " (unknown component)");
+ continue;
+ }
+ if (ComponentConfig.getAllTestSuiteInfos(name).isEmpty()) {
+ invalid.add(name + " (no testdef found)");
+ continue;
+ }
+ boolean componentEnabled =
TestRunServices.isTestApiEnabledForComponent(delegator, name);
+ if (!componentEnabled) {
+ invalid.add(name + " (test execution API is disabled for this
component)");
+ }
+ }
+ return invalid;
+ }
+}
diff --git
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/BatchRunTracker.java
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/BatchRunTracker.java
new file mode 100644
index 0000000000..f7d910a488
--- /dev/null
+++
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/BatchRunTracker.java
@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.testtools;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * In-memory registry of runBatchTestSuite-triggered batches, for
getBatchTestRunStatus polling.
+ * Deliberately not persisted - does not survive a server restart, same as
TestRunTracker. Unlike
+ * TestRunTracker, a batch's child list never changes after registration:
runBatchTestSuite always
+ * finishes fanning every child run out (each already registered in
TestRunServices' own TRACKER)
+ * before it ever hands a batchId back to a caller, so there is nothing left
to mutate here - a
+ * batch's aggregate status is instead computed live, at read time, from each
child's own tracked
+ * TestRunRecord (see BatchRunServices.getBatchTestRunStatus).
+ *
+ * <p>Like TestRunTracker's own map, nothing ever removes an entry here either
- every batch ever
+ * triggered stays resident in memory for the life of the server process. This
is a much smaller
+ * per-entry footprint than the ServiceDispatcher/Delegator leak
TestRunServices' own javadoc
+ * documents at length (each entry here is just a componentName/runId pair
list, not a live
+ * dispatcher), but it is still unbounded growth with no TTL, cap, or purge
mechanism - a known
+ * characteristic of this POC-level implementation, not a bug.
+ */
+final class BatchRunTracker {
+
+ /**
+ * One component queued into a batch run, and the runId TestRunServices'
own TRACKER tracks it
+ * under.
+ * @param componentName the component this child run belongs to
+ * @param runId the same runId TestRunServices.TRACKER.get(runId) resolves
+ */
+ record BatchChildRef(String componentName, String runId) {
+ }
+
+ private final Map<String, List<BatchChildRef>> records = new
ConcurrentHashMap<>();
+
+ void register(String batchId, List<BatchChildRef> children) {
+ records.put(batchId, List.copyOf(children));
+ }
+
+ List<BatchChildRef> get(String batchId) {
+ return records.get(batchId);
+ }
+}
diff --git
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
index d0d48f6cec..ef8a2c78f4 100644
---
a/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
+++
b/framework/testtools/src/main/java/org/apache/ofbiz/testtools/TestRunServices.java
@@ -160,7 +160,7 @@ public final class TestRunServices {
return ServiceUtil.returnError("You do not have permission to
trigger test runs (" + TESTEXEC_PERMISSION + ")");
}
- boolean apiEnabled =
"true".equalsIgnoreCase(readStringProperty(dctx.getDelegator(),
"test.api.enabled", "false"));
+ boolean apiEnabled = isTestApiGloballyEnabled(dctx.getDelegator());
if (!apiEnabled) {
Debug.logWarning("runTestSuite: rejected for user '" + userLoginId
+ "', suite '" + suiteName + "'"
+ " - test.api.enabled is false", MODULE);
@@ -189,8 +189,7 @@ public final class TestRunServices {
// non-blank by the guard above, so this always runs now - there is no
longer an unscoped,
// no-componentName path through this method to skip it for.
// See
plugins/supporting-docs/specs/2026-08-21-per-component-test-api-toggle-design.md.
- boolean componentEnabled = "true".equalsIgnoreCase(
- readStringProperty(dctx.getDelegator(), "test.api.enabled." +
componentName, "true"));
+ boolean componentEnabled =
isTestApiEnabledForComponent(dctx.getDelegator(), componentName);
if (!componentEnabled) {
Debug.logWarning("runTestSuite: rejected for user '" + userLoginId
+ "', suite '" + suiteName + "'"
+ " - test.api.enabled." + componentName + " is false",
MODULE);
@@ -475,8 +474,24 @@ public final class TestRunServices {
}
Map<String, Object> result = ServiceUtil.returnSuccess();
- result.put("status", record.status().name());
+ result.putAll(describeRun(record));
+ return result;
+ }
+
+ /**
+ * Assembles one tracked run's runId/componentName/status/resultSummary
into the exact shape
+ * getTestRunStatus returns for it - reused as-is by
BatchRunServices.getBatchTestRunStatus so a
+ * batch's per-component entry is indistinguishable from what GET
/testruns/{runId} would return
+ * for that same runId. resultSummary merges the tracked resultSummary map
(if any) with an
+ * errorMessage key (if any), same as getTestRunStatus always did before
this was extracted.
+ * @param record the tracked run to describe - never null for either
caller (TRACKER.register
+ * always creates one before any runId is handed to a caller, and
nothing ever removes one)
+ */
+ static Map<String, Object> describeRun(TestRunRecord record) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("runId", record.runId());
result.put("componentName", record.componentName());
+ result.put("status", record.status().name());
Map<String, Object> resultSummary = new LinkedHashMap<>();
if (record.resultSummary() != null) {
resultSummary.putAll(record.resultSummary());
@@ -488,7 +503,7 @@ public final class TestRunServices {
return result;
}
- private static String readStringProperty(Delegator delegator, String
propertyName, String defaultValue) {
+ static String readStringProperty(Delegator delegator, String propertyName,
String defaultValue) {
try {
String value = delegator == null
? UtilProperties.getPropertyValue(RESOURCE, propertyName,
defaultValue)
@@ -500,4 +515,28 @@ public final class TestRunServices {
return defaultValue;
}
}
+
+ /**
+ * Whether the test execution API is enabled at all, server-wide - the
same {@code
+ * test.api.enabled} flag {@link #runTestSuite}, {@code
BatchRunServices.discoverEligibleComponents},
+ * and {@code BatchRunServices.validateRequestedComponents} all gate on
identically.
+ * @param delegator the calling request's Delegator, for the
live-overridable property read
+ * @return {@code true} only when the property resolves to the literal
string {@code "true"}
+ */
+ static boolean isTestApiGloballyEnabled(Delegator delegator) {
+ return "true".equalsIgnoreCase(readStringProperty(delegator,
"test.api.enabled", "false"));
+ }
+
+ /**
+ * Whether one component's own {@code test.api.enabled.<componentName>}
override allows the test
+ * execution API for it - defaults to enabled ({@code true}) when unset.
Only meaningful once
+ * {@link #isTestApiGloballyEnabled} is already {@code true}; callers are
expected to check the
+ * global flag first, matching {@link #runTestSuite}'s own order.
+ * @param delegator the calling request's Delegator, for the
live-overridable property read
+ * @param componentName the component to check
+ * @return {@code true} unless the property is explicitly set to something
other than {@code "true"}
+ */
+ static boolean isTestApiEnabledForComponent(Delegator delegator, String
componentName) {
+ return "true".equalsIgnoreCase(readStringProperty(delegator,
"test.api.enabled." + componentName, "true"));
+ }
}
diff --git
a/framework/testtools/src/test/java/org/apache/ofbiz/testtools/BatchRunServicesTest.java
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/BatchRunServicesTest.java
new file mode 100644
index 0000000000..433e70b42a
--- /dev/null
+++
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/BatchRunServicesTest.java
@@ -0,0 +1,711 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.testtools;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.ofbiz.base.component.ComponentConfig;
+import org.apache.ofbiz.entity.Delegator;
+import org.apache.ofbiz.entity.GenericValue;
+import org.apache.ofbiz.entity.util.EntityUtilProperties;
+import org.apache.ofbiz.security.Security;
+import org.apache.ofbiz.service.DispatchContext;
+import org.apache.ofbiz.service.ServiceUtil;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+class BatchRunServicesTest {
+
+ private static ComponentConfig.TestSuiteInfo testSuiteInfoFor(String
componentName) {
+ ComponentConfig componentConfig = mock(ComponentConfig.class);
+ when(componentConfig.getComponentName()).thenReturn(componentName);
+ ComponentConfig.TestSuiteInfo testSuiteInfo =
mock(ComponentConfig.TestSuiteInfo.class);
+ when(testSuiteInfo.getComponentConfig()).thenReturn(componentConfig);
+ return testSuiteInfo;
+ }
+
+ @Test
+ void
discoverEligibleComponentsReturnsEveryComponentWithATestdefWhenBothFlagsAreOn()
{
+ Delegator delegator = mock(Delegator.class);
+
+ var example = testSuiteInfoFor("example");
+ var party = testSuiteInfoFor("party");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos(null))
+ .thenReturn(List.of(example, party));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.example",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.party",
delegator))
+ .thenReturn("true");
+
+ List<String> discovered =
BatchRunServices.discoverEligibleComponents(delegator);
+
+ assertThat(discovered, is(List.of("example", "party")));
+ }
+ }
+
+ @Test
+ void discoverEligibleComponentsExcludesAComponentWithItsOwnFlagOff() {
+ Delegator delegator = mock(Delegator.class);
+
+ var example = testSuiteInfoFor("example");
+ var party = testSuiteInfoFor("party");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos(null))
+ .thenReturn(List.of(example, party));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.example",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.party",
delegator))
+ .thenReturn("false");
+
+ List<String> discovered =
BatchRunServices.discoverEligibleComponents(delegator);
+
+ assertThat(discovered, is(List.of("example")));
+ }
+ }
+
+ @Test
+ void
discoverEligibleComponentsDeduplicatesMultipleTestSuitesInOneComponent() {
+ Delegator delegator = mock(Delegator.class);
+
+ var accounting1 = testSuiteInfoFor("accounting");
+ var accounting2 = testSuiteInfoFor("accounting");
+ var accounting3 = testSuiteInfoFor("accounting");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos(null))
+ .thenReturn(List.of(accounting1, accounting2,
accounting3));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools",
"test.api.enabled.accounting", delegator))
+ .thenReturn("true");
+
+ List<String> discovered =
BatchRunServices.discoverEligibleComponents(delegator);
+
+ assertThat(discovered, is(List.of("accounting")));
+ }
+ }
+
+ @Test
+ void discoverEligibleComponentsReturnsEmptyListWhenGlobalFlagIsOff() {
+ Delegator delegator = mock(Delegator.class);
+
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("false");
+
+ List<String> discovered =
BatchRunServices.discoverEligibleComponents(delegator);
+
+ assertThat(discovered, is(List.of()));
+ componentConfig.verifyNoInteractions();
+ }
+ }
+
+ @Test
+ void validateRequestedComponentsReturnsEmptyListWhenAllAreValid() {
+ Delegator delegator = mock(Delegator.class);
+ var party = testSuiteInfoFor("party");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("party")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("party"))
+ .thenReturn(List.of(party));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.party",
delegator))
+ .thenReturn("true");
+
+ List<String> invalid =
BatchRunServices.validateRequestedComponents(List.of("party"), delegator);
+
+ assertThat(invalid, is(List.of()));
+ }
+ }
+
+ @Test
+ void validateRequestedComponentsFlagsAnUnknownComponent() {
+ Delegator delegator = mock(Delegator.class);
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("nosuch")).thenReturn(false);
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+
+ List<String> invalid =
BatchRunServices.validateRequestedComponents(List.of("nosuch"), delegator);
+
+ assertThat(invalid, contains("nosuch (unknown component)"));
+ }
+ }
+
+ @Test
+ void validateRequestedComponentsFlagsAComponentWithNoTestdef() {
+ Delegator delegator = mock(Delegator.class);
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("birt")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("birt")).thenReturn(List.of());
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+
+ List<String> invalid =
BatchRunServices.validateRequestedComponents(List.of("birt"), delegator);
+
+ assertThat(invalid, contains("birt (no testdef found)"));
+ }
+ }
+
+ @Test
+ void validateRequestedComponentsFlagsADisabledComponent() {
+ Delegator delegator = mock(Delegator.class);
+ var accounting = testSuiteInfoFor("accounting");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("accounting")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("accounting"))
+ .thenReturn(List.of(accounting));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools",
"test.api.enabled.accounting", delegator))
+ .thenReturn("false");
+
+ List<String> invalid =
BatchRunServices.validateRequestedComponents(List.of("accounting"), delegator);
+
+ assertThat(invalid, contains("accounting (test execution API is
disabled for this component)"));
+ }
+ }
+
+ @Test
+ void
validateRequestedComponentsFlagsABlankEntryWithoutCallingComponentConfig() {
+ Delegator delegator = mock(Delegator.class);
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+
+ List<String> invalid =
BatchRunServices.validateRequestedComponents(List.of(""), delegator);
+
+ assertThat(invalid, contains(" (blank component name)"));
+ componentConfig.verifyNoInteractions();
+ }
+ }
+
+ @Test
+ void validateRequestedComponentsFlagsEveryEntryWhenGlobalFlagIsOff() {
+ Delegator delegator = mock(Delegator.class);
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("false");
+
+ List<String> invalid =
BatchRunServices.validateRequestedComponents(List.of("party"), delegator);
+
+ assertThat(invalid, contains("party (test execution API is
disabled)"));
+ componentConfig.verifyNoInteractions();
+ }
+ }
+
+ @Test
+ void runBatchTestSuiteReturnsErrorWhenPermissionDenied() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("nobody");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(false);
+
+ Map<String, Object> result = BatchRunServices.runBatchTestSuite(dctx,
Map.of("userLogin", userLogin));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ assertThat(result.get("batchId"), nullValue());
+ }
+
+ @Test
+ void runBatchTestSuiteRejectsWhenNoComponentsAreEligible() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("false");
+
+ Map<String, Object> result =
BatchRunServices.runBatchTestSuite(dctx, Map.of("userLogin", userLogin));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ assertThat((String) result.get("errorMessage"), containsString("No
components are eligible"));
+ assertThat(result.get("batchId"), nullValue());
+ }
+ }
+
+ @Test
+ void runBatchTestSuiteRejectsAnExplicitlyEmptyComponentsList() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ Map<String, Object> result = BatchRunServices.runBatchTestSuite(dctx,
+ Map.of("userLogin", userLogin, "components", List.of()));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ assertThat((String) result.get("errorMessage"), containsString("cannot
be empty"));
+ assertThat(result.get("batchId"), nullValue());
+ }
+
+ @Test
+ void runBatchTestSuiteRejectsWholeBatchWhenAnExplicitComponentIsInvalid() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("nosuch")).thenReturn(false);
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+
+ Map<String, Object> result =
BatchRunServices.runBatchTestSuite(dctx,
+ Map.of("userLogin", userLogin, "components",
List.of("nosuch")));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ assertThat((String) result.get("errorMessage"),
containsString("nosuch (unknown component)"));
+ assertThat(result.get("batchId"), nullValue());
+ }
+ }
+
+ private static Map<String, Object> successResult(String runId) {
+ Map<String, Object> result = ServiceUtil.returnSuccess();
+ result.put("runId", runId);
+ return result;
+ }
+
+ private static Map<String, Object> errorResult(String message) {
+ return ServiceUtil.returnError(message);
+ }
+
+ @Test
+ void runBatchTestSuiteTracksASuccessfullyQueuedComponent() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ var exampleSuiteInfo = testSuiteInfoFor("example");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS);
+ MockedStatic<TestRunServices> testRunServices =
+ Mockito.mockStatic(TestRunServices.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("example")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("example"))
+ .thenReturn(List.of(exampleSuiteInfo));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.example",
delegator))
+ .thenReturn("true");
+ testRunServices.when(() -> TestRunServices.runTestSuite(eq(dctx),
Mockito.<Map<String, ?>>any()))
+ .thenReturn(successResult("run-example"));
+
+ Map<String, Object> result =
BatchRunServices.runBatchTestSuite(dctx,
+ Map.of("userLogin", userLogin, "components",
List.of("example")));
+
+ assertThat(result.get("responseMessage"), is("success"));
+ String batchId = (String) result.get("batchId");
+ assertThat(batchId, notNullValue());
+ assertThat(BatchRunServices.BATCH_TRACKER.get(batchId),
+ is(List.of(new BatchRunTracker.BatchChildRef("example",
"run-example"))));
+ }
+ }
+
+ @Test
+ void
runBatchTestSuiteRejectsTheWholeBatchWhenEveryComponentsRunTestSuiteCallErrors()
{
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ var flakySuiteInfo = testSuiteInfoFor("flaky");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS);
+ MockedStatic<TestRunServices> testRunServices =
+ Mockito.mockStatic(TestRunServices.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("flaky")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("flaky"))
+ .thenReturn(List.of(flakySuiteInfo));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.flaky",
delegator))
+ .thenReturn("true");
+ testRunServices.when(() -> TestRunServices.runTestSuite(eq(dctx),
Mockito.<Map<String, ?>>any()))
+ .thenReturn(errorResult("No tests found"));
+
+ Map<String, Object> result =
BatchRunServices.runBatchTestSuite(dctx,
+ Map.of("userLogin", userLogin, "components",
List.of("flaky")));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ assertThat(result.get("batchId"), nullValue());
+ }
+ }
+
+ @Test
+ void
runBatchTestSuiteOmitsAFailingComponentButKeepsGoingWhenAtLeastOneSucceeds() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ var exampleSuiteInfo = testSuiteInfoFor("example");
+ var flakySuiteInfo = testSuiteInfoFor("flaky");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS);
+ MockedStatic<TestRunServices> testRunServices =
+ Mockito.mockStatic(TestRunServices.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("example")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("example"))
+ .thenReturn(List.of(exampleSuiteInfo));
+ componentConfig.when(() ->
ComponentConfig.componentExists("flaky")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("flaky"))
+ .thenReturn(List.of(flakySuiteInfo));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.example",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.flaky",
delegator))
+ .thenReturn("true");
+ testRunServices.when(() -> TestRunServices.runTestSuite(eq(dctx),
argThat(ctx ->
+ "example".equals(((Map<?, ?>) ctx).get("componentName")))))
+ .thenReturn(successResult("run-example"));
+ testRunServices.when(() -> TestRunServices.runTestSuite(eq(dctx),
argThat(ctx ->
+ "flaky".equals(((Map<?, ?>) ctx).get("componentName")))))
+ .thenReturn(errorResult("No tests found"));
+
+ Map<String, Object> result =
BatchRunServices.runBatchTestSuite(dctx,
+ Map.of("userLogin", userLogin, "components",
List.of("example", "flaky")));
+
+ assertThat(result.get("responseMessage"), is("success"));
+ String batchId = (String) result.get("batchId");
+ assertThat(BatchRunServices.BATCH_TRACKER.get(batchId),
+ is(List.of(new BatchRunTracker.BatchChildRef("example",
"run-example"))));
+ }
+ }
+
+ @Test
+ void runBatchTestSuiteDedupesDuplicateComponentNamesInAnExplicitList() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ Delegator delegator = mock(Delegator.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(dctx.getDelegator()).thenReturn(delegator);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ var partySuiteInfo = testSuiteInfoFor("party");
+ try (MockedStatic<ComponentConfig> componentConfig =
Mockito.mockStatic(ComponentConfig.class);
+ MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS);
+ MockedStatic<TestRunServices> testRunServices =
+ Mockito.mockStatic(TestRunServices.class,
Mockito.CALLS_REAL_METHODS)) {
+ componentConfig.when(() ->
ComponentConfig.componentExists("party")).thenReturn(true);
+ componentConfig.when(() ->
ComponentConfig.getAllTestSuiteInfos("party"))
+ .thenReturn(List.of(partySuiteInfo));
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.party",
delegator))
+ .thenReturn("true");
+ testRunServices.when(() -> TestRunServices.runTestSuite(eq(dctx),
Mockito.<Map<String, ?>>any()))
+ .thenReturn(successResult("run-party"));
+
+ Map<String, Object> result =
BatchRunServices.runBatchTestSuite(dctx,
+ Map.of("userLogin", userLogin, "components",
List.of("party", "party")));
+
+ assertThat(result.get("responseMessage"), is("success"));
+ String batchId = (String) result.get("batchId");
+ assertThat(BatchRunServices.BATCH_TRACKER.get(batchId),
+ is(List.of(new BatchRunTracker.BatchChildRef("party",
"run-party"))));
+ testRunServices.verify(() ->
TestRunServices.runTestSuite(eq(dctx), Mockito.<Map<String, ?>>any()),
+ times(1));
+ }
+ }
+
+ @Test
+ void getBatchTestRunStatusReturnsErrorWhenPermissionDenied() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("nobody");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(false);
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-1", "userLogin", userLogin));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ }
+
+ @Test
+ void getBatchTestRunStatusReturnsErrorForAnUnknownBatchId() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "no-such-batch", "userLogin", userLogin));
+
+ assertThat(result.get("responseMessage"), is("error"));
+ }
+
+ @Test
+ void getBatchTestRunStatusIsRunningWhileAnyChildIsQueuedOrRunning() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-passed", "example-tests",
"example", "admin", Map.of());
+ TestRunServices.TRACKER.markRunning("run-passed");
+ TestRunServices.TRACKER.markPassed("run-passed", Map.of("total", 1,
"passed", 1, "failed", 0));
+ TestRunServices.TRACKER.register("run-running", "party-tests",
"party", "admin", Map.of());
+ TestRunServices.TRACKER.markRunning("run-running");
+ BatchRunServices.BATCH_TRACKER.register("batch-running", List.of(
+ new BatchRunTracker.BatchChildRef("example", "run-passed"),
+ new BatchRunTracker.BatchChildRef("party", "run-running")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-running", "userLogin", userLogin));
+
+ assertThat(result.get("status"), is("RUNNING"));
+ Map<?, ?> summary = (Map<?, ?>) result.get("summary");
+ assertThat(summary.get("total"), is(2));
+ assertThat(summary.get("passed"), is(1));
+ assertThat(summary.get("running"), is(1));
+ }
+
+ @Test
+ void getBatchTestRunStatusIsPassedWhenEveryChildPassed() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-a", "example-tests", "example",
"admin", Map.of());
+ TestRunServices.TRACKER.markPassed("run-a", Map.of("total", 1,
"passed", 1, "failed", 0));
+ BatchRunServices.BATCH_TRACKER.register("batch-passed",
+ List.of(new BatchRunTracker.BatchChildRef("example",
"run-a")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-passed", "userLogin", userLogin));
+
+ assertThat(result.get("status"), is("PASSED"));
+ }
+
+ @Test
+ void getBatchTestRunStatusIsFailedWhenAnyChildFailed() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-a2", "example-tests", "example",
"admin", Map.of());
+ TestRunServices.TRACKER.markPassed("run-a2", Map.of("total", 1,
"passed", 1, "failed", 0));
+ TestRunServices.TRACKER.register("run-b2", "party-tests", "party",
"admin", Map.of());
+ TestRunServices.TRACKER.markFailed("run-b2", Map.of("total", 1,
"passed", 0, "failed", 1));
+ BatchRunServices.BATCH_TRACKER.register("batch-failed", List.of(
+ new BatchRunTracker.BatchChildRef("example", "run-a2"),
+ new BatchRunTracker.BatchChildRef("party", "run-b2")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-failed", "userLogin", userLogin));
+
+ assertThat(result.get("status"), is("FAILED"));
+ }
+
+ @Test
+ void getBatchTestRunStatusIsErrorWhenAnyChildErrored() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-a3", "example-tests", "example",
"admin", Map.of());
+ TestRunServices.TRACKER.markPassed("run-a3", Map.of("total", 1,
"passed", 1, "failed", 0));
+ TestRunServices.TRACKER.register("run-b3", "party-tests", "party",
"admin", Map.of());
+ TestRunServices.TRACKER.markError("run-b3", new
RuntimeException("suite blew up"));
+ BatchRunServices.BATCH_TRACKER.register("batch-error", List.of(
+ new BatchRunTracker.BatchChildRef("example", "run-a3"),
+ new BatchRunTracker.BatchChildRef("party", "run-b3")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-error", "userLogin", userLogin));
+
+ assertThat(result.get("status"), is("ERROR"));
+ }
+
+ @Test
+ void getBatchTestRunStatusIncludesPerComponentResults() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-a4", "example-tests", "example",
"admin", Map.of());
+ TestRunServices.TRACKER.markPassed("run-a4", Map.of("total", 1,
"passed", 1, "failed", 0));
+ BatchRunServices.BATCH_TRACKER.register("batch-components",
+ List.of(new BatchRunTracker.BatchChildRef("example",
"run-a4")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-components", "userLogin", userLogin));
+
+ List<?> components = (List<?>) result.get("components");
+ assertThat(components.size(), is(1));
+ Map<?, ?> componentResult = (Map<?, ?>) components.get(0);
+ assertThat(componentResult.get("componentName"), is("example"));
+ assertThat(componentResult.get("runId"), is("run-a4"));
+ assertThat(componentResult.get("status"), is("PASSED"));
+ }
+
+ @Test
+ void getBatchTestRunStatusPrefersErrorOverFailedWhenBothArePresent() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-p1-err", "example-tests",
"example", "admin", Map.of());
+ TestRunServices.TRACKER.markError("run-p1-err", new
RuntimeException("example error"));
+ TestRunServices.TRACKER.register("run-p1-fail", "party-tests",
"party", "admin", Map.of());
+ TestRunServices.TRACKER.markFailed("run-p1-fail", Map.of("total", 1,
"passed", 0, "failed", 1));
+ BatchRunServices.BATCH_TRACKER.register("batch-p1", List.of(
+ new BatchRunTracker.BatchChildRef("example", "run-p1-err"),
+ new BatchRunTracker.BatchChildRef("party", "run-p1-fail")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-p1", "userLogin", userLogin));
+
+ assertThat(result.get("status"), is("ERROR"));
+ }
+
+ @Test
+ void getBatchTestRunStatusPrefersFailedOverRunningWhenBothArePresent() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-p2-fail", "example-tests",
"example", "admin", Map.of());
+ TestRunServices.TRACKER.markFailed("run-p2-fail", Map.of("total", 1,
"passed", 0, "failed", 1));
+ TestRunServices.TRACKER.register("run-p2-run", "party-tests", "party",
"admin", Map.of());
+ TestRunServices.TRACKER.markRunning("run-p2-run");
+ BatchRunServices.BATCH_TRACKER.register("batch-p2", List.of(
+ new BatchRunTracker.BatchChildRef("example", "run-p2-fail"),
+ new BatchRunTracker.BatchChildRef("party", "run-p2-run")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-p2", "userLogin", userLogin));
+
+ assertThat(result.get("status"), is("FAILED"));
+ }
+
+ @Test
+ void
getBatchTestRunStatusIsRunningForAGenuinelyQueuedChildThatNeverStarted() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-p3-queued", "example-tests",
"example", "admin", Map.of());
+ BatchRunServices.BATCH_TRACKER.register("batch-p3",
+ List.of(new BatchRunTracker.BatchChildRef("example",
"run-p3-queued")));
+
+ Map<String, Object> result =
BatchRunServices.getBatchTestRunStatus(dctx,
+ Map.of("batchId", "batch-p3", "userLogin", userLogin));
+
+ assertThat(result.get("status"), is("RUNNING"));
+ Map<?, ?> summary = (Map<?, ?>) result.get("summary");
+ assertThat(summary.get("queued"), is(1));
+ }
+}
diff --git
a/framework/testtools/src/test/java/org/apache/ofbiz/testtools/BatchRunTrackerTest.java
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/BatchRunTrackerTest.java
new file mode 100644
index 0000000000..9e76860cdd
--- /dev/null
+++
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/BatchRunTrackerTest.java
@@ -0,0 +1,74 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+
*******************************************************************************/
+package org.apache.ofbiz.testtools;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class BatchRunTrackerTest {
+
+ @Test
+ void registerMakesTheChildListRetrievableByBatchId() {
+ BatchRunTracker tracker = new BatchRunTracker();
+ List<BatchRunTracker.BatchChildRef> children = List.of(
+ new BatchRunTracker.BatchChildRef("example", "run-1"),
+ new BatchRunTracker.BatchChildRef("party", "run-2"));
+
+ tracker.register("batch-1", children);
+
+ assertThat(tracker.get("batch-1"), is(children));
+ }
+
+ @Test
+ void getReturnsNullForAnUnknownBatchId() {
+ BatchRunTracker tracker = new BatchRunTracker();
+
+ assertThat(tracker.get("no-such-batch"), nullValue());
+ }
+
+ @Test
+ void registerDefensivelyCopiesTheCallersChildList() {
+ BatchRunTracker tracker = new BatchRunTracker();
+ List<BatchRunTracker.BatchChildRef> children = new ArrayList<>();
+ children.add(new BatchRunTracker.BatchChildRef("example", "run-1"));
+
+ tracker.register("batch-1", children);
+ children.add(new BatchRunTracker.BatchChildRef("party", "run-2"));
+
+ assertThat(tracker.get("batch-1"), is(List.of(new
BatchRunTracker.BatchChildRef("example", "run-1"))));
+ }
+
+ @Test
+ void registerRejectsFurtherMutationOfTheStoredList() {
+ BatchRunTracker tracker = new BatchRunTracker();
+ tracker.register("batch-1", List.of(new
BatchRunTracker.BatchChildRef("example", "run-1")));
+
+ List<BatchRunTracker.BatchChildRef> stored = tracker.get("batch-1");
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ stored.add(new BatchRunTracker.BatchChildRef("party",
"run-2")));
+ }
+}
diff --git
a/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
index 46eb5b05ba..c2569601cf 100644
---
a/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
+++
b/framework/testtools/src/test/java/org/apache/ofbiz/testtools/TestRunServicesTest.java
@@ -271,4 +271,77 @@ class TestRunServicesTest {
assertThat(result.get("runId"), nullValue());
}
}
+
+ @Test
+ void describeRunIncludesRunIdComponentNameStatusAndResultSummary() {
+ TestRunRecord record = TestRunRecord.queued("run-9", "example-tests",
"example", "admin", Map.of())
+ .passed(Map.of("total", 2, "passed", 2, "failed", 0));
+
+ Map<String, Object> described = TestRunServices.describeRun(record);
+
+ assertThat(described.get("runId"), is("run-9"));
+ assertThat(described.get("componentName"), is("example"));
+ assertThat(described.get("status"), is("PASSED"));
+ assertThat(described.get("resultSummary"), is(Map.of("total", 2,
"passed", 2, "failed", 0)));
+ }
+
+ @Test
+ void describeRunAddsErrorMessageIntoResultSummaryWhenPresent() {
+ TestRunRecord record = TestRunRecord.queued("run-9", "example-tests",
"example", "admin", Map.of())
+ .error(new RuntimeException("boom"));
+
+ Map<String, Object> described = TestRunServices.describeRun(record);
+
+ assertThat(described.get("status"), is("ERROR"));
+ assertThat(((Map<?, ?>)
described.get("resultSummary")).get("errorMessage"), is("boom"));
+ }
+
+ @Test
+ void getTestRunStatusIncludesRunId() {
+ DispatchContext dctx = mock(DispatchContext.class);
+ Security security = mock(Security.class);
+ GenericValue userLogin = mock(GenericValue.class);
+ when(dctx.getSecurity()).thenReturn(security);
+ when(userLogin.getString("userLoginId")).thenReturn("admin");
+ when(security.hasPermission("TESTEXEC_ADMIN",
userLogin)).thenReturn(true);
+ TestRunServices.TRACKER.register("run-id-check", "example-tests",
"example", "admin", Map.of());
+
+ Map<String, Object> result = TestRunServices.getTestRunStatus(dctx,
+ Map.of("runId", "run-id-check", "userLogin", userLogin));
+
+ assertThat(result.get("runId"), is("run-id-check"));
+ }
+
+ @Test
+ void isTestApiGloballyEnabledReadsTheGlobalFlag() {
+ Delegator delegator = mock(Delegator.class);
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled",
delegator))
+ .thenReturn("true");
+
+ assertThat(TestRunServices.isTestApiGloballyEnabled(delegator),
is(true));
+ }
+ }
+
+ @Test
+ void isTestApiEnabledForComponentDefaultsToTrueWhenUnset() {
+ Delegator delegator = mock(Delegator.class);
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ assertThat(TestRunServices.isTestApiEnabledForComponent(delegator,
"example"), is(true));
+ }
+ }
+
+ @Test
+ void isTestApiEnabledForComponentReflectsAnExplicitOverride() {
+ Delegator delegator = mock(Delegator.class);
+ try (MockedStatic<EntityUtilProperties> entityUtilProperties =
+ Mockito.mockStatic(EntityUtilProperties.class,
Mockito.CALLS_REAL_METHODS)) {
+ entityUtilProperties.when(() ->
EntityUtilProperties.getPropertyValue("testtools", "test.api.enabled.example",
delegator))
+ .thenReturn("false");
+
+ assertThat(TestRunServices.isTestApiEnabledForComponent(delegator,
"example"), is(false));
+ }
+ }
}