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

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new f74fd689b6 minor refactor: add aggregate test result summary to build
f74fd689b6 is described below

commit f74fd689b69ec2f3fadea19c095d21399ca9b56a
Author: Paul King <[email protected]>
AuthorDate: Wed Apr 15 17:59:41 2026 +1000

    minor refactor: add aggregate test result summary to build
---
 .../main/groovy/org.apache.groovy-tested.gradle    | 24 ++++--
 .../gradle/TestResultAggregatorService.groovy      | 89 ++++++++++++++++++++++
 2 files changed, 106 insertions(+), 7 deletions(-)

diff --git a/build-logic/src/main/groovy/org.apache.groovy-tested.gradle 
b/build-logic/src/main/groovy/org.apache.groovy-tested.gradle
index 99d6d429bf..ef13294b61 100644
--- a/build-logic/src/main/groovy/org.apache.groovy-tested.gradle
+++ b/build-logic/src/main/groovy/org.apache.groovy-tested.gradle
@@ -19,6 +19,7 @@
 
 import org.apache.groovy.gradle.ConcurrentExecutionControlBuildService
 import org.apache.groovy.gradle.TargetJavaHomeSupport
+import org.apache.groovy.gradle.TestResultAggregatorService
 
 // TODO: Instead of adding to the test sources, these should be a
 // separate source set so that we can run spec tests in isolation
@@ -42,6 +43,9 @@ dependencies {
     }
 }
 
+def aggregator = TestResultAggregatorService.register(
+    objects.newInstance(TestServices).buildEventsListenerRegistry, gradle)
+
 tasks.withType(Test).configureEach {
     def fs = objects.newInstance(TestServices).fileSystemOperations
     def grapeDirectory = new File(temporaryDir, '.groovy')
@@ -88,6 +92,7 @@ tasks.withType(Test).configureEach {
     }
 
     useJUnitPlatform()
+    usesService(aggregator)
     usesService(ConcurrentExecutionControlBuildService.restrict(Test, gradle, 
2))
 
     doFirst {
@@ -105,13 +110,16 @@ tasks.withType(Test).configureEach {
     }
 
     afterSuite { desc, result ->
-        if (!desc.parent && result.resultType == 
TestResult.ResultType.SUCCESS) {
-            def green = '\u001B[32m'
-            def yellow = '\u001B[33m'
-            def reset = '\u001B[0m'
-            logger.lifecycle "${desc.name}: 
${green}${result.successfulTestCount} passed${reset}, " +
-                "${yellow}${result.skippedTestCount} skipped${reset} " +
-                "(${result.testCount} tests)"
+        if (!desc.parent) {
+            aggregator.get().recordSuite(result.successfulTestCount, 
result.failedTestCount, result.skippedTestCount)
+            if (result.resultType == TestResult.ResultType.SUCCESS) {
+                def green = '\u001B[32m'
+                def yellow = '\u001B[33m'
+                def reset = '\u001B[0m'
+                logger.lifecycle "${desc.name}: 
${green}${result.successfulTestCount} passed${reset}, " +
+                    "${yellow}${result.skippedTestCount} skipped${reset} " +
+                    "(${result.testCount} tests)"
+            }
         }
     }
 }
@@ -170,4 +178,6 @@ Closure buildExcludeFilter(boolean legacyTestSuite) {
 interface TestServices {
     @javax.inject.Inject
     FileSystemOperations getFileSystemOperations()
+    @javax.inject.Inject
+    org.gradle.build.event.BuildEventsListenerRegistry 
getBuildEventsListenerRegistry()
 }
diff --git 
a/build-logic/src/main/groovy/org/apache/groovy/gradle/TestResultAggregatorService.groovy
 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/TestResultAggregatorService.groovy
new file mode 100644
index 0000000000..e525f231b7
--- /dev/null
+++ 
b/build-logic/src/main/groovy/org/apache/groovy/gradle/TestResultAggregatorService.groovy
@@ -0,0 +1,89 @@
+/*
+ *  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.groovy.gradle
+
+import groovy.transform.CompileStatic
+import org.gradle.api.provider.Provider
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+import org.gradle.build.event.BuildEventsListenerRegistry
+import org.gradle.tooling.events.FinishEvent
+import org.gradle.tooling.events.OperationCompletionListener
+
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * Aggregates test results across all subprojects and prints
+ * a summary at the end of the build.
+ *
+ * <p>Suite-level results are fed in via {@link #recordSuite} from
+ * {@code afterSuite} callbacks. The service also implements
+ * {@link OperationCompletionListener} so it stays alive until
+ * all tasks complete; {@link #close()} prints the aggregate.</p>
+ */
+@CompileStatic
+abstract class TestResultAggregatorService implements BuildService<Params>, 
OperationCompletionListener, AutoCloseable {
+
+    interface Params extends BuildServiceParameters {}
+
+    private final AtomicLong totalPassed = new AtomicLong()
+    private final AtomicLong totalFailed = new AtomicLong()
+    private final AtomicLong totalSkipped = new AtomicLong()
+
+    /** Called from afterSuite when a root suite completes. */
+    void recordSuite(long passed, long failed, long skipped) {
+        totalPassed.addAndGet(passed)
+        totalFailed.addAndGet(failed)
+        totalSkipped.addAndGet(skipped)
+    }
+
+    @Override
+    void onFinish(FinishEvent event) {
+        // Required by OperationCompletionListener; keeps the service
+        // alive until the last task finishes so close() runs at build end.
+    }
+
+    @Override
+    void close() {
+        long passed = totalPassed.get()
+        long failed = totalFailed.get()
+        long skipped = totalSkipped.get()
+        long total = passed + failed + skipped
+        if (total == 0) return
+
+        String green = '\u001B[32m'
+        String red = '\u001B[31m'
+        String yellow = '\u001B[33m'
+        String reset = '\u001B[0m'
+
+        String failText = failed ? "${red}${failed} failed${reset}, " : ''
+        System.out.println("Aggregate test results: 
${failText}${green}${passed} passed${reset}, " +
+                "${yellow}${skipped} skipped${reset} " +
+                "(${total} tests)")
+        System.out.flush()
+    }
+
+    static Provider<TestResultAggregatorService> 
register(BuildEventsListenerRegistry registry,
+                                                          
org.gradle.api.invocation.Gradle gradle) {
+        Provider<TestResultAggregatorService> provider = 
gradle.sharedServices.registerIfAbsent(
+                'testResultAggregator', TestResultAggregatorService) {}
+        registry.onTaskCompletion(provider)
+        return provider
+    }
+}

Reply via email to