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); }
