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

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 6d66a0f8adb9db5d40d011a4ae190e87873ef802
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sun Aug 30 19:06:48 2026 -0400

    resolving test failures between Modeler and MCP due to parallel prefs access
---
 .../apache/cayenne/mcp/InMemoryPreferences.java    | 96 ++++++++++++++++++++++
 .../tools/dbimport/DbImportRunValidationTest.java  |  6 +-
 .../tools/openproject/HandshakeWatcherTest.java    |  5 +-
 .../modeler/mcp/McpHandshakeWriterTest.java        |  4 +-
 .../cayenne/modeler/pref/InMemoryPreferences.java  | 96 ++++++++++++++++++++++
 .../_8_RemoveRedundantPathIndexMigrationTest.java  |  3 +-
 6 files changed, 202 insertions(+), 8 deletions(-)

diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/InMemoryPreferences.java
 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/InMemoryPreferences.java
new file mode 100644
index 000000000..4eab84b85
--- /dev/null
+++ 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/InMemoryPreferences.java
@@ -0,0 +1,96 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.mcp;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.prefs.AbstractPreferences;
+
+/**
+ * A {@link java.util.prefs.Preferences} node backed by a plain map, with no 
persistent store behind it.
+ * Pass it to {@code new PrefsLocator(root)} to keep a test off the platform 
preferences.
+ * <p>
+ * Unit tests must never touch the real {@link 
java.util.prefs.Preferences#userRoot()}: on macOS that is a
+ * single CFPreferences domain shared by every JVM of the current user, and 
the JDK writes the whole cached
+ * tree back on flush. Two Maven modules testing in parallel therefore corrupt 
each other - keys read back as
+ * {@code null} moments after being flushed, and {@code removeNode()} silently 
fails to stick - no matter how
+ * unique a node path each test picks. Integration tests are a different 
story: their handshake spans two
+ * processes, so they need the platform store.
+ */
+public class InMemoryPreferences extends AbstractPreferences {
+
+    private final Map<String, String> values = new HashMap<>();
+    private final Map<String, InMemoryPreferences> children = new HashMap<>();
+
+    /**
+     * Creates a root node.
+     */
+    public InMemoryPreferences() {
+        this(null, "");
+    }
+
+    private InMemoryPreferences(InMemoryPreferences parent, String name) {
+        super(parent, name);
+    }
+
+    @Override
+    protected void putSpi(String key, String value) {
+        values.put(key, value);
+    }
+
+    @Override
+    protected String getSpi(String key) {
+        return values.get(key);
+    }
+
+    @Override
+    protected void removeSpi(String key) {
+        values.remove(key);
+    }
+
+    @Override
+    protected void removeNodeSpi() {
+        ((InMemoryPreferences) parent()).children.remove(name());
+    }
+
+    @Override
+    protected String[] keysSpi() {
+        return values.keySet().toArray(new String[0]);
+    }
+
+    @Override
+    protected String[] childrenNamesSpi() {
+        return children.keySet().toArray(new String[0]);
+    }
+
+    @Override
+    protected AbstractPreferences childSpi(String name) {
+        return children.computeIfAbsent(name, n -> new 
InMemoryPreferences(this, n));
+    }
+
+    @Override
+    protected void syncSpi() {
+        // nothing to sync - the map is the store
+    }
+
+    @Override
+    protected void flushSpi() {
+        // nothing to flush - the map is the store
+    }
+}
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/dbimport/DbImportRunValidationTest.java
 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/dbimport/DbImportRunValidationTest.java
index 3671173d9..095383c4a 100644
--- 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/dbimport/DbImportRunValidationTest.java
+++ 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/dbimport/DbImportRunValidationTest.java
@@ -19,6 +19,7 @@
 package org.apache.cayenne.mcp.tools.dbimport;
 
 import org.apache.cayenne.modeler.pref.PreferenceNodeIds;
+import org.apache.cayenne.mcp.InMemoryPreferences;
 import org.apache.cayenne.modeler.pref.PrefsLocator;
 import org.apache.cayenne.modeler.pref.adapters.DataMapPrefs;
 import org.apache.cayenne.modeler.pref.dbconnector.DBConnector;
