| /* | |
| * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test | |
| * @bug 8354522 | |
| * @summary Check for cloning interference | |
| * @library /test/lib | |
| * @run junit/othervm --add-opens=java.base/java.text=ALL-UNNAMED CloneTest | |
| */ | |
| import jtreg.SkippedException; | |
| import org.junit.jupiter.api.Test; | |
| import java.lang.reflect.Field; | |
| import java.math.BigDecimal; | |
| import java.text.DecimalFormat; | |
| import java.util.concurrent.CountDownLatch; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import static org.junit.jupiter.api.Assertions.*; | |
| public class CloneTest { | |
| // Note: this is a white-box test that may fail if the implementation is changed | |
| @Test | |
| public void testClone() { | |
| DecimalFormat df = new DecimalFormat("#"); | |
| new CloneTester(df).testClone(); | |
| } | |
| // Note: this is a white-box test that may fail if the implementation is changed | |
| @Test | |
| public void testCloneAfterInit() { | |
| DecimalFormat df = new DecimalFormat("#"); | |
| // This initial use of the formatter initialises its internal state, which could | |
| // subsequently be shared across clones. This is key to reproducing this specific | |
| // issue. | |
| String _ = df.format(Math.PI); | |
| new CloneTester(df).testClone(); | |
| } | |
| private static class CloneTester { | |
| private final Field digitListField; | |
| private final Class<?> digitListClass; | |
| private final DecimalFormat original; | |
| public CloneTester(DecimalFormat original) { | |
| this.original = original; | |
| try { | |
| digitListField = DecimalFormat.class.getDeclaredField("digitList"); | |
| digitListField.setAccessible(true); | |
| DecimalFormat df = new DecimalFormat(); | |
| Object digitList = digitListField.get(df); | |
| digitListClass = digitList.getClass(); | |
| } catch (ReflectiveOperationException e) { | |
| throw new SkippedException("reflective access in white-box test failed", e); | |
| } | |
| } | |
| public void testClone() { | |
| try { | |
| DecimalFormat dfClone = (DecimalFormat) original.clone(); | |
| Object digits = valFromDigitList(original, "digits"); | |
| assertNotSame(digits, valFromDigitList(dfClone, "digits")); | |
| Object data = valFromDigitList(original, "data"); | |
| if (data != null) { | |
| assertNotSame(data, valFromDigitList(dfClone, "data")); | |
| } | |
| Object tempBuilder = valFromDigitList(original, "tempBuilder"); | |
| if (tempBuilder != null) { | |
| assertNotSame(data, valFromDigitList(dfClone, "data")); | |
| } | |
| assertEquals(digitListField.get(original), digitListField.get(dfClone)); | |
| } catch (ReflectiveOperationException e) { | |
| throw new SkippedException("reflective access in white-box test failed", e); | |
| } | |
| } | |
| private Object valFromDigitList(DecimalFormat df, String fieldName) throws ReflectiveOperationException { | |
| Object digitList = digitListField.get(df); | |
| Field field = digitListClass.getDeclaredField(fieldName); | |
| field.setAccessible(true); | |
| return field.get(digitList); | |
| } | |
| } | |
| // Tests that when DecimalFormat is cloned after use with | |
| // a long double/BigDecimal, clones will be independent. This is not an | |
| // exhaustive test. This tests for the issue of the same DigitList.data | |
| // array being reused across clones of DecimalFormat. | |
| @Test | |
| public void testCloneIndependence() { | |
| AtomicInteger mismatchCount = new AtomicInteger(0); | |
| DecimalFormat df = new DecimalFormat("#"); | |
| CountDownLatch startSignal = new CountDownLatch(1); | |
| // This initial use of the formatter initialises its internal state, which could | |
| // subsequently be shared across clones. This is key to reproducing this specific | |
| // issue. | |
| String _ = df.format(Math.PI); | |
| try (var ex = Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory())) { | |
| for (int i = 0; i < 5; i++) { | |
| final int finalI = i; | |
| // Each thread gets its own clone of df | |
| DecimalFormat threadDf = (DecimalFormat) df.clone(); | |
| Runnable task = () -> { | |
| try { | |
| startSignal.await(); | |
| for (int j = 0; j < 1_000; j++) { | |
| if (mismatchCount.get() > 0) { | |
| // Exit early if mismatch has already occurred | |
| break; | |
| } | |
| int value = finalI * j; | |
| String dfString = threadDf.format(BigDecimal.valueOf(value)); | |
| String str = String.valueOf(value); | |
| if (!str.equals(dfString)) { | |
| mismatchCount.getAndIncrement(); | |
| System.err.println("mismatch: str = " + str + " dfString = " + dfString); | |
| break; | |
| } | |
| } | |
| } catch (InterruptedException e) { | |
| throw new RuntimeException(e); | |
| } | |
| }; | |
| ex.execute(task); | |
| } | |
| startSignal.countDown(); // let all tasks start working at the same time | |
| } | |
| assertEquals(0, mismatchCount.get()); | |
| } | |
| } |