diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index c50e88464..1edb74105 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -21,7 +21,6 @@
import org.apache.unomi.itests.graphql.*;
import org.apache.unomi.itests.migration.MigrationIT;
import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
/**
@@ -29,7 +28,7 @@
*
* @author Sergiy Shyrkov
*/
-@RunWith(Suite.class)
+@RunWith(ProgressSuite.class)
@SuiteClasses({
Migrate16xToCurrentVersionIT.class,
MigrationIT.class,
diff --git a/itests/src/test/java/org/apache/unomi/itests/ProgressListener.java b/itests/src/test/java/org/apache/unomi/itests/ProgressListener.java
new file mode 100644
index 000000000..b39355a43
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/ProgressListener.java
@@ -0,0 +1,405 @@
+/*
+ * 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.unomi.itests;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A comprehensive JUnit test run listener that provides enhanced progress reporting
+ * with visual elements, timing information, and motivational quotes during test execution.
+ *
+ *
This listener extends JUnit's {@link RunListener} to provide real-time feedback
+ * about test execution progress. It features:
+ *
+ * - ASCII art logo display at test suite startup
+ * - Real-time progress bar with percentage completion
+ * - Colorized output (when ANSI is supported)
+ * - Estimated time remaining calculations
+ * - Test success/failure counters
+ * - Top 10 slowest tests tracking and reporting
+ * - Motivational quotes displayed at progress milestones
+ * - CSV-formatted performance data output
+ *
+ *
+ * The listener automatically detects ANSI color support based on the terminal
+ * environment and adjusts output accordingly. When ANSI is not supported,
+ * plain text output is used instead.
+ *
+ * Example usage in test configuration:
+ * {@code
+ * JUnitCore core = new JUnitCore();
+ * ProgressListener listener = new ProgressListener(totalTestCount, completedCounter);
+ * core.addListener(listener);
+ * core.run(testClasses);
+ * }
+ *
+ * The listener tracks test execution times and maintains a priority queue
+ * of the slowest tests, which is reported at the end of the test run along
+ * with CSV-formatted data for further analysis.
+ *
+ * @author Apache Unomi
+ * @since 3.0.0
+ * @see org.junit.runner.notification.RunListener
+ * @see org.junit.runner.Description
+ * @see org.junit.runner.Result
+ */
+public class ProgressListener extends RunListener {
+
+ /** ANSI escape code to reset text formatting */
+ private static final String RESET = "\u001B[0m";
+ /** ANSI escape code for green text color */
+ private static final String GREEN = "\u001B[32m";
+ /** ANSI escape code for yellow text color */
+ private static final String YELLOW = "\u001B[33m";
+ /** ANSI escape code for red text color */
+ private static final String RED = "\u001B[31m";
+ /** ANSI escape code for cyan text color */
+ private static final String CYAN = "\u001B[36m";
+ /** ANSI escape code for blue text color */
+ private static final String BLUE = "\u001B[34m";
+
+ /** Array of motivational quotes displayed at progress milestones */
+ private static final String[] QUOTES = {
+ "Success is not final, failure is not fatal: It is the courage to continue that counts. - Winston Churchill",
+ "Believe you can and you're halfway there. - Theodore Roosevelt",
+ "Don't watch the clock; do what it does. Keep going. - Sam Levenson",
+ "It does not matter how slowly you go as long as you do not stop. - Confucius",
+ "Hardships often prepare ordinary people for an extraordinary destiny. - C.S. Lewis"
+ };
+
+ /**
+ * Inner class representing a test execution time record.
+ * Used to track individual test performance for reporting the slowest tests.
+ */
+ private static class TestTime {
+ /** The display name of the test */
+ String name;
+ /** The execution time in milliseconds */
+ long time;
+
+ /**
+ * Creates a new test time record.
+ *
+ * @param name the display name of the test
+ * @param time the execution time in milliseconds
+ */
+ TestTime(String name, long time) {
+ this.name = name;
+ this.time = time;
+ }
+ }
+
+ /** Total number of tests to be executed */
+ private final int totalTests;
+ /** Thread-safe counter for completed tests */
+ private final AtomicInteger completedTests;
+ /** Thread-safe counter for successful tests */
+ private final AtomicInteger successfulTests = new AtomicInteger(0);
+ /** Thread-safe counter for failed tests */
+ private final AtomicInteger failedTests = new AtomicInteger(0);
+ /** Priority queue to track the slowest tests (limited to top 10) */
+ private final PriorityQueue slowTests;
+ /** Flag indicating whether ANSI color codes are supported in the terminal */
+ private final boolean ansiSupported;
+ /** Timestamp when the test suite started */
+ private long startTime = System.currentTimeMillis();
+ /** Timestamp when the current individual test started */
+ private long startTestTime = System.currentTimeMillis();
+
+ /**
+ * Creates a new ProgressListener instance.
+ *
+ * @param totalTests the total number of tests that will be executed
+ * @param completedTests a thread-safe counter that tracks the number of completed tests
+ * (this should be shared with the test runner for accurate progress tracking)
+ */
+ public ProgressListener(int totalTests, AtomicInteger completedTests) {
+ this.totalTests = totalTests;
+ this.completedTests = completedTests;
+ this.slowTests = new PriorityQueue<>((t1, t2) -> Long.compare(t1.time, t2.time));
+ this.ansiSupported = isAnsiSupported();
+ }
+
+ /**
+ * Determines if the current terminal supports ANSI color codes.
+ *
+ * @return true if ANSI colors are supported, false otherwise
+ */
+ private boolean isAnsiSupported() {
+ String term = System.getenv("TERM");
+ return System.console() != null && term != null && term.contains("xterm");
+ }
+
+ /**
+ * Applies ANSI color codes to text if the terminal supports them.
+ *
+ * @param text the text to colorize
+ * @param color the ANSI color code to apply
+ * @return the colorized text if ANSI is supported, otherwise the original text
+ */
+ private String colorize(String text, String color) {
+ if (ansiSupported) {
+ return color + text + RESET;
+ }
+ return text;
+ }
+
+ /**
+ * Called when the test run starts. Displays an ASCII art logo and welcome message.
+ *
+ * @param description the description of the test run
+ */
+ @Override
+ public void testRunStarted(Description description) {
+ startTime = System.currentTimeMillis();
+
+ // Provided ASCII Art Logo
+ String[] logoLines = {
+ " ____ ___ A P A C H E .__ ",
+ " | | \\____ ____ _____ |__| ",
+ " | | / \\ / _ \\ / \\| | ",
+ " | | / | ( <_> ) Y Y \\ | ",
+ " |______/|___| /\\____/|__|_| /__| ",
+ " \\/ \\/ ",
+ " ",
+ " I N T E G R A T I O N T E S T S "
+ };
+
+ // Box dimensions
+ int totalWidth = 68;
+ String topBorder = "╔" + "═".repeat(totalWidth) + "╗";
+ String bottomBorder = "╚" + "═".repeat(totalWidth) + "╝";
+
+ // Print the top border
+ System.out.println(colorize(topBorder, CYAN));
+
+ // Center-align each logo line
+ for (String line : logoLines) {
+ int padding = (totalWidth - line.length()) / 2;
+ String paddedLine = " ".repeat(padding) + line + " ".repeat(totalWidth - padding - line.length());
+ System.out.println(colorize("║" + paddedLine + "║", CYAN));
+ }
+
+ // Print the progress message
+ String progressMessage = "Starting test suite with " + totalTests + " tests. Good luck!";
+ int progressPadding = (totalWidth - progressMessage.length()) / 2;
+ String paddedProgressMessage = " ".repeat(progressPadding) + progressMessage + " ".repeat(totalWidth - progressPadding - progressMessage.length());
+
+ System.out.println(colorize("║" + paddedProgressMessage + "║", CYAN));
+
+ // Print the bottom border
+ System.out.println(colorize(bottomBorder, CYAN));
+ }
+
+ /**
+ * Called when an individual test starts. Records the start time for timing calculations.
+ *
+ * @param description the description of the test that started
+ */
+ @Override
+ public void testStarted(Description description) {
+ startTestTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Called when an individual test finishes successfully. Updates counters and displays progress.
+ *
+ * @param description the description of the test that finished
+ */
+ @Override
+ public void testFinished(Description description) {
+ long testDuration = System.currentTimeMillis() - startTestTime;
+ completedTests.incrementAndGet();
+ successfulTests.incrementAndGet(); // Default to success unless a failure is recorded separately.
+ slowTests.add(new TestTime(description.getDisplayName(), testDuration));
+ if (slowTests.size() > 10) {
+ // Remove the smallest time, keeping only the top 5 longest
+ slowTests.poll();
+ }
+ displayProgress();
+ }
+
+ /**
+ * Called when a test fails. Updates failure counters and displays the failure message.
+ *
+ * @param failure the failure information
+ */
+ @Override
+ public void testFailure(Failure failure) {
+ successfulTests.decrementAndGet(); // Remove the previous success count for this test.
+ failedTests.incrementAndGet();
+ System.out.println(colorize("Test failed: " + failure.getDescription(), RED));
+ displayProgress();
+ }
+
+ /**
+ * Called when the entire test run finishes. Displays final statistics and performance data.
+ *
+ * @param result the final result of the test run
+ */
+ @Override
+ public void testRunFinished(Result result) {
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ String resultMessage = result.wasSuccessful()
+ ? colorize("SUCCESS!", GREEN)
+ : colorize("FAILURE", RED);
+ System.out.printf("%s═══════════════════════════════════════════════════════════%n" +
+ "Test suite finished in %s%s%s. Result: %s%n" +
+ "Successful: %s%d%s, Failed: %s%d%s%n" +
+ "═══════════════════════════════════════════════════════════%n",
+ ansiSupported ? CYAN : "",
+ ansiSupported ? YELLOW : "",
+ formatTime(elapsedTime),
+ ansiSupported ? RESET : "",
+ resultMessage,
+ ansiSupported ? GREEN : "",
+ successfulTests.get(),
+ ansiSupported ? RESET : "",
+ ansiSupported ? RED : "",
+ failedTests.get(),
+ ansiSupported ? RESET : "");
+
+ // Display the top 10 slowest tests
+ System.out.printf("Top 10 Slowest Tests:%n");
+ // Prepare CSV data
+ StringBuilder csvBuilder = new StringBuilder();
+ csvBuilder.append("Rank,Test Name,Duration (ms)\n");
+
+ AtomicInteger rank = new AtomicInteger(1);
+ slowTests.stream()
+ .sorted((t1, t2) -> Long.compare(t2.time, t1.time)) // Sort by descending order
+ .limit(10)
+ .forEach(test -> csvBuilder.append(String.format("%d,\"%s\",%d%n",
+ rank.getAndIncrement(), escapeCsv(test.name), test.time)));
+
+ // Output CSV
+ System.out.println(csvBuilder.toString());
+ System.out.println("═══════════════════════════════════════════════════════════");
+
+ }
+
+ /**
+ * Escapes special characters for CSV compatibility.
+ *
+ * @param value the string value to escape
+ * @return the escaped string suitable for CSV output
+ */
+ private String escapeCsv(String value) {
+ if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
+ return "\"" + value.replace("\"", "\"\"") + "\"";
+ }
+ return value;
+ }
+
+ /**
+ * Displays the current progress of the test run including progress bar,
+ * percentage completion, estimated time remaining, and success/failure counts.
+ * Also displays motivational quotes at progress milestones.
+ */
+ private void displayProgress() {
+ int completed = completedTests.get();
+ long elapsedTime = System.currentTimeMillis() - startTime;
+
+ // Avoid division by very low completed count; use a floor value
+ int stableCompleted = Math.max(completed, 1);
+ double averageTestTimeMillis = elapsedTime / (double) stableCompleted;
+
+ // Calculate estimated time remaining
+ long estimatedRemainingTime = (long) (averageTestTimeMillis * (totalTests - completed));
+ String progressBar = generateProgressBar(((double) completed / totalTests) * 100);
+ String humanReadableTime = formatTime(estimatedRemainingTime);
+
+ System.out.printf("[%s] %sProgress: %s%.2f%%%s (%d/%d tests). Estimated time remaining: %s%s%s. " +
+ "Successful: %s%d%s, Failed: %s%d%s%n",
+ progressBar,
+ ansiSupported ? BLUE : "",
+ ansiSupported ? GREEN : "",
+ ((double) completed / totalTests) * 100,
+ ansiSupported ? RESET : "",
+ completed,
+ totalTests,
+ ansiSupported ? YELLOW : "",
+ humanReadableTime,
+ ansiSupported ? RESET : "",
+ ansiSupported ? GREEN : "",
+ successfulTests.get(),
+ ansiSupported ? RESET : "",
+ ansiSupported ? RED : "",
+ failedTests.get(),
+ ansiSupported ? RESET : "");
+
+ if (completed % Math.max(1, totalTests / 10) == 0 && completed < totalTests) {
+ String quote = QUOTES[completed % QUOTES.length];
+ System.out.println(colorize("Motivational Quote: " + quote, YELLOW));
+ }
+ }
+
+ /**
+ * Formats a time duration in milliseconds into a human-readable string.
+ *
+ * @param timeInMillis the time duration in milliseconds
+ * @return a formatted time string (e.g., "1h 23m 45s" or "2m 30s")
+ */
+ private String formatTime(long timeInMillis) {
+ long seconds = timeInMillis / 1000;
+ long hours = seconds / 3600;
+ long minutes = (seconds % 3600) / 60;
+ seconds = seconds % 60;
+
+ if (hours > 999) {
+ // Fallback for extremely large times
+ return ">999h";
+ }
+
+ StringBuilder timeBuilder = new StringBuilder();
+ if (hours > 0) {
+ timeBuilder.append(hours).append("h ");
+ }
+ if (minutes > 0 || hours > 0) { // Show minutes if hours are non-zero
+ timeBuilder.append(minutes).append("m ");
+ }
+ timeBuilder.append(seconds).append("s");
+
+ return timeBuilder.toString().trim(); // Trim any trailing spaces
+ }
+
+ /**
+ * Generates a visual progress bar based on the completion percentage.
+ *
+ * @param progressPercentage the completion percentage (0.0 to 100.0)
+ * @return a string representation of the progress bar with appropriate colors
+ */
+ private String generateProgressBar(double progressPercentage) {
+ int totalBars = 30;
+ int completedBars = (int) (progressPercentage / (100.0 / totalBars));
+ StringBuilder progressBar = new StringBuilder();
+ for (int i = 0; i < completedBars; i++) {
+ progressBar.append(ansiSupported ? GREEN + "█" + RESET : "#");
+ }
+ for (int i = completedBars; i < totalBars; i++) {
+ progressBar.append(ansiSupported ? "░" : "-");
+ }
+ return progressBar.toString();
+ }
+
+}
diff --git a/itests/src/test/java/org/apache/unomi/itests/ProgressSuite.java b/itests/src/test/java/org/apache/unomi/itests/ProgressSuite.java
new file mode 100644
index 000000000..0c9f70af2
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/ProgressSuite.java
@@ -0,0 +1,186 @@
+/*
+ * 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.unomi.itests;
+
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.Suite;
+import org.junit.runners.model.InitializationError;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A custom JUnit test suite runner that provides enhanced progress reporting
+ * during test execution by integrating with the {@link ProgressListener}.
+ *
+ * This suite extends JUnit's standard {@link Suite} runner to automatically
+ * count test methods across the entire class hierarchy and provide real-time
+ * progress feedback. It features:
+ *
+ * - Automatic test method counting across class hierarchies
+ * - Integration with {@link ProgressListener} for enhanced progress reporting
+ * - Thread-safe progress tracking using atomic counters
+ * - Support for nested test classes and inheritance
+ *
+ *
+ * The suite automatically counts all methods annotated with {@code @Test}
+ * in the specified test classes and their superclasses, providing an accurate
+ * total count for progress reporting.
+ *
+ * Example usage:
+ * {@code
+ * @RunWith(ProgressSuite.class)
+ * @Suite.SuiteClasses({
+ * TestClass1.class,
+ * TestClass2.class,
+ * TestClass3.class
+ * })
+ * public class AllTestsSuite {
+ * // This class serves as a container for the test suite
+ * }
+ * }
+ *
+ * The suite will automatically:
+ *
+ * - Count all test methods in the specified classes and their hierarchies
+ * - Create a {@link ProgressListener} with the accurate test count
+ * - Display real-time progress with visual elements and timing information
+ * - Provide detailed performance statistics at completion
+ *
+ *
+ * @author Apache Unomi
+ * @since 3.0.0
+ * @see org.junit.runners.Suite
+ * @see org.apache.unomi.itests.ProgressListener
+ * @see org.junit.runner.RunWith
+ * @see org.junit.runners.Suite.SuiteClasses
+ */
+public class ProgressSuite extends Suite {
+
+ /** Total number of test methods across all classes in the suite */
+ private final int totalTests;
+ /** Thread-safe counter for completed tests, shared with ProgressListener */
+ private final AtomicInteger completedTests = new AtomicInteger(0);
+
+ /**
+ * Creates a new ProgressSuite instance for the specified test suite class.
+ *
+ * The constructor initializes the suite by:
+ *
+ * - Extracting test classes from the {@code @Suite.SuiteClasses} annotation
+ * - Counting all test methods across the class hierarchies
+ * - Initializing the progress tracking infrastructure
+ *
+ *
+ * @param klass the test suite class that must be annotated with {@code @Suite.SuiteClasses}
+ * @throws InitializationError if the class is not properly annotated or if there are
+ * issues with the test class configuration
+ */
+ public ProgressSuite(Class> klass) throws InitializationError {
+ super(klass, getAnnotatedClasses(klass));
+ this.totalTests = countTestMethods(getAnnotatedClasses(klass));
+ }
+
+ /**
+ * Extracts the test classes from the {@code @Suite.SuiteClasses} annotation.
+ *
+ * @param klass the test suite class to examine
+ * @return an array of test classes specified in the annotation
+ * @throws InitializationError if the class is not annotated with {@code @Suite.SuiteClasses}
+ */
+ private static Class>[] getAnnotatedClasses(Class> klass) throws InitializationError {
+ Suite.SuiteClasses annotation = klass.getAnnotation(Suite.SuiteClasses.class);
+ if (annotation == null) {
+ throw new InitializationError(
+ String.format("Class '%s' must have a @Suite.SuiteClasses annotation", klass.getName()));
+ }
+ return annotation.value();
+ }
+
+ /**
+ * Counts the total number of test methods across all specified test classes.
+ *
+ * @param testClasses array of test classes to count methods in
+ * @return the total number of methods annotated with {@code @Test}
+ */
+ private static int countTestMethods(Class>[] testClasses) {
+ int count = 0;
+ for (Class> testClass : testClasses) {
+ count += countTestMethodsInClassHierarchy(testClass);
+ }
+ return count;
+ }
+
+ /**
+ * Recursively counts test methods in a class and its entire inheritance hierarchy.
+ *
+ * This method traverses the class hierarchy upward from the given class,
+ * counting all methods annotated with {@code @Test} in each class. It stops
+ * at {@code Object.class} to avoid counting system methods.
+ *
+ * @param clazz the class to count test methods in (including superclasses)
+ * @return the number of test methods found in this class and its hierarchy
+ */
+ private static int countTestMethodsInClassHierarchy(Class> clazz) {
+ int count = 0;
+ if (clazz == null || clazz == Object.class) {
+ return 0; // Stop at the base class
+ }
+ for (Method method : clazz.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(Test.class)) {
+ count++;
+ }
+ }
+ // Recurse into the superclass
+ count += countTestMethodsInClassHierarchy(clazz.getSuperclass());
+ return count;
+ }
+
+ /**
+ * Executes the test suite with enhanced progress reporting.
+ *
+ * This method overrides the standard suite execution to integrate
+ * the {@link ProgressListener} for real-time progress feedback. It:
+ *
+ * - Creates a {@link ProgressListener} with the accurate test count
+ * - Manually triggers the test run started event (since the listener
+ * is registered after this event would normally be fired)
+ * - Registers the listener with the run notifier
+ * - Delegates to the parent suite execution
+ *
+ *
+ * Note: Two separate {@link ProgressListener} instances are created:
+ * one for manual event triggering and another for the notifier. This is
+ * necessary because the test run started event is fired before listeners
+ * can be registered.
+ *
+ * @param notifier the run notifier to use for test execution notifications
+ */
+ @Override
+ public void run(RunNotifier notifier) {
+ ProgressListener listener = new ProgressListener(totalTests, completedTests);
+ Description suiteDescription = getDescription();
+ // We call this manually as we register the listener after this event has already been triggered.
+ listener.testRunStarted(suiteDescription);
+
+ notifier.addListener(new ProgressListener(totalTests, completedTests));
+ super.run(notifier);
+ }
+
+}