@@ -49,7 +50,7 @@ public class DbImportRunValidationTest {
 
     @BeforeAll
     public static void setUpClass() {
-        PrefsLocator prefsLocator = new 
PrefsLocator(Preferences.userRoot().node("cayenne-test/dbimport-validation"));
+        PrefsLocator prefsLocator = new PrefsLocator(new 
InMemoryPreferences());
         tool = new DbImportRunTool(prefsLocator);
     }
 
@@ -219,8 +220,7 @@ public class DbImportRunValidationTest {
     }
 
     private Preferences isolatedPrefsRoot() {
-        testPrefsRoot = Preferences.userRoot()
-                .node("cayenne-test/dbimport-val-" + 
UUID.randomUUID().toString().replace("-", ""));
+        testPrefsRoot = new InMemoryPreferences().node("dbimport-val-" + 
UUID.randomUUID().toString().replace("-", ""));
         return testPrefsRoot;
     }
 
diff --git 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/openproject/HandshakeWatcherTest.java
 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/openproject/HandshakeWatcherTest.java
index ea553fa17..aa4fcc4d5 100644
--- 
a/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/openproject/HandshakeWatcherTest.java
+++ 
b/cayenne-mcp-server/src/test/java/org/apache/cayenne/mcp/tools/openproject/HandshakeWatcherTest.java
@@ -18,9 +18,10 @@
  ****************************************************************/
 package org.apache.cayenne.mcp.tools.openproject;
 
-import org.apache.cayenne.modeler.pref.PrefsLocator;
+import org.apache.cayenne.mcp.InMemoryPreferences;
 import org.apache.cayenne.mcp.tools.openproject.HandshakeWatcher.Outcome;
 import org.apache.cayenne.mcp.tools.openproject.HandshakeWatcher.WatchResult;
+import org.apache.cayenne.modeler.pref.PrefsLocator;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
@@ -41,7 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 public class HandshakeWatcherTest {
 
     private static final BooleanSupplier ALIVE = () -> true;
-    private static final PrefsLocator LOCATOR = new PrefsLocator();
+    private static final PrefsLocator LOCATOR = new PrefsLocator(new 
InMemoryPreferences());
     private final String nonce = "test-" + 
UUID.randomUUID().toString().replace("-", "");
 
     @AfterEach
diff --git 
a/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/mcp/McpHandshakeWriterTest.java
 
b/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/mcp/McpHandshakeWriterTest.java
index 02b48fa90..ce2d0b018 100644
--- 
a/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/mcp/McpHandshakeWriterTest.java
+++ 
b/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/mcp/McpHandshakeWriterTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.cayenne.modeler.mcp;
 
+import org.apache.cayenne.modeler.pref.InMemoryPreferences;
 import org.apache.cayenne.modeler.pref.PrefsLocator;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
@@ -37,14 +38,13 @@ public class McpHandshakeWriterTest {
 
     // Unique per test method - keeps parallel runs and prior aborted runs 
from colliding.
     private final String nonce = UUID.randomUUID().toString().replace("-", "");
-    private final PrefsLocator locator = new PrefsLocator();
+    private final PrefsLocator locator = new PrefsLocator(new 
InMemoryPreferences());
 
     @AfterEach
     public void cleanup() throws BackingStoreException {
         Preferences node = locator.handshakeNode(nonce);
         if (node != null) {
             node.removeNode();
-            Preferences.userRoot().flush();
         }
     }
 
diff --git 
a/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/pref/InMemoryPreferences.java
 
b/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/pref/InMemoryPreferences.java
new file mode 100644
index 000000000..ec8f7cf6b
--- /dev/null
+++ 
b/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/pref/InMemoryPreferences.java
@@ -0,0 +1,96 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.modeler.pref;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.prefs.AbstractPreferences;
+
+/**
+ * A {@link java.util.prefs.Preferences} node backed by a plain map, with no 
persistent store behind it.
+ * Pass it to {@code new PrefsLocator(root)} to keep a test off the platform 
preferences.
+ * <p>
+ * Unit tests must never touch the real {@link 
java.util.prefs.Preferences#userRoot()}: on macOS that is a
+ * single CFPreferences domain shared by every JVM of the current user, and 
the JDK writes the whole cached
+ * tree back on flush. Two Maven modules testing in parallel therefore corrupt 
each other - keys read back as
+ * {@code null} moments after being flushed, and {@code removeNode()} silently 
fails to stick - no matter how
+ * unique a node path each test picks. Integration tests are a different 
story: their handshake spans two
+ * processes, so they need the platform store.
+ */
+public class InMemoryPreferences extends AbstractPreferences {
+
+    private final Map<String, String> values = new HashMap<>();
+    private final Map<String, InMemoryPreferences> children = new HashMap<>();
+
+    /**
+     * Creates a root node.
+     */
+    public InMemoryPreferences() {
+        this(null, "");
+    }
+
+    private InMemoryPreferences(InMemoryPreferences parent, String name) {
+        super(parent, name);
+    }
+
+    @Override
+    protected void putSpi(String key, String value) {
+        values.put(key, value);
+    }
+
+    @Override
+    protected String getSpi(String key) {
+        return values.get(key);
+    }
+
+    @Override
+    protected void removeSpi(String key) {
+        values.remove(key);
+    }
+
+    @Override
+    protected void removeNodeSpi() {
+        ((InMemoryPreferences) parent()).children.remove(name());
+    }
+
+    @Override
+    protected String[] keysSpi() {
+        return values.keySet().toArray(new String[0]);
+    }
+
+    @Override
+    protected String[] childrenNamesSpi() {
+        return children.keySet().toArray(new String[0]);
+    }
+
+    @Override
+    protected AbstractPreferences childSpi(String name) {
+        return children.computeIfAbsent(name, n -> new 
InMemoryPreferences(this, n));
+    }
+
+    @Override
+    protected void syncSpi() {
+        // nothing to sync - the map is the store
+    }
+
+    @Override
+    protected void flushSpi() {
+        // nothing to flush - the map is the store
+    }
+}
diff --git 
a/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/pref/migration/toV5/_8_RemoveRedundantPathIndexMigrationTest.java
 
b/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/pref/migration/toV5/_8_RemoveRedundantPathIndexMigrationTest.java
index 185a19f73..974e22ae4 100644
--- 
a/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/pref/migration/toV5/_8_RemoveRedundantPathIndexMigrationTest.java
+++ 
b/cayenne-modeler/src/test/java/org/apache/cayenne/modeler/pref/migration/toV5/_8_RemoveRedundantPathIndexMigrationTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.cayenne.modeler.pref.migration.toV5;
 
+import org.apache.cayenne.modeler.pref.InMemoryPreferences;
 import org.apache.cayenne.modeler.pref.PrefsLocator;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -38,7 +39,7 @@ public class _8_RemoveRedundantPathIndexMigrationTest {
 
     @BeforeEach
     public void setUp() {
-        testRoot = Preferences.userRoot().node("test-cayenne-prefs-" + 
UUID.randomUUID());
+        testRoot = new InMemoryPreferences().node("test-cayenne-prefs-" + 
UUID.randomUUID());
         locator = new PrefsLocator(testRoot);
     }
 

Reply via email to