Repository: unicode-org/icu
Pull request #3506: ICU-22995 templatize assert[Not]Equals of integral and enum types
Author: login: eggrobin
Created: 2025-05-22T02:05:29Z | Merged: 2025-05-28T11:56:53Z

#### Checklist
- [x] Required: Issue filed: ICU-22995
- [x] Required: The PR title must be prefixed with a JIRA Issue number. Example: "ICU-1234 Fix xyz"
- [x] Required: Each commit message must be prefixed with a JIRA Issue number. Example: "ICU-1234 Fix xyz"
- [x] Issue accepted (done by Technical Committee after discussion)
- [x] Tests included, if applicable
- [ ] API docs and/or User Guide docs changed or added, if applicable

ALLOW_MANY_COMMITS=true


Diff:
diff --git a/icu4c/source/test/intltest/Makefile.in b/icu4c/source/test/intltest/Makefile.in
index 673e0edbb49c..daba343d1ac9 100644
--- a/icu4c/source/test/intltest/Makefile.in
+++ b/icu4c/source/test/intltest/Makefile.in
@@ -77,7 +77,8 @@ formattedvaluetest.o formatted_string_builder_test.o numbertest_permutation.o \
 units_data_test.o units_router_test.o units_test.o displayoptions_test.o \
 numbertest_simple.o \
 cplusplus_header_api_build_test.o uchar_type_build_test.o \
-ucolheaderonlytest.o usetheaderonlytest.o utfiteratortest.o
+ucolheaderonlytest.o usetheaderonlytest.o utfiteratortest.o \
+intltesttest.o
 
 DEPS = $(OBJECTS:.o=.d)
 
