Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
156 lines
6.1 KiB
Java
156 lines
6.1 KiB
Java
/*
|
|
* 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 8358880 8367324
|
|
* @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"));
|
|
|
|
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());
|
|
}
|
|
}
|