Skip to content

Commit debd22c

Browse files
committed
8366733: Re-examine older java.text NF, DF, and DFS serialization tests
Reviewed-by: mdoerr Backport-of: 24a7349
1 parent 3e04da1 commit debd22c

12 files changed

Lines changed: 320 additions & 577 deletions

test/jdk/java/text/Format/NumberFormat/DecimalFormat.114.txt renamed to test/jdk/java/text/Format/DecimalFormat/DecimalFormat.114.txt

File renamed without changes.

test/jdk/java/text/Format/NumberFormat/DecimalFormatSymbols.114.txt renamed to test/jdk/java/text/Format/DecimalFormat/DecimalFormatSymbols.114.txt

File renamed without changes.

test/jdk/java/text/Format/NumberFormat/DecimalFormatSymbols.142.txt renamed to test/jdk/java/text/Format/DecimalFormat/DecimalFormatSymbols.142.txt

File renamed without changes.

test/jdk/java/text/Format/NumberFormat/NumberFormat4185761a.ser.txt renamed to test/jdk/java/text/Format/DecimalFormat/NumberFormat4185761a.ser.txt

File renamed without changes.

test/jdk/java/text/Format/NumberFormat/NumberFormat4185761b.ser.txt renamed to test/jdk/java/text/Format/DecimalFormat/NumberFormat4185761b.ser.txt

File renamed without changes.
Lines changed: 316 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -22,65 +22,344 @@
2222
*/
2323
/*
2424
* @test
25-
* @bug 8327640
26-
* @summary Check parseStrict correctness for DecimalFormat serialization
27-
* @run junit/othervm SerializationTest
25+
* @bug 4069754 4067878 4101150 4185761 8327640
26+
* @library /java/text/testlib
27+
* @build HexDumpReader
28+
* @summary Check de-serialization correctness for DecimalFormat. That is, ensure the
29+
* behavior for each stream version is correct during de-serialization.
30+
* @run junit/othervm --add-opens java.base/java.text=ALL-UNNAMED SerializationTest
2831
*/
2932

30-
import org.junit.jupiter.api.BeforeAll;
33+
import org.junit.jupiter.api.Nested;
3134
import org.junit.jupiter.api.Test;
3235

33-
import java.io.FileInputStream;
34-
import java.io.FileOutputStream;
36+
import java.io.ByteArrayInputStream;
37+
import java.io.ByteArrayOutputStream;
3538
import java.io.IOException;
39+
import java.io.InputStream;
40+
import java.io.InvalidObjectException;
3641
import java.io.ObjectInputStream;
3742
import java.io.ObjectOutputStream;
43+
import java.io.Serializable;
44+
import java.lang.reflect.Field;
45+
import java.math.RoundingMode;
46+
import java.text.DateFormat;
47+
import java.text.DecimalFormat;
3848
import java.text.NumberFormat;
39-
import java.text.ParseException;
49+
import java.util.Date;
50+
import java.util.GregorianCalendar;
51+
import java.util.Random;
4052

53+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4154
import static org.junit.jupiter.api.Assertions.assertEquals;
55+
import static org.junit.jupiter.api.Assertions.assertFalse;
56+
import static org.junit.jupiter.api.Assertions.assertNotNull;
57+
import static org.junit.jupiter.api.Assertions.assertNull;
4258
import static org.junit.jupiter.api.Assertions.assertThrows;
59+
import static org.junit.jupiter.api.Assertions.assertTrue;
4360