diff --git a/icu4c/source/test/intltest/alphaindextst.cpp b/icu4c/source/test/intltest/alphaindextst.cpp
index d96dcb32d142..0795f6e740d2 100644
--- a/icu4c/source/test/intltest/alphaindextst.cpp
+++ b/icu4c/source/test/intltest/alphaindextst.cpp
@@ -601,11 +601,11 @@ void AlphabeticIndexTest::TestPinyinFirst() {
     index.addLabels(Locale::getChinese(), status);
     assertEquals("getBucketCount()", 28, index.getBucketCount(status));  // ... A-Z ...
     int32_t bucketIndex = index.getBucketIndex(UnicodeString(static_cast<char16_t>(0x897f)), status);
-    assertEquals("getBucketIndex(U+897F)", static_cast<int32_t>(static_cast<char16_t>(0x0058) /*X*/ - static_cast<char16_t>(0x0041) /*A*/ + 1), bucketIndex);
+    assertEquals("getBucketIndex(U+897F)", u'X' - u'A' + 1, bucketIndex);
     bucketIndex = index.getBucketIndex("i", status);
     assertEquals("getBucketIndex(i)", 9, bucketIndex);
     bucketIndex = index.getBucketIndex(UnicodeString(static_cast<char16_t>(0x03B1)), status);
-    assertEquals("getBucketIndex(Greek alpha)", static_cast<int32_t>(27), bucketIndex);
+    assertEquals("getBucketIndex(Greek alpha)", 27, bucketIndex);
     // U+50005 is an unassigned code point which sorts at the end, independent of the Hani group.
     bucketIndex = index.getBucketIndex(UnicodeString(static_cast<UChar32>(0x50005)), status);
     assertEquals("getBucketIndex(U+50005)", 27, bucketIndex);
@@ -668,8 +668,8 @@ void AlphabeticIndexTest::TestNoLabels() {
     index.addRecord(UnicodeString(static_cast<char16_t>(0x03B1)), nullptr, status);
     assertEquals("getBucketCount()", 1, index.getBucketCount(status));  // ...
     TEST_ASSERT(index.nextBucket(status));
-    assertEquals("underflow label type", static_cast<int32_t>(U_ALPHAINDEX_UNDERFLOW), index.getBucketLabelType());
-    assertEquals("all records in the underflow bucket", static_cast<int32_t>(3), index.getBucketRecordCount());
+    assertEquals("underflow label type", U_ALPHAINDEX_UNDERFLOW, index.getBucketLabelType());
+    assertEquals("all records in the underflow bucket", 3, index.getBucketRecordCount());
 }
 
 void AlphabeticIndexTest::TestChineseZhuyin() {
diff --git a/icu4c/source/test/intltest/apicoll.cpp b/icu4c/source/test/intltest/apicoll.cpp
index 4464266b1ad5..4ccdc373e6ce 100644
--- a/icu4c/source/test/intltest/apicoll.cpp
+++ b/icu4c/source/test/intltest/apicoll.cpp
@@ -1573,9 +1573,9 @@ void CollationAPITest::TestVariableTopSetting() {
                static_cast<int64_t>(newVarTop2), static_cast<int64_t>(newVarTop));
 
   coll->setAttribute(UCOL_ALTERNATE_HANDLING, UCOL_SHIFTED, status);
-  assertEquals("empty==dollar", static_cast<int32_t>(UCOL_EQUAL), static_cast<int32_t>(coll->compare(UnicodeString(), dollar)));
-  assertEquals("empty==euro", static_cast<int32_t>(UCOL_EQUAL), static_cast<int32_t>(coll->compare(UnicodeString(), euro)));
-  assertEquals("dollar<zero", static_cast<int32_t>(UCOL_LESS), static_cast<int32_t>(coll->compare(dollar, UnicodeString(static_cast<char16_t>(0x30)))));
+  assertEquals("empty==dollar", UCOL_EQUAL, coll->compare(UnicodeString(), dollar));
+  assertEquals("empty==euro", UCOL_EQUAL, coll->compare(UnicodeString(), euro));
+  assertEquals("dollar<zero", UCOL_LESS, coll->compare(dollar, UnicodeString(static_cast<char16_t>(0x30))));
 
   coll->setVariableTop(oldVarTop, status);
 
@@ -2383,7 +2383,7 @@ void CollationAPITest::TestCloneBinary() {
     rbc->setAttribute(UCOL_STRENGTH, UCOL_PRIMARY, errorCode);
     UnicodeString uUmlaut(static_cast<char16_t>(0xfc));
     UnicodeString ue = UNICODE_STRING_SIMPLE("ue");
-    assertEquals("rbc/primary: u-umlaut==ue", static_cast<int32_t>(UCOL_EQUAL), rbc->compare(uUmlaut, ue, errorCode));
+    assertEquals("rbc/primary: u-umlaut==ue", UCOL_EQUAL, rbc->compare(uUmlaut, ue, errorCode));
     uint8_t bin[25000];
     int32_t binLength = rbc->cloneBinary(bin, UPRV_LENGTHOF(bin), errorCode);
     if(errorCode.errDataIfFailureAndReset("rbc->cloneBinary()")) {
@@ -2395,8 +2395,8 @@ void CollationAPITest::TestCloneBinary() {
     if(errorCode.errDataIfFailureAndReset("RuleBasedCollator(rbc binary)")) {
         return;
     }
-    assertEquals("rbc2.strength==primary", static_cast<int32_t>(UCOL_PRIMARY), rbc2.getAttribute(UCOL_STRENGTH, errorCode));
-    assertEquals("rbc2: u-umlaut==ue", static_cast<int32_t>(UCOL_EQUAL), rbc2.compare(uUmlaut, ue, errorCode));
+    assertEquals("rbc2.strength==primary", UCOL_PRIMARY, rbc2.getAttribute(UCOL_STRENGTH, errorCode));
+    assertEquals("rbc2: u-umlaut==ue", UCOL_EQUAL, rbc2.compare(uUmlaut, ue, errorCode));
     assertTrue("rbc==rbc2", *rbc == rbc2);
     uint8_t bin2[25000];
     int32_t bin2Length = rbc2.cloneBinary(bin2, UPRV_LENGTHOF(bin2), errorCode);
@@ -2407,8 +2407,8 @@ void CollationAPITest::TestCloneBinary() {
     if(errorCode.errDataIfFailureAndReset("RuleBasedCollator(rbc binary, length<0)")) {
         return;
     }
-    assertEquals("rbc3.strength==primary", static_cast<int32_t>(UCOL_PRIMARY), rbc3.getAttribute(UCOL_STRENGTH, errorCode));
-    assertEquals("rbc3: u-umlaut==ue", static_cast<int32_t>(UCOL_EQUAL), rbc3.compare(uUmlaut, ue, errorCode));
+    assertEquals("rbc3.strength==primary", UCOL_PRIMARY, rbc3.getAttribute(UCOL_STRENGTH, errorCode));
+    assertEquals("rbc3: u-umlaut==ue", UCOL_EQUAL, rbc3.compare(uUmlaut, ue, errorCode));
     assertTrue("rbc==rbc3", *rbc == rbc3);
 }
 
@@ -2432,7 +2432,7 @@ void CollationAPITest::TestIterNumeric() {
     uiter_setUTF8(&iter40, "\x34\x30", 2);
     uiter_setUTF8(&iter72, "\x37\x32", 2);
     UCollationResult result = coll.compare(iter40, iter72, errorCode);
-    assertEquals("40<72", static_cast<int32_t>(UCOL_LESS), static_cast<int32_t>(result));
+    assertEquals("40<72", UCOL_LESS, result);
 }
 
 void CollationAPITest::TestBadKeywords() {
diff --git a/icu4c/source/test/intltest/calregts.cpp b/icu4c/source/test/intltest/calregts.cpp
index 198901f88179..e2b5e8b61d6b 100644
--- a/icu4c/source/test/intltest/calregts.cpp
+++ b/icu4c/source/test/intltest/calregts.cpp
@@ -1561,7 +1561,7 @@ void CalendarRegressionTest::Test13745()
         errln("%s:%d Failure setting INT32_MAX+1 change on calendar: %s\n", __FILE__, __LINE__, u_errorName(status));
         return;
     }
-    assertEquals("getGregorianChange()", static_cast<double>(INT32_MAX) * MILLIS_IN_DAY, cal->getGregorianChange());
+    assertEquals("getGregorianChange()", INT32_MAX * MILLIS_IN_DAY, cal->getGregorianChange());
 
     // test underflow
     cal->setGregorianChange((static_cast<double>(INT32_MIN) - 1.0) * MILLIS_IN_DAY, status);
diff --git a/icu4c/source/test/intltest/convtest.cpp b/icu4c/source/test/intltest/convtest.cpp
index 981d577543d9..64add9df058a 100644
--- a/icu4c/source/test/intltest/convtest.cpp
+++ b/icu4c/source/test/intltest/convtest.cpp
@@ -816,7 +816,7 @@ ConversionTest::TestUTF8ToUTF8Overflow() {
     assertEquals("illFormed number of bytes written", 0, length);
     errorLength = UPRV_LENGTHOF(errorBytes);
     ucnv_getInvalidChars(cnv1.getAlias(), errorBytes, &errorLength, errorCode);
-    assertEquals("illFormed truncated errorLength", 2, static_cast<int32_t>(errorLength));
+    assertEquals("illFormed truncated errorLength", 2, errorLength);
     if (errorLength == 2) {
         assertEquals("illFormed truncated errorBytes", 0xf191, 
                      (static_cast<int32_t>(static_cast<uint8_t>(errorBytes[0])) << 8) | static_cast<uint8_t>(errorBytes[1]));
@@ -833,7 +833,7 @@ ConversionTest::TestUTF8ToUTF8Overflow() {
     assertEquals("illFormed trail byte number of bytes written", 0, length);
     errorLength = UPRV_LENGTHOF(errorBytes);
     ucnv_getInvalidChars(cnv1.getAlias(), errorBytes, &errorLength, errorCode);
-    assertEquals("illFormed trail byte errorLength", 1, static_cast<int32_t>(errorLength));
+    assertEquals("illFormed trail byte errorLength", 1, errorLength);
     if (errorLength == 1) {
         assertEquals("illFormed trail byte errorBytes", 0x93, static_cast<int32_t>(static_cast<uint8_t>(errorBytes[0])));
     }
diff --git a/icu4c/source/test/intltest/dcfmapts.cpp b/icu4c/source/test/intltest/dcfmapts.cpp
index 9794b338209a..43a5d4e09983 100644
--- a/icu4c/source/test/intltest/dcfmapts.cpp
+++ b/icu4c/source/test/intltest/dcfmapts.cpp
@@ -137,7 +137,7 @@ void IntlTestDecimalFormatAPI::testAPI(/*char *par*/)
         DecimalFormat df("0", {"en", status}, status);
         UnicodeString result;
         assertEquals("pat 0: ", 0, df.getGroupingSize());
-        assertEquals("pat 0: ", static_cast<UBool>(false), df.isGroupingUsed());
+        assertEquals("pat 0: ", false, df.isGroupingUsed());
         df.setGroupingUsed(false);
         assertEquals("pat 0 then disabled: ", 0, df.getGroupingSize());
         assertEquals("pat 0 then disabled: ", u"1111", df.format(1111, result.remove()));
@@ -149,7 +149,7 @@ void IntlTestDecimalFormatAPI::testAPI(/*char *par*/)
         DecimalFormat df("#,##0", {"en", status}, status);
         UnicodeString result;
         assertEquals("pat #,##0: ", 3, df.getGroupingSize());
-        assertEquals("pat #,##0: ", static_cast<UBool>(true), df.isGroupingUsed());
+        assertEquals("pat #,##0: ", true, df.isGroupingUsed());
         df.setGroupingUsed(false);
         assertEquals("pat #,##0 then disabled: ", 3, df.getGroupingSize());
         assertEquals("pat #,##0 then disabled: ", u"1111", df.format(1111, result.remove()));
diff --git a/icu4c/source/test/intltest/intltest.cpp b/icu4c/source/test/intltest/intltest.cpp
index 3806d0ff529b..a856715837e8 100644
--- a/icu4c/source/test/intltest/intltest.cpp
+++ b/icu4c/source/test/intltest/intltest.cpp
@@ -18,6 +18,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <cinttypes>
 #include <cmath>
 #include <math.h>
 #include <string_view>
@@ -73,12 +74,12 @@ UCharToUnicodeString(char16_t c) { return {c}; }
 // [rtg] Just to get things working
 UnicodeString
 operator+(const UnicodeString& left,
-      long num)
+      int64_t num)
 {
     char buffer[64];    // nos changed from 10 to 64
     char danger = 'p';  // guard against overrunning the buffer (rtg)
 
-    snprintf(buffer, sizeof(buffer), "%ld", num);
+    snprintf(buffer, sizeof(buffer), "%" PRId64, num);
     assert(danger == 'p');
 
     return left + buffer;
@@ -86,12 +87,12 @@ operator+(const UnicodeString& left,
 
 UnicodeString
 operator+(const UnicodeString& left,
-      unsigned long num)
+      uint64_t num)
 {
     char buffer[64];    // nos changed from 10 to 64
     char danger = 'p';  // guard against overrunning the buffer (rtg)
 
-    snprintf(buffer, sizeof(buffer), "%lu", num);
+    snprintf(buffer, sizeof(buffer), "%" PRIu64, num);
     assert(danger == 'p');
 
     return left + buffer;
@@ -103,11 +104,7 @@ Int64ToUnicodeString(int64_t num)
     char buffer[64];    // nos changed from 10 to 64
     char danger = 'p';  // guard against overrunning the buffer (rtg)
 
-#if defined(_MSC_VER)
-    snprintf(buffer, sizeof(buffer), "%I64d", num);
-#else
-    snprintf(buffer, sizeof(buffer), "%lld", static_cast<long long>(num));
-#endif
+    snprintf(buffer, sizeof(buffer), "%" PRId64, num);
     assert(danger == 'p');
 
     return buffer;
@@ -143,14 +140,6 @@ operator+(const UnicodeString& left,
     return left + buffer;
 }
 
-#if 0
-UnicodeString
-operator+(const UnicodeString& left,
-          int64_t num) {
-  return left + Int64ToUnicodeString(num);
-}
-#endif
-
 #if !UCONFIG_NO_FORMATTING
 
 /**
@@ -232,13 +221,18 @@ UnicodeString toString(const Formattable& f) {
 
 // useful when operator+ won't cooperate
 UnicodeString toString(int32_t n) {
-    return UnicodeString() + static_cast<long>(n);
+    return UnicodeString() + static_cast<int64_t>(n);
 }
 
 
 
 UnicodeString toString(UBool b) {
-  return b ? UnicodeString("true"):UnicodeString("false");
+    return b ? b != 1 ? UnicodeString("static_cast<UBool>(") + b + ")" : UnicodeString("true")
+             : UnicodeString("false");
+}
+
+UnicodeString toString(bool b) {
+  return b ? UnicodeString("true") : UnicodeString("false");
 }
 
 UnicodeString toString(const UnicodeSet& uniset, UErrorCode& status) {
@@ -249,17 +243,17 @@ UnicodeString toString(const UnicodeSet& uniset, UErrorCode& status) {
 
 // stephen - cleaned up 05/05/99
 UnicodeString operator+(const UnicodeString& left, char num)
-{ return left + static_cast<long>(num); }
+{ return left + static_cast<int64_t>(num); }
 UnicodeString operator+(const UnicodeString& left, short num)
-{ return left + static_cast<long>(num); }
+{ return left + static_cast<int64_t>(num); }
 UnicodeString operator+(const UnicodeString& left, int num)
-{ return left + static_cast<long>(num); }
+{ return left + static_cast<int64_t>(num); }
 UnicodeString operator+(const UnicodeString& left, unsigned char num)
-{ return left + static_cast<unsigned long>(num); }
+{ return left + static_cast<uint64_t>(num); }
 UnicodeString operator+(const UnicodeString& left, unsigned short num)
-{ return left + static_cast<unsigned long>(num); }
+{ return left + static_cast<uint64_t>(num); }
 UnicodeString operator+(const UnicodeString& left, unsigned int num)
-{ return left + static_cast<unsigned long>(num); }
+{ return left + static_cast<uint64_t>(num); }
 UnicodeString operator+(const UnicodeString& left, float num)
 { return left + static_cast<double>(num); }
 
@@ -2091,12 +2085,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
+        return false;
+    }
+#ifdef VERBOSE_ASSERTIONS
+    else {
+        logln(UnicodeString("Ok: ") + message + "; got " + actual);
+    }
+#endif
+    return true;
+}
+
+bool IntlTest::assertSigned32Equals(const char *message, int32_t expected, int32_t actual) {
+    if (expected != actual) {
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "=0x" + toHex(actual) +
               "; expected " + expected + "=0x" + toHex(expected));
         return false;
     }
@@ -2108,18 +2112,18 @@ UBool IntlTest::assertEquals(const char* message,
     return true;
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int64_t expected,
-                             int64_t actual) {
+bool IntlTest::assertCodePointEquals(const char *message, char32_t expected, char32_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got int64 " +
-              Int64ToUnicodeString(actual) + 
-              "; expected " + Int64ToUnicodeString(expected) );
+        errln(UnicodeString("FAIL: ") + message + "; got U+" + toHex(actual, actual <= 0xFFFF ? 4 : -1) +
+              " " + UnicodeString(static_cast<UChar32>(actual)) + "; expected U+" +
+              toHex(expected, expected <= 0xFFFF ? 4 : -1) + +" " +
+              UnicodeString(static_cast<UChar32>(expected)));
         return false;
     }
 #ifdef VERBOSE_ASSERTIONS
     else {
-      logln(UnicodeString("Ok: ") + message + "; got int64 " + Int64ToUnicodeString(actual));
+        logln(UnicodeString("Ok: ") + message + "; got U+" + toHex(actual, actual <= 0xFFFF ? 4 : -1) +
+              " " + UnicodeString(static_cast<UChar32>(actual)));
     }
 #endif
     return true;
@@ -2143,18 +2147,29 @@ UBool IntlTest::assertEquals(const char* message,
     return true;
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             UBool expected,
-                             UBool actual) {
+bool IntlTest::assertBooleanEquals(const char *message, int8_t expected, int8_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              toString(actual) +
-              "; expected " + toString(expected));
+        errln(UnicodeString("FAIL: ") + message + "; got " + toString(actual) + "; expected " +
+              toString(expected));
         return false;
     }
 #ifdef VERBOSE_ASSERTIONS
     else {
-      logln(UnicodeString("Ok: ") + message + "; got " + toString(actual));
+        logln(UnicodeString("Ok: ") + message + "; got " + toString(actual));
+    }
+#endif
+    return true;
+}
+
+bool IntlTest::assertBooleanNotEquals(const char *message, int8_t expected, int8_t actual) {
+    if (expected == actual) {
+        errln(UnicodeString("FAIL: ") + message + "; got " + toString(actual) + "; expected != " +
+              toString(expected));
+        return false;
+    }
+#ifdef VERBOSE_ASSERTIONS
+    else {
+        logln(UnicodeString("Ok: ") + message + "; got " + toString(actual));
     }
 #endif
     return true;
@@ -2259,9 +2274,22 @@ UBool IntlTest::assertEquals(const char* message,
     return true;
 }
 
-UBool IntlTest::assertNotEquals(const char* message,
-                                int32_t expectedNot,
-                                int32_t actual) {
+bool IntlTest::assertSigned64NotEquals(const char *message, int64_t expected, int64_t actual) {
+    if (expected == actual) {
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected != " + expected);
+        return false;
+    }
+#ifdef VERBOSE_ASSERTIONS
+    else {
+        logln(UnicodeString("Ok: ") + message + "; got " + actual);
+    }
+#endif
+    return true;
+}
+
+bool IntlTest::assertSigned32NotEquals(const char* message,
+                                       int32_t expectedNot,
+                                       int32_t actual) {
     if (expectedNot == actual) {
         errln(UnicodeString("FAIL: ") + message + "; got " + actual + "=0x" + toHex(actual) +
               "; expected != " + expectedNot);
@@ -2276,6 +2304,23 @@ UBool IntlTest::assertNotEquals(const char* message,
     return true;
 }
 
+bool IntlTest::assertCodePointNotEquals(const char *message, char32_t expected, char32_t actual) {
+    if (expected == actual) {
+        errln(UnicodeString("FAIL: ") + message + "; got U+" + toHex(actual, actual <= 0xFFFF ? 4 : -1) +
+              " " + UnicodeString(static_cast<UChar32>(actual)) + "; expected != U+" +
+              toHex(expected, expected <= 0xFFFF ? 4 : -1) + +" " +
+              UnicodeString(static_cast<UChar32>(expected)));
+        return false;
+    }
+#ifdef VERBOSE_ASSERTIONS
+    else {
+        logln(UnicodeString("Ok: ") + message + "; got U+" + toHex(actual, actual <= 0xFFFF ? 4 : -1) +
+              " " + UnicodeString(static_cast<UChar32>(actual)));
+    }
+#endif
+    return true;
+}
+
 UBool IntlTest::assertEqualsNear(const char* message,
                                  double expected,
                                  double actual,
@@ -2308,7 +2353,7 @@ UBool IntlTest::assertEqualsNear(const char* message,
 
 static char ASSERT_BUF[256];
 
-static const char* extractToAssertBuf(std::u16string_view message) {
+const char* IntlTest::extractToAssertBuf(std::u16string_view message) {
     UnicodeString buf;
     escape(message, buf);
     buf.extract(0, 0x7FFFFFFF, ASSERT_BUF, sizeof(ASSERT_BUF) - 1, nullptr);
@@ -2340,21 +2385,7 @@ UBool IntlTest::assertEquals(std::u16string_view message,
                              const char* actual) {
     return assertEquals(extractToAssertBuf(message), expected, actual);
 }
-UBool IntlTest::assertEquals(std::u16string_view message,
-                             UBool expected,
-                             UBool actual) {
-    return assertEquals(extractToAssertBuf(message), expected, actual);
-}
-UBool IntlTest::assertEquals(std::u16string_view message,
-                             int32_t expected,
-                             int32_t actual) {
-    return assertEquals(extractToAssertBuf(message), expected, actual);
-}
-UBool IntlTest::assertEquals(std::u16string_view message,
-                             int64_t expected,
-                             int64_t actual) {
-    return assertEquals(extractToAssertBuf(message), expected, actual);
-}
+
 UBool IntlTest::assertEquals(std::u16string_view message,
                              double expected,
                              double actual) {
@@ -2375,11 +2406,6 @@ UBool IntlTest::assertEquals(std::u16string_view message,
                              const std::vector<std::string>& actual) {
     return assertEquals(extractToAssertBuf(message), expected, actual);
 }
-UBool IntlTest::assertNotEquals(std::u16string_view message,
-                                int32_t expectedNot,
-                                int32_t actual) {
-    return assertNotEquals(extractToAssertBuf(message), expectedNot, actual);
-}
 UBool IntlTest::assertEqualsNear(std::u16string_view message,
                                  double expected,
                                  double actual,
diff --git a/icu4c/source/test/intltest/intltest.h b/icu4c/source/test/intltest/intltest.h
index d4ecb9c0353c..6b5971bea50d 100644
--- a/icu4c/source/test/intltest/intltest.h
+++ b/icu4c/source/test/intltest/intltest.h
@@ -36,9 +36,8 @@ U_NAMESPACE_USE
 UnicodeString UCharToUnicodeString(char16_t c);
 UnicodeString Int64ToUnicodeString(int64_t num);
 UnicodeString DoubleToUnicodeString(double num);
-//UnicodeString operator+(const UnicodeString& left, int64_t num); // Some compilers don't allow this because of the long type.
-UnicodeString operator+(const UnicodeString& left, long num);
-UnicodeString operator+(const UnicodeString& left, unsigned long num);
+UnicodeString operator+(const UnicodeString& left, int64_t num);
+UnicodeString operator+(const UnicodeString& left, uint64_t num);
 UnicodeString operator+(const UnicodeString& left, double num);
 UnicodeString operator+(const UnicodeString& left, char num);
 UnicodeString operator+(const UnicodeString& left, short num);
@@ -300,9 +299,84 @@ class IntlTest : public TestLog {
     UBool assertEquals(const char* message, std::u16string_view expected,
                        std::u16string_view actual, UBool possibleDataError=false);
     UBool assertEquals(const char* message, const char* expected, const char* actual);
-    UBool assertEquals(const char* message, UBool expected, UBool actual);
-    UBool assertEquals(const char* message, int32_t expected, int32_t actual);
-    UBool assertEquals(const char* message, int64_t expected, int64_t actual);
+
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<(std::is_same_v<T, UBool> || std::is_same_v<T, bool>) &&
+                                          (std::is_same_v<U, UBool> || std::is_same_v<U, bool>)>>
+    bool assertEquals(const Message &message, T expected, U actual) {
+        return assertBooleanEquals(extractToAssertBuf(message), expected, actual);
+    }
+
+    // Since UChar32 is int32_t, we do not know whether we are looking at code points or some other
+    // number.  Show both the decimal and hexadecimal representations on failure.
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<std::is_same_v<T, int32_t> && std::is_same_v<U, int32_t>>,
+              typename = void, typename = void>
+    bool assertEquals(const Message &message, T expected, U actual) {
+        return assertSigned32Equals(extractToAssertBuf(message), expected, actual);
+    }
+
+    // This overload applies when at least one of the sides of the comparison is char32_t or char16_t.
+    // In this case, we can be pretty sure that we are comparing code points or UTF-16 (or UTF-32) code
+    // units, so we do not show the decimal value, only the hex value in U+ notation, and we show the
+    // literal character as well.
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<std::is_same_v<T, char32_t> || std::is_same_v<U, char32_t> ||
+                                          std::is_same_v<T, char16_t> || std::is_same_v<U, char16_t>>,
+              typename = void>
+    bool assertEquals(const Message& message, T expected, U actual) {
+        return assertCodePointEquals(extractToAssertBuf(message), expected, actual);
+    }
+
+    // Otherwise, for enumeration or integral types, log only the decimal value.
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<(std::is_integral_v<T> || std::is_enum_v<T>) &&
+                                          (std::is_integral_v<U> || std::is_enum_v<U>) &&
+                                          !(std::is_same_v<T, int32_t> && std::is_same_v<U, int32_t>) &&
+                                          !((std::is_same_v<T, UBool> || std::is_same_v<T, bool>) &&
+                                            (std::is_same_v<U, UBool> || std::is_same_v<U, bool>)) &&
+                                          !(std::is_same_v<T, char32_t> || std::is_same_v<U, char32_t> ||
+                                            std::is_same_v<T, char16_t> || std::is_same_v<U, char16_t>)>,
+              typename = void, typename = void, typename = void>
+    bool assertEquals(const Message &message, T expected, U actual) {
+        return assertSigned64Equals(extractToAssertBuf(message), expected, actual);
+    }
+
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<(std::is_same_v<T, UBool> || std::is_same_v<T, bool>) &&
+                                          (std::is_same_v<U, UBool> || std::is_same_v<U, bool>)>>
+    bool assertNotEquals(const Message &message, T expected, U actual) {
+        return assertBooleanNotEquals(extractToAssertBuf(message), expected, actual);
+    }
+
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<std::is_same_v<T, int32_t> && std::is_same_v<U, int32_t>>,
+              typename = void, typename = void>
+    bool assertNotEquals(const Message &message, T expected, U actual) {
+        return assertSigned32NotEquals(extractToAssertBuf(message), expected, actual);
+    }
+
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<std::is_same_v<T, char32_t> || std::is_same_v<U, char32_t> ||
+                                          std::is_same_v<T, char16_t> || std::is_same_v<U, char16_t>>,
+              typename = void>
+    bool assertNotEquals(const Message &message, T expected, U actual) {
+        return assertCodePointNotEquals(extractToAssertBuf(message), expected, actual);
+    }
+
+    template <typename Message, typename T, typename U,
+              typename = std::enable_if_t<(std::is_integral_v<T> || std::is_enum_v<T>) &&
+                                          (std::is_integral_v<U> || std::is_enum_v<U>) &&
+                                          !(std::is_same_v<T, int32_t> && std::is_same_v<U, int32_t>) &&
+                                          !((std::is_same_v<T, UBool> || std::is_same_v<T, bool>) &&
+                                            (std::is_same_v<U, UBool> || std::is_same_v<U, bool>)) &&
+                                          !(std::is_same_v<T, char32_t> || std::is_same_v<U, char32_t> ||
+                                            std::is_same_v<T, char16_t> || std::is_same_v<U, char16_t>)>,
+              typename = void, typename = void, typename = void>
+    bool assertNotEquals(const Message &message, T expected, U actual) {
+        return assertSigned64NotEquals(extractToAssertBuf(message), expected, actual);
+    }
+
     UBool assertEquals(const char* message, double expected, double actual);
 
     // for disambiguation
@@ -340,22 +414,19 @@ class IntlTest : public TestLog {
                                   const Formattable& actual);
 #endif
 #endif
-    UBool assertNotEquals(const char* message, int32_t expectedNot, int32_t actual);
     UBool assertTrue(std::u16string_view message, UBool condition, UBool quiet=false, UBool possibleDataError=false);
     UBool assertFalse(std::u16string_view message, UBool condition, UBool quiet=false, UBool possibleDataError=false);
     UBool assertSuccess(std::u16string_view message, UErrorCode ec);
     UBool assertEquals(std::u16string_view message, std::u16string_view expected,
                        std::u16string_view actual, UBool possibleDataError=false);
     UBool assertEquals(std::u16string_view message, const char* expected, const char* actual);
-    UBool assertEquals(std::u16string_view message, UBool expected, UBool actual);
-    UBool assertEquals(std::u16string_view message, int32_t expected, int32_t actual);
-    UBool assertEquals(std::u16string_view message, int64_t expected, int64_t actual);
     UBool assertEquals(std::u16string_view message, double expected, double actual);
 
     // for disambiguation
     UBool assertEquals(std::u16string_view message, const char* expected,
                        std::u16string_view actual, UBool possibleDataError=false);
 
+
     /**
      * Asserts that two doubles are equal to within a positive delta. Returns
      * false if they are not.
@@ -376,7 +447,6 @@ class IntlTest : public TestLog {
 #endif
     UBool assertEquals(std::u16string_view message,
         const std::vector<std::string>& expected, const std::vector<std::string>& actual);
-    UBool assertNotEquals(std::u16string_view message, int32_t expectedNot, int32_t actual);
 
     virtual void runIndexedTest( int32_t index, UBool exec, const char* &name, char* par = nullptr ); // override !
 
@@ -399,6 +469,24 @@ class IntlTest : public TestLog {
     int32_t     threadCount;
 
 private:
+    // This takes int8_t (aka UBool), not bool, so that in case we actually mean to compare int8_t
+    // integers, the test result is at least correct. The error message will still be useless (unless one
+    // of the values is 0, it will say that we expected true and got true instead).
+    bool assertBooleanEquals(const char *message, int8_t expected, int8_t actual);
+    bool assertSigned32Equals(const char *message, int32_t expected, int32_t actual);
+    bool assertSigned64Equals(const char *message, int64_t expected, int64_t actual);
+    bool assertCodePointEquals(const char *message, char32_t expected, char32_t actual);
+
+    bool assertBooleanNotEquals(const char *message, int8_t expected, int8_t actual);
+    bool assertSigned32NotEquals(const char *message, int32_t expected, int32_t actual);
+    bool assertSigned64NotEquals(const char *message, int64_t expected, int64_t actual);
+    bool assertCodePointNotEquals(const char *message, char32_t expected, char32_t actual);
+
+    static const char* extractToAssertBuf(const char* message) {
+      return message;
+    }
+    static const char* extractToAssertBuf(std::u16string_view message);
+
     UBool       LL_linestart;
     int32_t     LL_indentlevel;
 
diff --git a/icu4c/source/test/intltest/intltest.vcxproj b/icu4c/source/test/intltest/intltest.vcxproj
index 8dab6d3b737c..472271a2a386 100644
--- a/icu4c/source/test/intltest/intltest.vcxproj
+++ b/icu4c/source/test/intltest/intltest.vcxproj
@@ -73,6 +73,7 @@
     <ClCompile Include="collationtest.cpp" />
     <ClCompile Include="colldata.cpp" />
     <ClCompile Include="erarulestest.cpp" />
+    <ClCompile Include="intltesttest.cpp" />
     <ClCompile Include="numfmtspectest.cpp" />
     <ClCompile Include="regiontst.cpp" />
     <ClCompile Include="ucharstrietest.cpp" />
diff --git a/icu4c/source/test/intltest/intltest.vcxproj.filters b/icu4c/source/test/intltest/intltest.vcxproj.filters
index 6f7a2851b36f..32ae91e6a2dd 100644
--- a/icu4c/source/test/intltest/intltest.vcxproj.filters
+++ b/icu4c/source/test/intltest/intltest.vcxproj.filters
@@ -592,6 +592,9 @@
     <ClCompile Include="usetheaderonlytest.cpp">
       <Filter>misc</Filter>
     </ClCompile>
+    <ClCompile Include="intltesttest.cpp">
+      <Filter>misc</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="itrbbi.h">
diff --git a/icu4c/source/test/intltest/intltesttest.cpp b/icu4c/source/test/intltest/intltesttest.cpp
new file mode 100644
index 000000000000..607a4920f547
--- /dev/null
+++ b/icu4c/source/test/intltest/intltesttest.cpp
@@ -0,0 +1,258 @@
+// © 2025 and later: Unicode, Inc. and others.
+// License & terms of use: https://www.unicode.org/copyright.html
+
+#include "intltest.h"
+
+class IntlTestTest : public IntlTest {
+  public:
+    void runIndexedTest(int32_t index, UBool exec, const char*& name, char* /*par*/) override {
+        if (exec) {
+            logln("TestSuite IntlTestTest: ");
+        }
+        TESTCASE_AUTO_BEGIN;
+
+        TESTCASE_AUTO(testAssertEquals);
+        TESTCASE_AUTO(testAssertNotEquals);
+
+        TESTCASE_AUTO_END;
+    }
+
+  protected:
+    class TestableIntlTest : public IntlTest {
+      public:
+        TestableIntlTest(std::u16string* const lastLogLine) : lastLogLine(lastLogLine) {}
+
+        size_t newErrors() {
+            const size_t result = errors;
+            errors = 0;
+            return result;
+        }
+
+        virtual int32_t IncErrorCount() override {
+          ++errors;
+          return IntlTest::IncErrorCount();
+        }
+
+      protected:
+        virtual void LL_message(std::u16string_view message, UBool newline) override {
+            currentLogLine += message;
+            if (newline) {
+                *lastLogLine = currentLogLine;
+                currentLogLine.clear();
+            }
+        }
+
+      private:
+        size_t errors = 0;
+        std::u16string* lastLogLine;
+        std::u16string currentLogLine;
+    };
+
+    void testAssertEquals() {
+        std::u16string lastLogLine;
+        TestableIntlTest metatest(&lastLogLine);
+        // Booleans: Both UBool, mixed, both bool.
+        metatest.assertEquals(WHERE "should fail", static_cast<UBool>(true), static_cast<UBool>(false));
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("boolean in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"true"));
+        metatest.assertEquals(WHERE "should fail", static_cast<UBool>(true), false);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("boolean in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"true"));
+        metatest.assertEquals(WHERE "should fail", true, false);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("boolean in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"true"));
+        // The mysterious int32_t.
+        metatest.assertEquals(WHERE "should fail", -1, 66);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32", std::u16string::npos, lastLogLine.find(u"-1"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"0xFFFFFFFF"));
+        metatest.assertEquals(WHERE "should fail", 65, 66);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"0x41"));
+        metatest.assertEquals(WHERE "should fail", UnicodeString("").char32At(0),
+                              UnicodeString("B").char32At(0));
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65535"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"0xFFFF"));
+        metatest.assertEquals(WHERE "should fail", UnicodeString("AB").char32At(0),
+                              UnicodeString("AB").char32At(1));
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"0x41"));
+        // Definitely characters.
+        // char32_t.
+        metatest.assertEquals(WHERE "should fail", U'A', U'B');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char32_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertEquals(WHERE "should fail", UnicodeString("A").char32At(0), U'B');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char32_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertEquals(WHERE "should fail", U'A', 0x42);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char32_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        // char16_t.
+        metatest.assertEquals(WHERE "should fail", u'A', u'B');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char16_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertEquals(WHERE "should fail", 0x41, u'B');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char16_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertEquals(WHERE "should fail", UnicodeString("A").charAt(0), 0x42);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char16_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        // Actual numeric comparisons.
+        metatest.assertEquals(WHERE "should fail", static_cast<uint32_t>(65), 66);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal uint32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65"));
+        assertEquals("NO hex uint32 in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"41"));
+        constexpr std::u16string_view s = u"𒌉 𒂍𒁾𒁀𒀀 𒌓 𒌌𒆷𒀀𒀭 𒈨𒂠 𒉌𒁺𒉈𒂗";
+        metatest.assertEquals(WHERE "should fail", s.size(), 38);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal size_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"37"));
+        assertEquals("NO hex size_t in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"25"));
+        // This one is int32_t on 32-bit platforms.
+        metatest.assertEquals(WHERE "should fail", s.end() - s.begin(), 38);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal ptrdiff_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"37"));
+        if (sizeof(std::ptrdiff_t) == 4) {
+            assertNotEquals("hex 32-bit ptrdiff_t in " + lastLogLine, std::u16string::npos,
+                            lastLogLine.find(u"25"));
+        } else {
+            assertEquals("NO hex ptrdiff_t in " + lastLogLine, std::u16string::npos,
+                         lastLogLine.find(u"25"));
+        }
+        // Making the enum inherit from int8_t, which is UBool, also allows us to check that we don’t
+        // fall into the UBool overload (this would have been the case before this got templatized).
+        enum Button : int8_t {
+            Abort = 65,
+            Retry = 66,
+            Ignore = 67,
+        };
+        metatest.assertEquals(WHERE "should fail", Abort, Retry);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal enum in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"65"));
+        assertEquals("NO hex enum in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"41"));
+        assertEquals("NO UBool in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"UBool"));
+    }
+
+    void testAssertNotEquals() {
+        std::u16string lastLogLine;
+        TestableIntlTest metatest(&lastLogLine);
+        // Booleans: Both UBool, mixed, both bool.
+        metatest.assertNotEquals(WHERE "should fail", static_cast<UBool>(true), static_cast<UBool>(true));
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("boolean in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"true"));
+        metatest.assertNotEquals(WHERE "should fail", static_cast<UBool>(true), true);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("boolean in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"true"));
+        metatest.assertNotEquals(WHERE "should fail", true, true);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("boolean in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"true"));
+        // The mysterious int32_t.
+        metatest.assertNotEquals(WHERE "should fail", -1, -1);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32", std::u16string::npos, lastLogLine.find(u"-1"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"0xFFFFFFFF"));
+        metatest.assertNotEquals(WHERE "should fail", 65, 65);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"0x41"));
+        metatest.assertNotEquals(WHERE "should fail", UnicodeString("").char32At(0),
+                              UnicodeString("B").char32At(1));
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65535"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"0xFFFF"));
+        metatest.assertNotEquals(WHERE "should fail", UnicodeString("AB").char32At(0),
+                              UnicodeString("BA").char32At(1));
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal int32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65"));
+        assertNotEquals("hex int32 in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"0x41"));
+        // Definitely characters.
+        // char32_t.
+        metatest.assertNotEquals(WHERE "should fail", U'A', U'A');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char32_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertNotEquals(WHERE "should fail", UnicodeString("A").char32At(0), U'A');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char32_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertNotEquals(WHERE "should fail", U'A', 0x41);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char32_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        // char16_t.
+        metatest.assertNotEquals(WHERE "should fail", u'A', u'A');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char16_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertNotEquals(WHERE "should fail", 0x41, u'A');
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char16_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        metatest.assertNotEquals(WHERE "should fail", UnicodeString("A").charAt(0), 0x41);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("char16_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"U+0041 A"));
+        // Actual numeric comparisons.
+        metatest.assertNotEquals(WHERE "should fail", static_cast<uint32_t>(65), 65);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal uint32 in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"65"));
+        assertEquals("NO hex uint32 in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"41"));
+        constexpr std::u16string_view s = u"𒌉 𒂍𒁾𒁀𒀀 𒌓 𒌌𒆷𒀀𒀭 𒈨𒂠 𒉌𒁺𒉈𒂗";
+        metatest.assertNotEquals(WHERE "should fail", s.size(), 37);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal size_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"37"));
+        assertEquals("NO hex size_t in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"25"));
+        // This one is int32_t on 32-bit platforms.
+        metatest.assertNotEquals(WHERE "should fail", s.end() - s.begin(), 37);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal ptrdiff_t in " + lastLogLine, std::u16string::npos,
+                        lastLogLine.find(u"37"));
+        if (sizeof(std::ptrdiff_t) == 4) {
+            assertNotEquals("hex 32-bit ptrdiff_t in " + lastLogLine, std::u16string::npos,
+                            lastLogLine.find(u"25"));
+        } else {
+            assertEquals("NO hex ptrdiff_t in " + lastLogLine, std::u16string::npos,
+                         lastLogLine.find(u"25"));
+        }
+        // Making the enum inherit from int8_t, which is UBool, also allows us to check that we don’t
+        // fall into the UBool overload (this would have been the case before this got templatized).
+        enum Button : int8_t {
+            Abort = 65,
+            Retry = 66,
+            Ignore = 67,
+        };
+        metatest.assertNotEquals(WHERE "should fail", Abort, Abort);
+        assertEquals(WHERE "should have failed", 1, metatest.newErrors());
+        assertNotEquals("decimal enum in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"65"));
+        assertEquals("NO hex enum in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"41"));
+        assertEquals("NO UBool in " + lastLogLine, std::u16string::npos, lastLogLine.find(u"UBool"));
+    }
+};
+
+extern IntlTest *createIntlTestTest() {
+    return new IntlTestTest();
+}
diff --git a/icu4c/source/test/intltest/itutil.cpp b/icu4c/source/test/intltest/itutil.cpp
index d387541164bc..f564f4029a36 100644
--- a/icu4c/source/test/intltest/itutil.cpp
+++ b/icu4c/source/test/intltest/itutil.cpp
@@ -52,11 +52,13 @@ extern IntlTest *createStaticUnicodeSetsTest();
 #endif
 static IntlTest *createUHashTest();
 extern IntlTest *createUTFIteratorTest();
+extern IntlTest *createIntlTestTest();
 
 void IntlTestUtilities::runIndexedTest( int32_t index, UBool exec, const char* &name, char* par )
 {
     if (exec) logln("TestSuite Utilities: ");
     TESTCASE_AUTO_BEGIN;
+    TESTCASE_AUTO_CREATE_CLASS(IntlTestTest);
     TESTCASE_AUTO_CLASS(MultithreadTest);
     TESTCASE_AUTO_CLASS(StringTest);
     TESTCASE_AUTO_CLASS(UnicodeStringTest);
diff --git a/icu4c/source/test/intltest/loctest.cpp b/icu4c/source/test/intltest/loctest.cpp
index 4c7cec7f1605..3a53673bc523 100644
--- a/icu4c/source/test/intltest/loctest.cpp
+++ b/icu4c/source/test/intltest/loctest.cpp
@@ -4372,7 +4372,7 @@ LocaleTest::TestCreateKeywordSet() {
             status);
     status.errIfFailureAndReset("\"%s\"", l.getName());
 
-    assertEquals("set::size()", 2, static_cast<int32_t>(result.size()));
+    assertEquals("set::size()", 2, result.size());
     assertTrue("set::find(\"calendar\")",
                result.find("calendar") != result.end());
     assertTrue("set::find(\"collation\")",
@@ -4391,7 +4391,7 @@ LocaleTest::TestCreateKeywordSetEmpty() {
             status);
     status.errIfFailureAndReset("\"%s\"", l.getName());
 
-    assertEquals("set::size()", 0, static_cast<int32_t>(result.size()));
+    assertEquals("set::size()", 0, result.size());
 }
 
 void
@@ -4427,7 +4427,7 @@ LocaleTest::TestCreateUnicodeKeywordSet() {
             status);
     status.errIfFailureAndReset("\"%s\"", l.getName());
 
-    assertEquals("set::size()", 2, static_cast<int32_t>(result.size()));
+    assertEquals("set::size()", 2, result.size());
     assertTrue("set::find(\"ca\")",
                result.find("ca") != result.end());
     assertTrue("set::find(\"co\")",
@@ -4451,7 +4451,7 @@ LocaleTest::TestCreateUnicodeKeywordSetEmpty() {
             status);
     status.errIfFailureAndReset("\"%s\"", l.getName());
 
-    assertEquals("set::size()", 0, static_cast<int32_t>(result.size()));
+    assertEquals("set::size()", 0, result.size());
 
     LocalPointer<StringEnumeration> se(l.createUnicodeKeywords(status), status);
     assertTrue("createUnicodeKeywords", se.isNull());
@@ -4476,7 +4476,7 @@ LocaleTest::TestCreateUnicodeKeywordSetWithPrivateUse() {
                result.find("x") == result.end());
     assertTrue("getUnicodeKeywords set::find(\"foo\")",
                result.find("foo") == result.end());
-    assertEquals("set::size()", 1, static_cast<int32_t>(result.size()));
+    assertEquals("set::size()", 1, result.size());
 
     LocalPointer<StringEnumeration> se(l.createUnicodeKeywords(status), status);
     status.errIfFailureAndReset("\"%s\" createUnicodeKeywords()", l.getName());
diff --git a/icu4c/source/test/intltest/normconf.cpp b/icu4c/source/test/intltest/normconf.cpp
index cdb917c77787..3b28419e98c0 100644
--- a/icu4c/source/test/intltest/normconf.cpp
+++ b/icu4c/source/test/intltest/normconf.cpp
@@ -511,13 +511,14 @@ UBool NormalizerConformanceTest::checkNorm(UNormalizationMode mode, int32_t opti
 
     // Do the Edits cover the entire input & output?
     UBool pass = true;
-    pass &= assertEquals("edits.hasChanges()", static_cast<UBool>(s8 != out8), edits.hasChanges());
+    pass &= assertEquals("edits.hasChanges()", s8 != out8, edits.hasChanges());
     pass &= assertEquals("edits.lengthDelta()",
-                         static_cast<int32_t>(out8.length() - s8.length()), edits.lengthDelta());
+                         static_cast<int64_t>(out8.length()) - static_cast<int64_t>(s8.length()),
+                         edits.lengthDelta());
     Edits::Iterator iter = edits.getCoarseIterator();
     while (iter.next(errorCode)) {}
-    pass &= assertEquals("edits source length", static_cast<int32_t>(s8.length()), iter.sourceIndex());
-    pass &= assertEquals("edits destination length", static_cast<int32_t>(out8.length()), iter.destinationIndex());
+    pass &= assertEquals("edits source length", s8.length(), iter.sourceIndex());
+    pass &= assertEquals("edits destination length", out8.length(), iter.destinationIndex());
     return pass;
 }
 
diff --git a/icu4c/source/test/intltest/numbertest_decimalquantity.cpp b/icu4c/source/test/intltest/numbertest_decimalquantity.cpp
index 8eeef7b11365..80c3eb4daf25 100644
--- a/icu4c/source/test/intltest/numbertest_decimalquantity.cpp
+++ b/icu4c/source/test/intltest/numbertest_decimalquantity.cpp
@@ -431,7 +431,7 @@ void DecimalQuantityTest::testMaxDigits() {
     assertEquals("Should trim, toPlainString", "76.54", dq.toPlainString());
     assertEquals("Should trim, toScientificString", "7.654E+1", dq.toScientificString());
     assertEquals("Should trim, toLong", 76LL, dq.toLong(true));
-    assertEquals("Should trim, toFractionLong", static_cast<int64_t>(54), static_cast<int64_t>(dq.toFractionLong(false)));
+    assertEquals("Should trim, toFractionLong", 54, dq.toFractionLong(false));
     assertEquals("Should trim, toDouble", 76.54, dq.toDouble());
     // To test DecNum output, check the round-trip.
     DecNum dn;
diff --git a/icu4c/source/test/intltest/numfmtst.cpp b/icu4c/source/test/intltest/numfmtst.cpp
index 2c54a696c48c..6c74689f3e3b 100644
--- a/icu4c/source/test/intltest/numfmtst.cpp
+++ b/icu4c/source/test/intltest/numfmtst.cpp
@@ -8197,7 +8197,7 @@ void NumberFormatTest::TestCurrencyUsage() {
 
             // test the getter here
             UCurrencyUsage curUsage = fmt->getCurrencyUsage();
-            assertEquals("Test usage getter - standard", static_cast<int32_t>(curUsage), static_cast<int32_t>(UCURR_USAGE_STANDARD));
+            assertEquals("Test usage getter - standard", curUsage, UCURR_USAGE_STANDARD);
 
             fmt->setCurrencyUsage(UCURR_USAGE_CASH, &status);
         }else{
@@ -8209,7 +8209,7 @@ void NumberFormatTest::TestCurrencyUsage() {
 
         // must be usage = cash
         UCurrencyUsage curUsage = fmt->getCurrencyUsage();
-        assertEquals("Test usage getter - cash", static_cast<int32_t>(curUsage), static_cast<int32_t>(UCURR_USAGE_CASH));
+        assertEquals("Test usage getter - cash", curUsage, UCURR_USAGE_CASH);
 
         UnicodeString cash_currency;
         fmt->format(agent,cash_currency);
@@ -9031,9 +9031,9 @@ void NumberFormatTest::TestFormatFailIfMoreThanMaxDigits() {
     if (status.errDataIfFailureAndReset()) {
         return;
     }
-    assertEquals("Coverage for getter 1", static_cast<UBool>(false), df.isFormatFailIfMoreThanMaxDigits());
+    assertEquals("Coverage for getter 1", false, df.isFormatFailIfMoreThanMaxDigits());
     df.setFormatFailIfMoreThanMaxDigits(true);
-    assertEquals("Coverage for getter 2", static_cast<UBool>(true), df.isFormatFailIfMoreThanMaxDigits());
+    assertEquals("Coverage for getter 2", true, df.isFormatFailIfMoreThanMaxDigits());
     df.setMaximumIntegerDigits(2);
     UnicodeString result;
     df.format(1234, result, status);
@@ -9047,9 +9047,9 @@ void NumberFormatTest::TestParseCaseSensitive() {
     if (status.errDataIfFailureAndReset()) {
         return;
     }
-    assertEquals("Coverage for getter 1", static_cast<UBool>(false), df.isParseCaseSensitive());
+    assertEquals("Coverage for getter 1", false, df.isParseCaseSensitive());
     df.setParseCaseSensitive(true);
-    assertEquals("Coverage for getter 1", static_cast<UBool>(true), df.isParseCaseSensitive());
+    assertEquals("Coverage for getter 1", true, df.isParseCaseSensitive());
     Formattable result;
     ParsePosition ppos;
     df.parse(u"1e2", result, ppos);
@@ -9064,9 +9064,9 @@ void NumberFormatTest::TestParseNoExponent() {
     if (status.errDataIfFailureAndReset()) {
         return;
     }
-    assertEquals("Coverage for getter 1", static_cast<UBool>(false), df.isParseNoExponent());
+    assertEquals("Coverage for getter 1", false, df.isParseNoExponent());
     df.setParseNoExponent(true);
-    assertEquals("Coverage for getter 1", static_cast<UBool>(true), df.isParseNoExponent());
+    assertEquals("Coverage for getter 1", true, df.isParseNoExponent());
     Formattable result;
     ParsePosition ppos;
     df.parse(u"1E2", result, ppos);
@@ -9081,9 +9081,9 @@ void NumberFormatTest::TestSignAlwaysShown() {
     if (status.errDataIfFailureAndReset()) {
         return;
     }
-    assertEquals("Coverage for getter 1", static_cast<UBool>(false), df.isSignAlwaysShown());
+    assertEquals("Coverage for getter 1", false, df.isSignAlwaysShown());
     df.setSignAlwaysShown(true);
-    assertEquals("Coverage for getter 1", static_cast<UBool>(true), df.isSignAlwaysShown());
+    assertEquals("Coverage for getter 1", true, df.isSignAlwaysShown());
     UnicodeString result;
     df.format(1234, result, status);
     status.errIfFailureAndReset();
diff --git a/icu4c/source/test/intltest/pluralmaptest.cpp b/icu4c/source/test/intltest/pluralmaptest.cpp
index 4d8b745df54e..897065c2291a 100644
--- a/icu4c/source/test/intltest/pluralmaptest.cpp
+++ b/icu4c/source/test/intltest/pluralmaptest.cpp
@@ -56,25 +56,16 @@ void PluralMapTest::runIndexedTest(int32_t index, UBool exec, const char* &name,
 }
 
 void PluralMapTest::TestToCategory() {
-    assertEquals("", static_cast<int32_t>(PluralMapBase::OTHER), PluralMapBase::toCategory("other"));
-    assertEquals("", static_cast<int32_t>(PluralMapBase::ZERO), PluralMapBase::toCategory("zero"));
-    assertEquals("", static_cast<int32_t>(PluralMapBase::ONE), PluralMapBase::toCategory("one"));
-    assertEquals("", static_cast<int32_t>(PluralMapBase::TWO), PluralMapBase::toCategory("two"));
-    assertEquals("", static_cast<int32_t>(PluralMapBase::FEW), PluralMapBase::toCategory("few"));
-    assertEquals("", static_cast<int32_t>(PluralMapBase::MANY), PluralMapBase::toCategory("many"));
-    assertEquals("", static_cast<int32_t>(PluralMapBase::NONE), PluralMapBase::toCategory("Many"));
-    assertEquals(
-            "",
-            static_cast<int32_t>(PluralMapBase::FEW),
-            PluralMapBase::toCategory(UnicodeString("few")));
-    assertEquals(
-            "",
-            static_cast<int32_t>(PluralMapBase::MANY),
-            PluralMapBase::toCategory(UnicodeString("many")));
-    assertEquals(
-            "",
-            static_cast<int32_t>(PluralMapBase::NONE),
-            PluralMapBase::toCategory(UnicodeString("Many")));
+    assertEquals("", PluralMapBase::OTHER, PluralMapBase::toCategory("other"));
+    assertEquals("", PluralMapBase::ZERO, PluralMapBase::toCategory("zero"));
+    assertEquals("", PluralMapBase::ONE, PluralMapBase::toCategory("one"));
+    assertEquals("", PluralMapBase::TWO, PluralMapBase::toCategory("two"));
+    assertEquals("", PluralMapBase::FEW, PluralMapBase::toCategory("few"));
+    assertEquals("", PluralMapBase::MANY, PluralMapBase::toCategory("many"));
+    assertEquals("", PluralMapBase::NONE, PluralMapBase::toCategory("Many"));
+    assertEquals("", PluralMapBase::FEW, PluralMapBase::toCategory(UnicodeString("few")));
+    assertEquals("", PluralMapBase::MANY, PluralMapBase::toCategory(UnicodeString("many")));
+    assertEquals("", PluralMapBase::NONE, PluralMapBase::toCategory(UnicodeString("Many")));
 }
 
 void PluralMapTest::TestGetCategoryName() {
@@ -117,24 +108,24 @@ void PluralMapTest::TestIterate() {
     PluralMapBase::Category index = PluralMapBase::NONE;
     const UnicodeString *current = map.next(index);
     assertEquals("", "pickles", *current);
-    assertEquals("", static_cast<int32_t>(PluralMapBase::OTHER), index);
+    assertEquals("", PluralMapBase::OTHER, index);
     current = map.next(index);
     assertEquals("", "pickle", *current);
-    assertEquals("", static_cast<int32_t>(PluralMapBase::ONE), index);
+    assertEquals("", PluralMapBase::ONE, index);
     current = map.next(index);
     assertEquals("", "picklefew", *current);
-    assertEquals("", static_cast<int32_t>(PluralMapBase::FEW), index);
+    assertEquals("", PluralMapBase::FEW, index);
     current = map.next(index);
-    assertEquals("", static_cast<int32_t>(PluralMapBase::CATEGORY_COUNT), index);
+    assertEquals("", PluralMapBase::CATEGORY_COUNT, index);
     assertTrue("", current == nullptr);
 
     PluralMapForPluralMapTest map2;
     index = PluralMapBase::NONE;
     current = map2.next(index);
     assertEquals("", "", *current);
-    assertEquals("", static_cast<int32_t>(PluralMapBase::OTHER), index);
+    assertEquals("", PluralMapBase::OTHER, index);
     current = map2.next(index);
-    assertEquals("", static_cast<int32_t>(PluralMapBase::CATEGORY_COUNT), index);
+    assertEquals("", PluralMapBase::CATEGORY_COUNT, index);
     assertTrue("", current == nullptr);
 }
 
diff --git a/icu4c/source/test/intltest/quantityformattertest.cpp b/icu4c/source/test/intltest/quantityformattertest.cpp
index 3c410bce6a01..9e4fbdf211a8 100644
--- a/icu4c/source/test/intltest/quantityformattertest.cpp
+++ b/icu4c/source/test/intltest/quantityformattertest.cpp
@@ -47,12 +47,12 @@ void QuantityFormatterTest::TestBasic() {
     assertFalse(
             "adding bad variant",
             fmt.addIfAbsent("a bad variant", "{0} pounds", status));
-    assertEquals("adding bad variant status", static_cast<int32_t>(U_ILLEGAL_ARGUMENT_ERROR), status);
+    assertEquals("adding bad variant status", U_ILLEGAL_ARGUMENT_ERROR, status);
     status = U_ZERO_ERROR;
     assertFalse(
             "Adding bad pattern",
             fmt.addIfAbsent("other", "{0} {1} too many placeholders", status));
-    assertEquals("adding bad pattern status", static_cast<int32_t>(U_ILLEGAL_ARGUMENT_ERROR), status);
+    assertEquals("adding bad pattern status", U_ILLEGAL_ARGUMENT_ERROR, status);
     status = U_ZERO_ERROR;
     assertFalse("isValid with no patterns", fmt.isValid());
     assertTrue(
diff --git a/icu4c/source/test/intltest/reldatefmttest.cpp b/icu4c/source/test/intltest/reldatefmttest.cpp
index cbcdeb6c325a..dcb63fe9c4dd 100644
--- a/icu4c/source/test/intltest/reldatefmttest.cpp
+++ b/icu4c/source/test/intltest/reldatefmttest.cpp
@@ -1109,11 +1109,9 @@ void RelativeDateTimeFormatterTest::TestGetters() {
     // copy and assignment.
     RelativeDateTimeFormatter fmt2(fmt);
     fmt3 = fmt2;
-    assertEquals("style", static_cast<int32_t>(UDAT_STYLE_NARROW), fmt3.getFormatStyle());
-    assertEquals(
-            "context",
-            static_cast<int32_t>(UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE),
-            fmt3.getCapitalizationContext());
+    assertEquals("style", UDAT_STYLE_NARROW, fmt3.getFormatStyle());
+    assertEquals("context", UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE,
+                 fmt3.getCapitalizationContext());
     assertSuccess("", status);
 }
 
diff --git a/icu4c/source/test/intltest/simpleformattertest.cpp b/icu4c/source/test/intltest/simpleformattertest.cpp
index fe272b1e4059..079afce705eb 100644
--- a/icu4c/source/test/intltest/simpleformattertest.cpp
+++ b/icu4c/source/test/intltest/simpleformattertest.cpp
@@ -91,10 +91,10 @@ void SimpleFormatterTest::TestNoArguments() {
 void SimpleFormatterTest::TestSyntaxErrors() {
     UErrorCode status = U_ZERO_ERROR;
     SimpleFormatter fmt("{}", status);
-    assertEquals("syntax error {}", static_cast<int32_t>(U_ILLEGAL_ARGUMENT_ERROR), status);
+    assertEquals("syntax error {}", U_ILLEGAL_ARGUMENT_ERROR, status);
     status = U_ZERO_ERROR;
     fmt.applyPattern("{12d", status);
-    assertEquals("syntax error {12d", static_cast<int32_t>(U_ILLEGAL_ARGUMENT_ERROR), status);
+    assertEquals("syntax error {12d", U_ILLEGAL_ARGUMENT_ERROR, status);
 }
 
 void SimpleFormatterTest::TestOneArgument() {
diff --git a/icu4c/source/test/intltest/strcase.cpp b/icu4c/source/test/intltest/strcase.cpp
index 70cb8a399674..1dcf63907f05 100644
--- a/icu4c/source/test/intltest/strcase.cpp
+++ b/icu4c/source/test/intltest/strcase.cpp
@@ -893,8 +893,7 @@ void StringCaseTest::assertGreekUpperNormalized(const UnicodeString &s16,
             expectedErrorCode = U_STRING_NOT_TERMINATED_WARNING;
         } else {
             expectedErrorCode = U_ZERO_ERROR;
-            // Casts to int32_t to avoid matching UBool.
-            assertEquals(msg + cap + " NUL", static_cast<int32_t>(0), static_cast<int32_t>(dest8b[length]));
+            assertEquals(msg + cap + " NUL", 0, dest8b[length]);
         }
         assertEquals(msg + cap + " errorCode", expectedErrorCode, errorCode);
         if (cap >= expected8Length) {
diff --git a/icu4c/source/test/intltest/strtest.cpp b/icu4c/source/test/intltest/strtest.cpp
index 7537c6f4d9ce..73c09fc4e8f1 100644
--- a/icu4c/source/test/intltest/strtest.cpp
+++ b/icu4c/source/test/intltest/strtest.cpp
@@ -503,7 +503,7 @@ StringTest::TestStringPieceOther() {
     Other other;
     StringPiece piece(other);
 
-    assertEquals("size()", piece.size(), static_cast<int32_t>(other.size()));
+    assertEquals("size()", piece.size(), other.size());
     assertEquals("data()", piece.data(), other.data());
 }
 
@@ -876,6 +876,6 @@ void
 StringTest::Testctou() {
   const char *cs = "Fa\\u0127mu";
   UnicodeString u = ctou(cs);
-  assertEquals("Testing unescape@0", static_cast<int32_t>(0x0046), u.charAt(0));
-  assertEquals("Testing unescape@2", static_cast<int32_t>(295), u.charAt(2));
+  assertEquals("Testing unescape@0", 0x0046, u.charAt(0));
+  assertEquals("Testing unescape@2", 295, u.charAt(2));
 }
diff --git a/icu4c/source/test/intltest/tztest.cpp b/icu4c/source/test/intltest/tztest.cpp
index 968f2473135f..6a0887e46fe6 100644
--- a/icu4c/source/test/intltest/tztest.cpp
+++ b/icu4c/source/test/intltest/tztest.cpp
@@ -2057,14 +2057,14 @@ void TimeZoneTest::TestCanonicalIDAPI() {
     UnicodeString canonicalID;
     UErrorCode ec = U_ZERO_ERROR;
     UnicodeString *pResult = &TimeZone::getCanonicalID(bogus, canonicalID, ec);
-    assertEquals("TimeZone::getCanonicalID(bogus) should fail", static_cast<int32_t>(U_ILLEGAL_ARGUMENT_ERROR), ec);
+    assertEquals("TimeZone::getCanonicalID(bogus) should fail", U_ILLEGAL_ARGUMENT_ERROR, ec);
     assertTrue("TimeZone::getCanonicalID(bogus) should return the dest string", pResult == &canonicalID);
 
     // U_FAILURE on input.
     UnicodeString berlin("Europe/Berlin");
     ec = U_MEMORY_ALLOCATION_ERROR;
     pResult = &TimeZone::getCanonicalID(berlin, canonicalID, ec);
-    assertEquals("TimeZone::getCanonicalID(failure) should fail", static_cast<int32_t>(U_MEMORY_ALLOCATION_ERROR), ec);
+    assertEquals("TimeZone::getCanonicalID(failure) should fail", U_MEMORY_ALLOCATION_ERROR, ec);
     assertTrue("TimeZone::getCanonicalID(failure) should return the dest string", pResult == &canonicalID);
 
     // Valid input should un-bogus the dest string.
@@ -2681,7 +2681,7 @@ void TimeZoneTest::TestGetIanaID() {
 
         TimeZone::getIanaID(inputID, ianaID, sts);
         if (u_strcmp(TESTDATA[i].expected, UNKNOWN) == 0) {
-            assertEquals(inputID + " should fail", static_cast<int32_t>(U_ILLEGAL_ARGUMENT_ERROR), sts);
+            assertEquals(inputID + " should fail", U_ILLEGAL_ARGUMENT_ERROR, sts);
             assertTrue(inputID + " should set bogus", ianaID.isBogus());
         } else {
             assertEquals(inputID, UnicodeString(TESTDATA[i].expected), ianaID);
diff --git a/icu4c/source/test/intltest/ucdtest.cpp b/icu4c/source/test/intltest/ucdtest.cpp
index 36f40c327136..9f9c439750c0 100644
--- a/icu4c/source/test/intltest/ucdtest.cpp
+++ b/icu4c/source/test/intltest/ucdtest.cpp
@@ -493,7 +493,7 @@ void UnicodeTest::TestScriptMetadata() {
                          static_cast<int32_t>(charScript), static_cast<int32_t>(uscript_getScript(firstChar, errorCode)));
             assertEquals("RTL vs. set", rtl.contains(firstChar), uscript_isRightToLeft(sc));
             assertEquals("cased vs. set", cased.contains(firstChar), uscript_isCased(sc));
-            assertEquals("encoded, has characters", static_cast<UBool>(sc == charScript), static_cast<UBool>(!scriptSet.isEmpty()));
+            assertEquals("encoded, has characters", sc == charScript, !scriptSet.isEmpty());
             if(uscript_isRightToLeft(sc)) {
                 rtl.removeAll(scriptSet);
             }
diff --git a/icu4c/source/test/intltest/ustrtest.cpp b/icu4c/source/test/intltest/ustrtest.cpp
index f0466982cff2..56976b3e3d23 100644
--- a/icu4c/source/test/intltest/ustrtest.cpp
+++ b/icu4c/source/test/intltest/ustrtest.cpp
@@ -2412,7 +2412,7 @@ void UnicodeStringTest::TestLargeAppend() {
             str2.releaseBuffer(remain);
             dest.append(str2);
             total += remain;
-            assertEquals("When a string of exactly the maximum size works", static_cast<int64_t>(kMaxCapacity), total);
+            assertEquals("When a string of exactly the maximum size works", kMaxCapacity, total);
             assertEquals("When a string of exactly the maximum size works", kMaxCapacity, dest.length());
             assertFalse("dest is not bogus", dest.isBogus());
 
@@ -2459,11 +2459,11 @@ void UnicodeStringTest::TestU16StringView() {
 
     UnicodeString aliasFromSV = UnicodeString::readOnlyAlias(sv16);
     assertTrue("aliasFromSV pointer alias", aliasFromSV.getBuffer() == sv16.data());
-    assertEquals("aliasFromSV length", static_cast<int32_t>(sv16.length()), aliasFromSV.length());
+    assertEquals("aliasFromSV length", sv16.length(), aliasFromSV.length());
 
     UnicodeString aliasFromStr = UnicodeString::readOnlyAlias(str16);
     assertTrue("aliasFromStr pointer alias", aliasFromStr.getBuffer() == str16.data());
-    assertEquals("aliasFromStr length", static_cast<int32_t>(str16.length()), aliasFromStr.length());
+    assertEquals("aliasFromStr length", str16.length(), aliasFromStr.length());
 
     UnicodeString aliasFromUStr = UnicodeString::readOnlyAlias(ustr);
     assertTrue("aliasFromUStr pointer alias", aliasFromUStr.getBuffer() == ustr.getBuffer());
@@ -2514,7 +2514,7 @@ void UnicodeStringTest::TestU16StringView() {
     // Convert UnicodeString to string view.
     std::u16string_view sv16FromUniStr(any);
     assertTrue("sv16FromUniStr buffer alias", sv16FromUniStr.data() == any.getBuffer());
-    assertEquals("sv16FromUniStr length", any.length(), static_cast<int32_t>(sv16FromUniStr.length()));
+    assertEquals("sv16FromUniStr length", any.length(), sv16FromUniStr.length());
 
     // Just to show convenience: Convert UnicodeString to string view, then to std string.
     std::u16string str16FromUniStr(any);
diff --git a/icu4c/source/test/intltest/utfiteratortest.cpp b/icu4c/source/test/intltest/utfiteratortest.cpp
index 0cda864aa0f6..13708206519d 100644
--- a/icu4c/source/test/intltest/utfiteratortest.cpp
+++ b/icu4c/source/test/intltest/utfiteratortest.cpp
@@ -1140,7 +1140,7 @@ void UTFIteratorTest::checkUnits(
                          static_cast<UChar32>(part[j]), static_cast<UChar32>(unit));
             ++j;
         }
-        assertEquals("units.iter.length[i]", static_cast<int32_t>(part.length()), j);
+        assertEquals("units.iter.length[i]", part.length(), j);
     }
     if constexpr (type >= CONTIG) {
         assertTrue("stringView[i]", part == units.stringView());
diff --git a/icu4c/source/test/intltest/utxttest.cpp b/icu4c/source/test/intltest/utxttest.cpp
index 0293a6ca7170..526412400962 100644
--- a/icu4c/source/test/intltest/utxttest.cpp
+++ b/icu4c/source/test/intltest/utxttest.cpp
@@ -1594,18 +1594,18 @@ void UTextTest::Ticket13344() {
 
     assertSuccess("UTextTest::Ticket13344-status", status);
     UTEXT_SETNATIVEINDEX(ut.getAlias(), 3);
-    assertEquals("UTextTest::Ticket13344-lead", static_cast<int64_t>(3), utext_getNativeIndex(ut.getAlias()));
+    assertEquals("UTextTest::Ticket13344-lead", 3, utext_getNativeIndex(ut.getAlias()));
     UTEXT_SETNATIVEINDEX(ut.getAlias(), 4);
-    assertEquals("UTextTest::Ticket13344-trail", static_cast<int64_t>(3), utext_getNativeIndex(ut.getAlias()));
+    assertEquals("UTextTest::Ticket13344-trail", 3, utext_getNativeIndex(ut.getAlias()));
     UTEXT_SETNATIVEINDEX(ut.getAlias(), 5);
-    assertEquals("UTextTest::Ticket13344-bmp", static_cast<int64_t>(5), utext_getNativeIndex(ut.getAlias()));
+    assertEquals("UTextTest::Ticket13344-bmp", 5, utext_getNativeIndex(ut.getAlias()));
 
     utext_setNativeIndex(ut.getAlias(), 3);
-    assertEquals("UTextTest::Ticket13344-lead-2", static_cast<int64_t>(3), utext_getNativeIndex(ut.getAlias()));
+    assertEquals("UTextTest::Ticket13344-lead-2", 3, utext_getNativeIndex(ut.getAlias()));
     utext_setNativeIndex(ut.getAlias(), 4);
-    assertEquals("UTextTest::Ticket13344-trail-2", static_cast<int64_t>(3), utext_getNativeIndex(ut.getAlias()));
+    assertEquals("UTextTest::Ticket13344-trail-2", 3, utext_getNativeIndex(ut.getAlias()));
     utext_setNativeIndex(ut.getAlias(), 5);
-    assertEquals("UTextTest::Ticket13344-bmp-2", static_cast<int64_t>(5), utext_getNativeIndex(ut.getAlias()));
+    assertEquals("UTextTest::Ticket13344-bmp-2", 5, utext_getNativeIndex(ut.getAlias()));
 }
 
 // ICU-21653 UText does not handle access callback that changes chunk size


Reviews:
user: login: markusicu
body: very nice, thank you!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T18:28:17Z
user: login: eggrobin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T18:35:09Z
user: login: eggrobin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T18:35:28Z
user: login: markusicu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T18:37:39Z
user: login: eggrobin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T18:38:02Z
user: login: eggrobin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T18:39:42Z
user: login: eggrobin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T19:06:09Z
user: login: eggrobin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T19:16:00Z
user: login: markusicu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T19:41:25Z
user: login: markusicu
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-27T16:21:26Z

Review comments:
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: markusicu
body: no hex?
created_at: 2025-05-22T18:13:32Z
author_association: MEMBER
diff_hunk: @@ -241,6 +230,10 @@ UnicodeString toString(UBool b) {
   return b ? UnicodeString("true"):UnicodeString("false");
path: icu4c/source/test/intltest/intltest.cpp
user: login: markusicu
body: would it make sense to print the actual value if it's not 0 or 1, in case someone does assertEquals with tiny ints that are not booleans?
created_at: 2025-05-22T18:16:01Z
author_association: MEMBER
diff_hunk: @@ -2143,18 +2146,29 @@ UBool IntlTest::assertEquals(const char* message,
     return true;
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             UBool expected,
-                             UBool actual) {
+bool IntlTest::assertBooleanEquals(const char *message, int8_t expected, int8_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              toString(actual) +
-              "; expected " + toString(expected));
+        errln(UnicodeString("FAIL: ") + message + "; got " + toString(actual) + "; expected " +
+              toString(expected));
         return false;
     }
 #ifdef VERBOSE_ASSERTIONS
     else {
-      logln(UnicodeString("Ok: ") + message + "; got " + toString(actual));
+        logln(UnicodeString("Ok: ") + message + "; got " + toString(actual));
+    }
+#endif
+    return true;
+}
+
+bool IntlTest::assertBooleanNotEquals(const char *message, int8_t expected, int8_t actual) {
path: icu4c/source/test/intltest/intltest.cpp
user: login: markusicu
body: for ...NotEquals(), should we share implementations with the positive versions and parameterize?
created_at: 2025-05-22T18:17:51Z
author_association: MEMBER
diff_hunk: @@ -2259,9 +2273,22 @@ UBool IntlTest::assertEquals(const char* message,
     return true;
 }
 
-UBool IntlTest::assertNotEquals(const char* message,
-                                int32_t expectedNot,
-                                int32_t actual) {
+bool IntlTest::assertSigned64NotEquals(const char *message, int64_t expected, int64_t actual) {
+    if (expected == actual) {
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected != " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: markusicu
body: no hex?
created_at: 2025-05-22T18:18:39Z
author_association: MEMBER
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: eggrobin
body: The diff is misleading; we are in the function that takes int64_t (the hex was, and remains, in the one that takes int32_t).
created_at: 2025-05-22T18:35:09Z
author_association: MEMBER
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: eggrobin
body: See also the comments in the .h.
created_at: 2025-05-22T18:35:28Z
author_association: MEMBER
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: markusicu
body: I did know where we were ;-)
I am just wondering why we show hex for int32_t but only decimal for int64_t.
Sometimes large values would also be more readable in hex, e.g., for collation elements.

Not required here, but maybe consider adding hex output here?
created_at: 2025-05-22T18:37:39Z
author_association: MEMBER
diff_hunk: @@ -2143,18 +2146,29 @@ UBool IntlTest::assertEquals(const char* message,
     return true;
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             UBool expected,
-                             UBool actual) {
+bool IntlTest::assertBooleanEquals(const char *message, int8_t expected, int8_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              toString(actual) +
-              "; expected " + toString(expected));
+        errln(UnicodeString("FAIL: ") + message + "; got " + toString(actual) + "; expected " +
+              toString(expected));
         return false;
     }
 #ifdef VERBOSE_ASSERTIONS
     else {
-      logln(UnicodeString("Ok: ") + message + "; got " + toString(actual));
+        logln(UnicodeString("Ok: ") + message + "; got " + toString(actual));
+    }
+#endif
+    return true;
+}
+
+bool IntlTest::assertBooleanNotEquals(const char *message, int8_t expected, int8_t actual) {
path: icu4c/source/test/intltest/intltest.cpp
user: login: eggrobin
body: Doesn’t seem to be worth it for now. If we want to factorize more, I would probably go instead in the direction of a single assertEquals and a single assertNotEquals implementation, with a debugString function to show the values.
created_at: 2025-05-22T18:38:02Z
author_association: MEMBER
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: eggrobin
body: The idea was that showing hex gets a bit spammy in general, e.g., when comparing sizes.

But if we think we want hex always, we can simplify the code drastically, by dropping the int32_t overload entirely.
created_at: 2025-05-22T18:39:42Z
author_association: MEMBER
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: eggrobin
body: Hmm, wait, no, it gets messy.  This 64-bit overload gets used for all numeric types.
If we have negative numbers, we don’t want to log the hex of the unsigned representation of their widening.
(That is, if we have an int8_t that is -1, we want to show FF, not FFFFFFFF; the latter would be massively confusing if you are debugging some byte-twiddling code.)

So this would require lifting some kind of debugString out of the assert functions as I wrote in the other comment, which is fine, but then it would require templatizing it so it doesn’t widen. That gets messy because we cannot use UnicodeString in the header.

Let’s not go there for now.
created_at: 2025-05-22T19:06:09Z
author_association: MEMBER
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: eggrobin
body: > That gets messy because we cannot use UnicodeString in the header.

OK, admittedly we don’t _need_ to use UnicodeString here, std::string would do the job just fine for printing out integers.

It would be a nontrivial rewrite though, and we have stuff on top of this. How about doing it in a future PR on the same ticket?
created_at: 2025-05-22T19:16:00Z
author_association: MEMBER
diff_hunk: @@ -2091,12 +2084,22 @@ UBool IntlTest::assertEquals(const char* message, std::u16string_view expected,
         possibleDataError);
 }
 
-UBool IntlTest::assertEquals(const char* message,
-                             int32_t expected,
-                             int32_t actual) {
+bool IntlTest::assertSigned64Equals(const char *message, int64_t expected, int64_t actual) {
     if (expected != actual) {
-        errln(UnicodeString("FAIL: ") + message + "; got " +
-              actual + "=0x" + toHex(actual) +
+        errln(UnicodeString("FAIL: ") + message + "; got " + actual + "; expected " + expected);
path: icu4c/source/test/intltest/intltest.cpp
user: login: markusicu
body: > It would be a nontrivial rewrite though, and we have stuff on top of this. How about doing it in a future PR on the same ticket?

ok
created_at: 2025-05-22T19:41:25Z
author_association: MEMBER

PR comments:
user: login: jira-pull-request-webhook[bot]
created_at: 2025-05-22T02:13:12Z
body: Notice: the branch changed across the force-push!

- icu4c/source/test/intltest/intltesttest.cpp is different

[View Diff Across Force-Push](https://github.com/unicode-org/icu/compare/unicode-org:ec1385c..unicode-org:b479b99)

~ Your Friendly Jira-GitHub PR Checker Bot
author_association: NONE
user: login: jira-pull-request-webhook[bot]
created_at: 2025-05-22T11:59:59Z
body: Notice: the branch changed across the force-push!

- icu4c/source/test/intltest/Makefile.in is now changed in the branch

[View Diff Across Force-Push](https://github.com/unicode-org/icu/compare/unicode-org:b479b99..unicode-org:5223688)

~ Your Friendly Jira-GitHub PR Checker Bot
author_association: NONE
user: login: jira-pull-request-webhook[bot]
created_at: 2025-05-22T12:36:15Z
body: Notice: the branch changed across the force-push!

- icu4c/source/test/intltest/intltest.cpp is different

[View Diff Across Force-Push](https://github.com/unicode-org/icu/compare/unicode-org:9a062d4..unicode-org:2624f67)

~ Your Friendly Jira-GitHub PR Checker Bot
author_association: NONE
user: login: jira-pull-request-webhook[bot]
created_at: 2025-05-22T12:49:21Z
body: Notice: the branch changed across the force-push!

- icu4c/source/test/intltest/intltest.h is different

[View Diff Across Force-Push](https://github.com/unicode-org/icu/compare/unicode-org:2624f67..unicode-org:de75668)

~ Your Friendly Jira-GitHub PR Checker Bot
author_association: NONE
user: login: jira-pull-request-webhook[bot]
created_at: 2025-05-22T14:31:35Z
body: Hooray! The files in the branch are the same across the force-push. 😃

~ Your Friendly Jira-GitHub PR Checker Bot
author_association: NONE
user: login: jira-pull-request-webhook[bot]
created_at: 2025-05-22T14:55:39Z
body: Notice: the branch changed across the force-push!

- icu4c/source/test/intltest/normconf.cpp is different

[View Diff Across Force-Push](https://github.com/unicode-org/icu/compare/unicode-org:a5c5326..unicode-org:675bd6d)

~ Your Friendly Jira-GitHub PR Checker Bot
author_association: NONE
user: login: markusicu
created_at: 2025-05-22T18:29:27Z
body: I assume that you will want to merge two commits, so I added the magic incantation.
author_association: MEMBER
user: login: jira-pull-request-webhook[bot]
created_at: 2025-05-27T16:54:09Z
body: Hooray! The files in the branch are the same across the force-push. 😃

~ Your Friendly Jira-GitHub PR Checker Bot
author_association: NONE