diff --git a/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApi.java b/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApi.java index c15088d..ebfa194 100644 --- a/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApi.java +++ b/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApi.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Calendar; +import java.util.List; import java.util.Optional; public class GatewayApi extends RetrofitApiBase { @@ -78,6 +79,10 @@ public void updateRuntimeCaseStatus(CaseStatusUpdateRequest request) throws CbCl executeAsync(retroApi.updateRuntimeCaseStatus(request.getRunId(), request.getInstanceId(), request)); } + public void bulkUpdateRuntimeCaseStatus(String runId, String instanceId, List requests) throws CbClientException { + executeAsync(retroApi.bulkUpdateRuntimeCaseStatus(runId, instanceId, requests)); + } + public void updateRuntimeSuiteStatus(SuiteStatusUpdateRequest request) throws CbClientException { executeAsync(retroApi.updateRuntimeSuiteStatus(request.getRunId(), request.getInstanceId(), request)); } diff --git a/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApiRetro.java b/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApiRetro.java index 4549443..82083f0 100644 --- a/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApiRetro.java +++ b/cb-kit-common/src/main/java/io/cloudbeat/common/client/api/GatewayApiRetro.java @@ -9,6 +9,8 @@ import retrofit2.http.POST; import retrofit2.http.Path; +import java.util.List; + public interface GatewayApiRetro { @POST("testresult/Status") Call updateTestCaseStatus(@Body TestStatusRequest statusRequest); @@ -16,6 +18,8 @@ public interface GatewayApiRetro { Call updateLoadTestMetrics(@Path("runId") String runId, @Path("instanceId") String instanceId, @Body LoadTestMetricsUpdateRequest request); @POST("testresult/runtime/run/{runId}/instance/{instanceId}/case/status") Call updateRuntimeCaseStatus(@Path("runId") String runId, @Path("instanceId") String instanceId, @Body CaseStatusUpdateRequest request); + @POST("testresult/runtime/run/{runId}/instance/{instanceId}/case/status/bulk") + Call bulkUpdateRuntimeCaseStatus(@Path("runId") String runId, @Path("instanceId") String instanceId, @Body List requests); @POST("testresult/runtime/run/{runId}/instance/{instanceId}/suite/status") Call updateRuntimeSuiteStatus(@Path("runId") String runId, @Path("instanceId") String instanceId, @Body SuiteStatusUpdateRequest request); } diff --git a/cb-kit-common/src/main/java/io/cloudbeat/common/reporter/CbTestReporter.java b/cb-kit-common/src/main/java/io/cloudbeat/common/reporter/CbTestReporter.java index 60f9ad2..72b04ee 100755 --- a/cb-kit-common/src/main/java/io/cloudbeat/common/reporter/CbTestReporter.java +++ b/cb-kit-common/src/main/java/io/cloudbeat/common/reporter/CbTestReporter.java @@ -424,6 +424,59 @@ public void reportPendingCase(final String name, final String fqn, final String } } + /** + * A case's name/fqn/parent, as known upfront during test discovery - before any CaseResult + * exists. See {@link #reportPendingCases}. + */ + public static class PendingCaseInfo { + public final String name; + public final String fqn; + public final String parentFqn; + public final String parentName; + + public PendingCaseInfo(String name, String fqn, String parentFqn, String parentName) { + this.name = name; + this.fqn = fqn; + this.parentFqn = parentFqn; + this.parentName = parentName; + } + } + + /** + * Announces every case that will run, before any of them start - the bulk equivalent of + * {@link #reportPendingCase}. A whole suite's worth of cases reported one at a time each pays + * its own HTTP round trip, throttled by OkHttp's default 5-concurrent-per-host cap, so the + * full Pending list can take a while to finish populating on the live progress screen for a + * suite with many test methods. Sending them all in a single request avoids that entirely. + */ + public void reportPendingCases(final List cases) { + if (!config.isRunningInCb() || !this.gatewayApi.isPresent() || result == null || cases.isEmpty()) + return; + try { + List reqList = new ArrayList<>(cases.size()); + for (PendingCaseInfo c : cases) { + CaseStatusUpdateRequest req = new CaseStatusUpdateRequest(); + req.setTimestamp(System.currentTimeMillis()); + req.setRunId(result.getRunId()); + req.setInstanceId(result.getInstanceId()); + req.setId(UUID.randomUUID().toString()); + req.setFqn(c.fqn); + req.setName(c.name); + if (c.parentFqn != null) { + req.setParentFqn(c.parentFqn); + req.setParentName(c.parentName); + } + req.setRunStatus(RunStatusEnum.PENDING); + req.setFramework(frameworkName); + req.setLanguage(language); + reqList.add(req); + } + this.gatewayApi.get().bulkUpdateRuntimeCaseStatus(result.getRunId(), result.getInstanceId(), reqList); + } catch (CbClientException e) { + // best-effort - never fail the test run because live-status reporting failed + } + } + /** * Reports a suite's status to the new Redis-backed runtime status API. See * {@link #reportRuntimeCaseStatus} for the gating/best-effort rationale. diff --git a/cb-kit-junit5/src/main/java/io/cloudbeat/junit/CbJunitExecutionListener.java b/cb-kit-junit5/src/main/java/io/cloudbeat/junit/CbJunitExecutionListener.java index 9e2f81a..194cac9 100644 --- a/cb-kit-junit5/src/main/java/io/cloudbeat/junit/CbJunitExecutionListener.java +++ b/cb-kit-junit5/src/main/java/io/cloudbeat/junit/CbJunitExecutionListener.java @@ -2,6 +2,7 @@ import io.cloudbeat.common.CbTestContext; import io.cloudbeat.common.reporter.CbTestReporter; +import io.cloudbeat.common.reporter.CbTestReporter.PendingCaseInfo; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.descriptor.MethodSource; @@ -9,6 +10,8 @@ import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; /** @@ -32,12 +35,21 @@ public void testPlanExecutionStarted(TestPlan testPlan) { reporter.setFramework("JUnit", "5"); JunitReporterUtils.startInstance(reporter, true); } + // Suites (classes) are reported individually - there's no bulk endpoint for them, but a + // suite typically only has a handful of classes, unlike the potentially large number of + // test methods below, so the per-request overhead there is negligible. + List pendingCases = new ArrayList<>(); for (TestIdentifier root : testPlan.getRoots()) { - reportPending(testPlan, root, reporter); + reportPending(testPlan, root, reporter, pendingCases); } + // Cases are collected and sent as a single bulk request instead of one HTTP call per + // case - with dozens of test methods, reporting them one at a time (even as fire-and-forget + // calls) gets throttled by OkHttp's default 5-concurrent-per-host cap, so the full Pending + // list can take a noticeable while to finish appearing on the live progress screen. + reporter.reportPendingCases(pendingCases); } - private void reportPending(TestPlan testPlan, TestIdentifier identifier, CbTestReporter reporter) { + private void reportPending(TestPlan testPlan, TestIdentifier identifier, CbTestReporter reporter, List pendingCases) { Optional source = identifier.getSource(); if (source.isPresent() && source.get() instanceof ClassSource) { final String classFqn = ((ClassSource) source.get()).getClassName(); @@ -47,10 +59,10 @@ else if (source.isPresent() && source.get() instanceof MethodSource) { final MethodSource methodSource = (MethodSource) source.get(); final String classFqn = methodSource.getClassName(); final String methodFqn = String.format(JunitReporterUtils.JAVA_METHOD_FQN_FORMAT, classFqn, methodSource.getMethodName()); - reporter.reportPendingCase(identifier.getDisplayName(), methodFqn, classFqn, classFqn); + pendingCases.add(new PendingCaseInfo(identifier.getDisplayName(), methodFqn, classFqn, classFqn)); } for (TestIdentifier child : testPlan.getChildren(identifier)) { - reportPending(testPlan, child, reporter); + reportPending(testPlan, child, reporter, pendingCases); } } } diff --git a/cb-kit-testng/src/main/java/io/cloudbeat/testng/CbTestNGReporter.java b/cb-kit-testng/src/main/java/io/cloudbeat/testng/CbTestNGReporter.java index 540d3a7..eb492bf 100644 --- a/cb-kit-testng/src/main/java/io/cloudbeat/testng/CbTestNGReporter.java +++ b/cb-kit-testng/src/main/java/io/cloudbeat/testng/CbTestNGReporter.java @@ -1,6 +1,7 @@ package io.cloudbeat.testng; import io.cloudbeat.common.reporter.CbTestReporter; +import io.cloudbeat.common.reporter.CbTestReporter.PendingCaseInfo; import io.cloudbeat.common.reporter.model.CaseResult; import io.cloudbeat.common.reporter.model.StepResult; import io.cloudbeat.common.reporter.model.SuiteResult; @@ -11,6 +12,8 @@ import org.testng.ITestResult; import org.testng.xml.XmlSuite; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -59,11 +62,14 @@ public static void reportPendingMethods(CbTestReporter reporter, ISuite suite) { if (!reporter.getInstance().isPresent()) return; final String suiteFqn = generateFqnForSuite(suite.getXmlSuite()); + // collected and sent as a single bulk request - see CbTestReporter.reportPendingCases + List pendingCases = new ArrayList<>(); for (ITestNGMethod testMethod : suite.getAllMethods()) { final String methodDisplayName = testMethod.getMethodName(); final String methodFqn = fixFqnWithHash(testMethod.getQualifiedName()); - reporter.reportPendingCase(methodDisplayName, methodFqn, suiteFqn, suite.getName()); + pendingCases.add(new PendingCaseInfo(methodDisplayName, methodFqn, suiteFqn, suite.getName())); } + reporter.reportPendingCases(pendingCases); } public static void endSuite(CbTestReporter reporter, ISuite suite) {