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

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 9c5d68f70b565659e86fa67344bf510eb81522a6
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Feb 20 10:36:06 2024 +0100

    Logs handled by the wrong `LoggingWatcher` during execution of tests in 
parallel.
    Require Java 16 for the test (but not for the main code, which still on 
Java 11)
    for avoiding an unreliable hack in `JDK16` placeholder class.
---
 endorsed/build.gradle.kts                          |  2 ++
 .../factory/ConcurrentAuthorityFactoryTest.java    | 38 +++++++++++++-------
 .../main/org/apache/sis/pending/jdk/JDK16.java     | 29 ----------------
 .../test/org/apache/sis/test/LoggingWatcher.java   | 40 ++++++++++------------
 4 files changed, 46 insertions(+), 63 deletions(-)

diff --git a/endorsed/build.gradle.kts b/endorsed/build.gradle.kts
index d3198451b9..6a8f136011 100644
--- a/endorsed/build.gradle.kts
+++ b/endorsed/build.gradle.kts
@@ -104,6 +104,8 @@ tasks.compileTestJava {
     srcDir.list().forEach {
         addRead(options.compilerArgs, it, 
"org.apache.sis.test.endorsed,org.junit.jupiter.api")
     }
+    options.compilerArgs.add("-source")         // "source", not "release", 
because we accept any target version.
+    options.compilerArgs.add("16")              // Minimal Java version 
required by some API that the tests use.
     addExportForTests(options.compilerArgs)
 }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
index 884c82c107..b1c7e84cf1 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
 import java.lang.reflect.Field;
 import org.opengis.util.FactoryException;
 import static 