4461
public class SerializationTest {
4562

46-
private static final NumberFormat FORMAT = NumberFormat.getInstance();
63+
@Nested // Test that rely on hex dump files that were written from older JDK versions
64+
class HexDumpTests {
4765

48-
@BeforeAll
49-
public static void mutateFormat() {
50-
FORMAT.setStrict(true);
66+
@Test // See 4101150 and CDF which is the serialized hex dump
67+
void JDK1_1_4Test() {
68+
// Reconstruct a 1.1.4 serializable class which has a DF holder
69+
var cdf = (CheckDecimalFormat) assertDoesNotThrow(
70+
() -> deSer("DecimalFormat.114.txt"));
71+
assertDoesNotThrow(cdf::Update); // Checks format call succeeds
72+
}
73+
74+
@Test // See 4185761
75+
void minMaxDigitsTest() {
76+
// Reconstructing a DFS stream from an older JDK version
77+
// The min digits are smaller than the max digits and should fail
78+
// minint maxint minfrac maxfrac
79+
// 0x122 0x121 0x124 0x123
80+
assertEquals("Digit count range invalid",
81+
assertThrows(InvalidObjectException.class,
82+
() -> deSer("NumberFormat4185761a.ser.txt")).getMessage());
83+
}
84+
85+
@Test // See 4185761
86+
void digitLimitTest() {
87+
// Reconstructing a DFS stream from an older JDK version
88+
// The digit values exceed the class invariant limits
89+
// minint maxint minfrac maxfrac
90+
// 0x311 0x312 0x313 0x314
91+
assertEquals("Digit count out of range",
92+
assertThrows(InvalidObjectException.class,
93+
() -> deSer("NumberFormat4185761b.ser.txt")).getMessage());
94+
}
5195
}
5296

97+
@Nested
98+
class VersionTests {
99+
100+
// Version 0 did not have exponential fields and defaulted the value to false
101+
@Test
102+
void version0Test() {
103+
var crafted = new DFBuilder()
104+
.setVer(0)
105+
.set("useExponentialNotation", true)
106+
.build();
107+
var bytes = ser(crafted);
108+
var df = assertDoesNotThrow(() -> deSer(bytes));
109+
// Ensure we do not observe exponential notation form
110+
assertFalse(df.format(0).contains("E"));
111+
}
112+
113+
// Version 1 did not support the affix pattern Strings. Ensure when they
114+
// are read in from the stream they are not defaulted and remain null.
115+
@Test
116+
void version1Test() {
117+
var crafted = new DFBuilder()
118+
.setVer(1)
119+
.set("posPrefixPattern", null)
120+
.set("posSuffixPattern", null)
121+
.set("negPrefixPattern", null)
122+
.set("negSuffixPattern", null)
123+
.build();
124+
var bytes = ser(crafted);
125+
var df = assertDoesNotThrow(() -> deSer(bytes));
126+
assertNull(readField(df, "posPrefixPattern"));
127+
assertNull(readField(df, "posSuffixPattern"));
128+
assertNull(readField(df, "negPrefixPattern"));
129+
assertNull(readField(df, "negSuffixPattern"));
130+
}
131+
132+
// Version 2 did not support the min/max int and frac digits.
133+
// Ensure the proper defaults are set.
134+
@Test
135+
void version2Test() {
136+
var crafted = new DFBuilder()
137+
.setVer(2)
138+
.set("maximumIntegerDigits", -1)
139+
.set("maximumFractionDigits", -1)
140+
.set("minimumIntegerDigits", -1)
141+
.set("minimumFractionDigits", -1)
142+
.build();
143+
var bytes = ser(crafted);
144+
var df = assertDoesNotThrow(() -> deSer(bytes));
145+
assertEquals(1, df.getMinimumIntegerDigits());
146+
assertEquals(3, df.getMaximumFractionDigits());
147+
assertEquals(309, df.getMaximumIntegerDigits());
148+
assertEquals(0, df.getMinimumFractionDigits());
149+
}
150+
151+
// Version 3 did not support rounding mode. Should default to HALF_EVEN
152+
@Test
153+
void version3Test() {
154+
var crafted = new DFBuilder()
155+
.setVer(3)
156+
.set("roundingMode", RoundingMode.UNNECESSARY)
157+
.build();
158+
var bytes = ser(crafted);
159+
var df = assertDoesNotThrow(() -> deSer(bytes));
160+
assertEquals(RoundingMode.HALF_EVEN, df.getRoundingMode());
161+
}
162+
}
163+
164+
// Some invariant checking in DF relies on checking NF fields.
165+
// Either via NF.readObject() or through super calls in DF.readObject
166+
@Nested // For all these nested tests, see 4185761
167+
class NumberFormatTests {
168+
169+
// Ensure the max integer value invariant is not exceeded
170+
@Test
171+
void integerTest() {
172+
var crafted = new DFBuilder()
173+
.setSuper("maximumIntegerDigits", 786)
174+
.setSuper("minimumIntegerDigits", 785)
175+
.build();
176+
var bytes = ser(crafted);
177+
assertEquals("Digit count out of range",
178+
assertThrows(InvalidObjectException.class, () -> deSer(bytes)).getMessage());
179+
}
180+
181+
// Ensure the max fraction value invariant is not exceeded
182+
@Test
183+
void fractionTest() {
184+
var crafted = new DFBuilder()
185+
.setSuper("maximumFractionDigits", 788)
186+
.setSuper("minimumFractionDigits", 787)
187+
.build();
188+
var bytes = ser(crafted);
189+
assertEquals("Digit count out of range",
190+
assertThrows(InvalidObjectException.class, () -> deSer(bytes)).getMessage());
191+
}
192+
193+
// Ensure the minimum integer digits cannot be greater than the max
194+
@Test
195+
void maxMinIntegerTest() {
196+
var crafted = new DFBuilder()
197+
.setSuper("maximumIntegerDigits", 5)
198+
.setSuper("minimumIntegerDigits", 6)
199+
.build();
200+
var bytes = ser(crafted);
201+
assertEquals("Digit count range invalid",
202+
assertThrows(InvalidObjectException.class, () -> deSer(bytes)).getMessage());
203+
}
204+
205+
// Ensure the minimum fraction digits cannot be greater than the max
206+
@Test
207+
void maxMinFractionTest() {
208+
var crafted = new DFBuilder()
209+
.setSuper("maximumFractionDigits", 5)
210+
.setSuper("minimumFractionDigits", 6)
211+
.build();
212+
var bytes = ser(crafted);
213+
assertEquals("Digit count range invalid",
214+
assertThrows(InvalidObjectException.class, () -> deSer(bytes)).getMessage());
215+
}
216+
}
217+
218+
// Ensure the serial version is updated to the current after de-serialization.
53219
@Test
54-
public void testSerialization() throws IOException, ClassNotFoundException {
55-
// Serialize
56-
serialize("fmt.ser", FORMAT);
57-
// Deserialize
58-
deserialize("fmt.ser", FORMAT);
59-
}
60-
61-
private void serialize(String fileName, NumberFormat... formats)
62-
throws IOException {
63-
try (ObjectOutputStream os = new ObjectOutputStream(
64-
new FileOutputStream(fileName))) {
65-
for (NumberFormat fmt : formats) {
66-
os.writeObject(fmt);
67-
}
220+
void versionTest() {
221+
var bytes = ser(new DFBuilder().setVer(-25).build());
222+
var df = assertDoesNotThrow(() -> deSer(bytes));
223+
assertEquals(4, readField(df, "serialVersionOnStream"));
224+
}
225+
226+
// Ensure strictness value is read properly when it is set.
227+
@Test
228+
void strictnessTest() {
229+
var crafted = new DecimalFormat();
230+
crafted.setStrict(true);
231+
var bytes = ser(crafted);
232+
var df = assertDoesNotThrow(() -> deSer(bytes));
233+
assertTrue(df.isStrict());
234+
}
235+
236+
// Ensure invalid grouping sizes are corrected to the default invariant.
237+
@Test
238+
void groupingSizeTest() {
239+
var crafted = new DFBuilder()
240+
.set("groupingSize", (byte) -5)
241+
.build();
242+
var bytes = ser(crafted);
243+
var df = assertDoesNotThrow(() -> deSer(bytes));
244+
assertEquals(3, df.getGroupingSize());
245+
}
246+
247+
// Ensure a de-serialized dFmt does not throw NPE from missing digitList
248+
// later when formatting. i.e. re-construct the transient digitList field
249+
@Test // See 4069754, 4067878
250+
void digitListTest() {
251+
var crafted = new DecimalFormat();
252+
var bytes = ser(crafted);
253+
var df = assertDoesNotThrow(() -> deSer(bytes));
254+
assertDoesNotThrow(() -> df.format(1));
255+
assertNotNull(readField(df, "digitList"));
256+
}
257+
258+
// Similar to the previous test, but the original regression test
259+
// which was a failure in DateFormat due to DecimalFormat NPE
260+
@Test // See 4069754 and 4067878
261+
void digitListDateFormatTest() {
262+
var fmt = new FooFormat();
263+
fmt.now();
264+
var bytes = ser(fmt);
265+
var ff = (FooFormat) assertDoesNotThrow(() -> deSer0(bytes));
266+
assertDoesNotThrow(ff::now);
267+
}
268+
269+
static class FooFormat implements Serializable {
270+
DateFormat dateFormat = DateFormat.getDateInstance();
271+
272+
public String now() {
273+
GregorianCalendar calendar = new GregorianCalendar();
274+
Date t = calendar.getTime();
275+
return dateFormat.format(t);
68276
}
69277
}
70278

71-
private static void deserialize(String fileName, NumberFormat... formats)
72-
throws IOException, ClassNotFoundException {
73-
try (ObjectInputStream os = new ObjectInputStream(
74-
new FileInputStream(fileName))) {
75-
for (NumberFormat fmt : formats) {
76-
NumberFormat obj = (NumberFormat) os.readObject();
77-
assertEquals(fmt, obj, "Serialized and deserialized"
78-
+ " objects do not match");
279+
// Utilities ----
79280

80-
String badNumber = "fooofooo23foo";
81-
assertThrows(ParseException.class, () -> fmt.parse(badNumber));
82-
assertThrows(ParseException.class, () -> obj.parse(badNumber));
281+
// Utility to serialize
282+
private static byte[] ser(Object obj) {
283+
return assertDoesNotThrow(() -> {
284+
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
285+
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream)) {
286+
oos.writeObject(obj);
287+
return byteArrayOutputStream.toByteArray();
83288
}
289+
}, "Unexpected error during serialization");
290+
}
291+
292+
// Utility to deserialize
293+
private static Object deSer0(byte[] bytes) throws IOException, ClassNotFoundException {
294+
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
295+
ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream)) {
296+
return ois.readObject();
84297
}
85298
}
299+
300+
// Convenience cast to DF
301+
private static DecimalFormat deSer(byte[] bytes) throws IOException, ClassNotFoundException {
302+
return (DecimalFormat) deSer0(bytes);
303+
}
304+
305+
// Utility to deserialize from file in hex format
306+
private static Object deSer(String file) throws IOException, ClassNotFoundException {
307+
try (InputStream stream = HexDumpReader.getStreamFromHexDump(file);
308+
ObjectInputStream ois = new ObjectInputStream(stream)) {
309+
return ois.readObject();
310+
}
311+
}
312+
313+
// Utility to read a private field
314+
private static Object readField(DecimalFormat df, String name) {
315+
return assertDoesNotThrow(() -> {
316+
var field = DecimalFormat.class.getDeclaredField(name);
317+
field.setAccessible(true);
318+
return field.get(df);
319+
}, "Unexpected error during field reading");
320+
}
321+
322+
// Utility class to build instances of DF via reflection
323+
private static class DFBuilder {
324+
325+
private final DecimalFormat df;
326+
327+
private DFBuilder() {
328+
df = new DecimalFormat();
329+
}
330+
331+
private DFBuilder setVer(Object value) {
332+
return set("serialVersionOnStream", value);
333+
}
334+
335+
private DFBuilder setSuper(String field, Object value) {
336+
return set(df.getClass().getSuperclass(), field, value);
337+
}
338+
339+
private DFBuilder set(String field, Object value) {
340+
return set(df.getClass(), field, value);
341+
}
342+
343+
private DFBuilder set(Class<?> clzz, String field, Object value) {
344+
return assertDoesNotThrow(() -> {
345+
Field f = clzz.getDeclaredField(field);
346+
f.setAccessible(true);
347+
f.set(df, value);
348+
return this;
349+
}, "Unexpected error during reflection setting");
350+
}
351+
352+
private DecimalFormat build() {
353+
return df;
354+
}
355+
}
356+
}
357+
358+
// Not nested, so that it can be recognized and cast correctly for the 1.1.4 test
359+
class CheckDecimalFormat implements Serializable {
360+
DecimalFormat _decFormat = (DecimalFormat) NumberFormat.getInstance();
361+
public String Update() {
362+
Random r = new Random();
363+
return _decFormat.format(r.nextDouble());
364+
}
86365
}

0 commit comments

Comments
 (0)