Check case pair stability policy in Java

Fixes https://github.com/unicode-org/unicodetools/issues/386
diff --git a/unicodetools/src/main/resources/org/unicode/text/UCD/UnicodeInvariantTest.txt b/unicodetools/src/main/resources/org/unicode/text/UCD/UnicodeInvariantTest.txt
index b5822cf..827afac 100644
--- a/unicodetools/src/main/resources/org/unicode/text/UCD/UnicodeInvariantTest.txt
+++ b/unicodetools/src/main/resources/org/unicode/text/UCD/UnicodeInvariantTest.txt
@@ -310,7 +310,9 @@
 In \P{U-1:gc=Cn}, R-1:NFKC_Simple_Casefold = NFKC_Simple_Casefold
 
 # Case Pair Stability: If two characters form a case pair in a version of Unicode, they will remain a case pair in each subsequent version of Unicode. If two characters do not form a case pair in a version of Unicode, they will never become a case pair in any subsequent version of Unicode.
-# TODO
+#
+# Tested for full and simple case mappings in TestCodeInvariants.testCasePairStability.
+# The simple counterpart is UTC-174-A11, as for case folding above.
 
 # Red Flag: cased and case_ignorable should be disjoint,
 # except for modifier letters and ◌ͅ.
diff --git a/unicodetools/src/test/java/org/unicode/text/UCD/TestCodeInvariants.java b/unicodetools/src/test/java/org/unicode/text/UCD/TestCodeInvariants.java
index 0eea266..492e859 100644
--- a/unicodetools/src/test/java/org/unicode/text/UCD/TestCodeInvariants.java
+++ b/unicodetools/src/test/java/org/unicode/text/UCD/TestCodeInvariants.java
@@ -29,6 +29,7 @@
 import org.unicode.props.UcdPropertyValues.Grapheme_Cluster_Break_Values;
 import org.unicode.props.UcdPropertyValues.Script_Values;
 import org.unicode.props.UnicodeProperty;
+import org.unicode.text.utility.Settings;
 import org.unicode.text.utility.Utility;
 
 public class TestCodeInvariants {
@@ -70,6 +71,73 @@ void testBlockRanges() {
     }
 
     @Test
+    void testCasePairStability() {
+        // https://www.unicode.org/policies/stability_policy.html#Case_Pair
+        // Also test the simple counterpart per UTC-174-A11; see
+        // https://www.unicode.org/L2/L2023/23005.htm#174-A11 and
+        // https://github.com/unicode-org/unicodetools/issues/386.
+        IndexUnicodeProperties previous = IndexUnicodeProperties.make(Settings.lastVersion);
+        UnicodeSet oldAssigned =
+                previous.getProperty(UcdProperty.General_Category)
+                        .getSet("Cn")
+                        .complement()
+                        .freeze();
+        checkCasePairStability(
+                previous,
+                oldAssigned,
+                UcdProperty.Lowercase_Mapping,
+                UcdProperty.Uppercase_Mapping);
+        checkCasePairStability(
+                previous,
+                oldAssigned,
+                UcdProperty.Simple_Lowercase_Mapping,
+                UcdProperty.Simple_Uppercase_Mapping);
+    }
+
+    private static void checkCasePairStability(
+            IndexUnicodeProperties previous,
+            UnicodeSet oldAssigned,
+            UcdProperty lowercaseProperty,
+            UcdProperty uppercaseProperty) {
+        UnicodeProperty oldLowercase = previous.getProperty(lowercaseProperty);
+        UnicodeProperty oldUppercase = previous.getProperty(uppercaseProperty);
+        UnicodeProperty lowercase = IUP.getProperty(lowercaseProperty);
+        UnicodeProperty uppercase = IUP.getProperty(uppercaseProperty);
+        for (String source : oldAssigned) {
+            // Comparing the other member of the case pair (or null) catches lost, gained, and
+            // changed pairs.
+            assertEquals(
+                    casePairTarget(source, oldLowercase, oldUppercase, oldAssigned),
+                    casePairTarget(source, lowercase, uppercase, oldAssigned),
+                    () ->
+                            lowercaseProperty
+                                    + " / "
+                                    + uppercaseProperty
+                                    + " case pair changed for U+"
+                                    + Utility.hex(source));
+        }
+    }
+
+    private static String casePairTarget(
+            String source,
+            UnicodeProperty lowercase,
+            UnicodeProperty uppercase,
+            UnicodeSet oldAssigned) {
+        String target = lowercase.getValue(source.codePointAt(0));
+        // Both members of the case pair must be distinct, single characters assigned in the old
+        // version.
+        // Pairs involving newly encoded characters are allowed.
+        if (target == null
+                || source.equals(target)
+                || target.codePointCount(0, target.length()) != 1
+                || !oldAssigned.contains(target)
+                || !source.equals(uppercase.getValue(target.codePointAt(0)))) {
+            return null;
+        }
+        return target;
+    }
+
+    @Test
     public void testScriptExtensions() {
         int testResult = TEST_PASS;