org.apache.sis.util.internal.StandardDateFormat.NANOS_PER_MILLISECOND;
@@ -118,10 +119,11 @@ public final class ConcurrentAuthorityFactoryTest extends 
TestCase {
         assertEquals(1, factory.countAvailableDataAccess(), "Expected one 
valid DAO.");
         assertFalse (createdDAOs.get(0).isClosed(),         "Should not be 
disposed yet.");
 
-        sleepUntilAfterTimeout(3 * 
ConcurrentAuthorityFactory.TIMEOUT_RESOLUTION, factory);
-        assertEquals(createdDAOs, factory.createdDAOs(),    "Expected no new 
DAO.");
-        assertEquals(0, factory.countAvailableDataAccess(), "Worker should be 
disposed.");
-        assertTrue  (createdDAOs.get(0).isClosed(),         "Worker should be 
disposed.");
+        boolean expired;
+        expired = sleepUntilAfterTimeout(3 * 
ConcurrentAuthorityFactory.TIMEOUT_RESOLUTION, factory);
+        assertEquals(createdDAOs, factory.createdDAOs(),    
unexpectedDAO("Expected no new DAO.",       expired));
+        assertEquals(0, factory.countAvailableDataAccess(), 
unexpectedDAO("Worker should be disposed.", expired));
+        assertTrue  (createdDAOs.get(0).isClosed(),         
unexpectedDAO("Worker should be disposed.", expired));
         /*
          * Ask again for the same object and check that no new DAO
          * were created because the value was taken from the cache.
@@ -156,11 +158,11 @@ public final class ConcurrentAuthorityFactoryTest extends 
TestCase {
         assertFalse (createdDAOs.get(1).isClosed(),         "Should not be 
disposed yet.");
         assertEquals(createdDAOs, factory.createdDAOs(),    "Expected no new 
DAO.");
 
-        sleepUntilAfterTimeout(3 * 
ConcurrentAuthorityFactory.TIMEOUT_RESOLUTION, factory);
-        assertEquals(createdDAOs, factory.createdDAOs(),    "Expected no new 
DAO.");
-        assertEquals(0, factory.countAvailableDataAccess(), "Worker should be 
disposed.");
-        assertTrue  (createdDAOs.get(1).isClosed(),         "Worker should be 
disposed.");
-        assertTrue  (createdDAOs.get(0).isClosed(),         "Worker should be 
disposed.");
+        expired = sleepUntilAfterTimeout(3 * 
ConcurrentAuthorityFactory.TIMEOUT_RESOLUTION, factory);
+        assertEquals(createdDAOs, factory.createdDAOs(),    
unexpectedDAO("Expected no new DAO.",       expired));
+        assertEquals(0, factory.countAvailableDataAccess(), 
unexpectedDAO("Worker should be disposed.", expired));
+        assertTrue  (createdDAOs.get(1).isClosed(),         
unexpectedDAO("Worker should be disposed.", expired));
+        assertTrue  (createdDAOs.get(0).isClosed(),         
unexpectedDAO("Worker should be disposed.", expired));
     }
 
     /**
@@ -182,19 +184,31 @@ public final class ConcurrentAuthorityFactoryTest extends 
TestCase {
      * it is subject to the hazard of thread scheduling.</p>
      *
      * @param  waitTime  the time to wait, in nanoseconds.
+     * @return whether there is more pending factories to dispose.
      */
-    private static void sleepUntilAfterTimeout(final long waitTime, final 
ConcurrentAuthorityFactory<?> factory)
+    private static boolean sleepUntilAfterTimeout(final long waitTime, final 
ConcurrentAuthorityFactory<?> factory)
             throws InterruptedException
     {
         Thread.sleep(TimeUnit.NANOSECONDS.toMillis(waitTime));
         int n = 3;
         while (factory.isCleanScheduled()) {
-            ConcurrentAuthorityFactory.LOGGER.warning("Execution of 
ConcurrentAuthorityFactory.disposeExpired() has been delayed.");
             Thread.sleep(TIMEOUT / NANOS_PER_MILLISECOND);
             System.gc();
             if (--n == 0) {
-                break;
+                return true;
             }
         }
+        return false;
+    }
+
+    /**
+     * Returns the supplier of the error message to report if a 
timeout-dependent test fails.
+     *
+     * @param  message  the message.
+     * @param  expired  the result of the call to {@link 
#sleepUntilAfterTimeout(long, ConcurrentAuthorityFactory)}.
+     * @return the supplied to give to a JUnit {@code assert} method.
+     */
+    private static Supplier<String> unexpectedDAO(final String message, final 
boolean expired) {
+        return () -> expired ? message + " Note: the execution of 
ConcurrentAuthorityFactory.disposeExpired() has been delayed." : message;
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK16.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK16.java
index a756e706c7..53270b08ea 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK16.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK16.java
@@ -18,7 +18,6 @@ package org.apache.sis.pending.jdk;
 
 import java.util.List;
 import java.util.stream.Stream;
-import java.util.logging.LogRecord;
 import org.apache.sis.util.internal.UnmodifiableArrayList;
 
 
@@ -45,32 +44,4 @@ public final class JDK16 {
     public static <T> List<T> toList(final Stream<T> s) {
         return (List<T>) UnmodifiableArrayList.wrap(s.toArray());
     }
-
-    /**
-     * Place holder for {@code LogRecord.getLongThreadID()}.
-     *
-     * @param  record  the record for which to get the thread identifier.
-     * @return thread identifier of the given record.
-     */
-    @SuppressWarnings("deprecation")
-    public static long getLongThreadID(final LogRecord record) {
-        return record.getThreadID();
-    }
-
-    /**
-     * Returns {@code true} if the thread identifier of the given record is 
probably equal to the given identifier.
-     * This method reproduces the hashing algorithm used in {@code 
LogRecord.shortThreadID(long)} private method.
-     *
-     * @param  record    the record for which to compare the thread identifier.
-     * @param  threadId  the thread identifier to compare.
-     * @return whether the thread identifier of the given record is probably 
equal to the given value.
-     */
-    @SuppressWarnings("deprecation")
-    public static boolean isSameThread(final LogRecord record, final long 
threadId) {
-        int hash = Long.hashCode(threadId);
-        if (threadId < 0 || threadId > Integer.MAX_VALUE) {
-            if (hash >= 0) hash = ~hash;        // Makes it negative.
-        }
-        return record.getThreadID() == hash;
-    }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java
index 5c3697cf6e..f4047385b7 100644
--- 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java
+++ 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/test/LoggingWatcher.java
@@ -25,7 +25,6 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.LogRecord;
 import java.util.logging.SimpleFormatter;
-import org.apache.sis.pending.jdk.JDK16;
 
 // Test dependencies
 import static org.junit.jupiter.api.Assertions.*;
@@ -129,7 +128,7 @@ public final class LoggingWatcher implements 
BeforeEachCallback, AfterEachCallba
      * All filters registered on the logger. This is a singleton containing 
only {@code this},
      * unless many tests are running in parallel with their own {@code 
LoggingWatcher}.
      */
-    private Queue<Filter> allFilters;
+    private Queue<LoggingWatcher> allFilters;
 
     /**
      * Creates a new watcher for the given logger.
@@ -183,14 +182,13 @@ public final class LoggingWatcher implements 
BeforeEachCallback, AfterEachCallba
         synchronized (logger) {
             assertNull(allFilters);
             final Filter current = logger.getFilter();
-            if (current instanceof LoggingWatcher) {
-                allFilters = ((LoggingWatcher) current).allFilters;
+            if (current != null) {
+                var w = assertInstanceOf(LoggingWatcher.class, current, () -> 
"The \"" + logger.getName()
+                                + "\" logger has a " + 
current.getClass().getCanonicalName() + " filter.");
+                allFilters = w.allFilters;
                 assertNotNull(allFilters);
             } else {
                 allFilters = new LinkedList<>();
-                if (current != null) {
-                    allFilters.add(current);
-                }
                 logger.setFilter(this);
             }
             allFilters.add(this);
@@ -221,26 +219,24 @@ public final class LoggingWatcher implements 
BeforeEachCallback, AfterEachCallba
      */
     @Override
     public final boolean isLoggable(final LogRecord record) {
-        /*
-         * In the simple mono-thread case, everything use fields of `this`.
-         * However, if many tests are running in parallel, we need to check
-         * which `LoggingWatcher` should take the given log record.
-         */
-        LoggingWatcher owner = this;
-        synchronized (logger) {
-            for (final Filter filter : allFilters) {
-                if (filter instanceof LoggingWatcher) {
-                    final var w = (LoggingWatcher) filter;
-                    if (w.isMultiThread || JDK16.isSameThread(record, 
w.threadId)) {
+        if (record.getLevel().intValue() >= Level.INFO.intValue()) {
+            /*
+             * In the simple mono-thread case, everything use fields of `this`.
+             * However, if many tests are running in parallel, we need to check
+             * which `LoggingWatcher` should take the given log record.
+             */
+            LoggingWatcher owner = null;
+            synchronized (logger) {
+                for (final LoggingWatcher w : allFilters) {
+                    if (w.isMultiThread || w.threadId == 
record.getLongThreadID()) {
                         owner = w;
                         break;
                     }
-                } else if (!filter.isLoggable(record)) {
-                    return false;
                 }
             }
-        }
-        if (record.getLevel().intValue() >= Level.INFO.intValue()) {
+            if (owner == null) {
+                return true;
+            }
             synchronized (owner) {
                 owner.messages.add(owner.formatter.formatMessage(record));
             }

Reply via email to