/
githubmirror
/
panama-vector
Обзор
Документация
Войти
/
githubmirror
/
panama-vector
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/jdk/java/util/Currency/ValidateISO4217.java
482 строки
21 KB
Justin Lu
8368308: ISO 4217 Amendment 180 Update
24 сен 2025, 19:06
24 сен 2025, 19:06
85f5bf3
Код
Авторство
О чём код?
/* * Copyright (c) 2007, 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 4691089 4819436 4942982 5104960 6544471 6627549 7066203 7195759 * 8039317 8074350 8074351 8145952 8187946 8193552 8202026 8204269 * 8208746 8209775 8264792 8274658 8283277 8296239 8321480 8334653 * 8354343 8354344 8356096 8368308 * @summary Validate ISO 4217 data for Currency class. * @modules java.base/java.util:open * jdk.localedata * @library /test/lib * @run junit/othervm -DMOCKED.TIME=setup ValidateISO4217 * @run main/othervm --patch-module java.base=${test.class.path} * -DMOCKED.TIME=check -Djava.util.currency.data=${test.src}/currency.properties ValidateISO4217 * @run junit/othervm --patch-module java.base=${test.class.path} * -DMOCKED.TIME=true ValidateISO4217 */ /* The run invocation order is important. The first invocation will generate * class files for Currency that mock System.currentTimeMillis() as Long.MAX_VALUE, * which is required by the subsequent invocations. The second invocation ensures that * the module patch and mocked time are functioning correctly; it does not run any tests. * The third invocation using the modded class files via a module patch allow us * to test any cut-over dates after the transition. * Valid MOCKED.TIME values are "setup", "check", and "true". */ import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.lang.classfile.ClassFile; import java.lang.classfile.ClassTransform; import java.lang.classfile.CodeTransform; import java.lang.classfile.MethodTransform; import java.lang.classfile.instruction.InvokeInstruction; import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Currency; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.StringTokenizer; import java.util.TimeZone; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** * This class tests the latest ISO 4217 data and Java's currency data which is * based on ISO 4217. The golden-data file, 'ISO4217-list-one.txt', based on the * "List one: Currency, fund and precious metal codes" has the following * format: <Country code>\t<Currency code>\t<Numeric code>\t<Minor unit>[\t<Cutover Date>\t<new Currency code>\t<new Numeric code>\t<new Minor unit>] * The Cutover Date is given in SimpleDateFormat's 'yyyy-MM-dd-HH-mm-ss' format in the GMT time zone. */ public class ValidateISO4217 { // Input golden-data file private static final File dataFile = new File(System.getProperty( "test.src", "."), "ISO4217-list-one.txt"); // Code statuses private static final byte UNDEFINED = 0; private static final byte DEFINED = 1; private static final byte SKIPPED = 2; private static final byte TESTED = 4; private static final int ALPHA_NUM = 26; // An alpha2 code table which maps the status of a country private static final byte[] codes = new byte[ALPHA_NUM * ALPHA_NUM]; // Codes derived from ISO4217 golden-data file private static final List<Arguments> ISO4217Codes = new ArrayList<Arguments>(); // Additional codes not from the ISO4217 golden-data file private static final List<Arguments> additionalCodes = new ArrayList<Arguments>(); // Currencies to test (derived from ISO4217Codes and additionalCodes) private static final Set<Currency> testCurrencies = new HashSet<>(); // Special case currencies that should only exist after the cut-over occurs private static final Set<String> currenciesNotYetDefined = new HashSet<>(); // Codes that are obsolete, do not have related country, extra currency private static final String otherCodes = "ADP-AFA-ATS-AYM-AZM-BEF-BGL-BGN-BOV-BYB-BYR-CHE-CHW-CLF-COU-CUC-CYP-" + "DEM-EEK-ESP-FIM-FRF-GHC-GRD-GWP-HRK-IEP-ITL-LTL-LUF-LVL-MGF-MRO-MTL-MXV-MZM-NLG-" + "PTE-ROL-RUR-SDD-SIT-SLL-SKK-SRG-STD-TMM-TPE-TRL-VEF-UYI-USN-USS-VEB-VED-" + "XAD-XAG-XAU-XBA-XBB-XBC-XBD-XDR-XFO-XFU-XPD-XPT-XSU-XTS-XUA-XXX-" + "YUM-ZMK-ZWD-ZWL-ZWN-ZWR"; private static final String[][] extraCodes = { /* Defined in ISO 4217 list, but don't have code and minor unit info. */ {"AQ", "", "", "0"}, // Antarctica /* * Defined in ISO 4217 list, but don't have code and minor unit info in * it. On the other hand, both code and minor unit are defined in * .properties file. I don't know why, though. */ {"GS", "GBP", "826", "2"}, // South Georgia And The South Sandwich Islands /* Not defined in ISO 4217 list, but defined in .properties file. */ {"AX", "EUR", "978", "2"}, // ÅLAND ISLANDS {"PS", "ILS", "376", "2"}, // Palestinian Territory, Occupied /* Not defined in ISO 4217 list, but added in ISO 3166 country code list */ {"JE", "GBP", "826", "2"}, // Jersey {"GG", "GBP", "826", "2"}, // Guernsey {"IM", "GBP", "826", "2"}, // Isle of Man {"BL", "EUR", "978", "2"}, // Saint Barthelemy {"MF", "EUR", "978", "2"}, // Saint Martin /* Defined neither in ISO 4217 nor ISO 3166 list */ {"XK", "EUR", "978", "2"}, // Kosovo }; private static SimpleDateFormat format = null; private static final String MODULE_PATCH_LOCATION = System.getProperty("test.classes") + "/java/util/"; private static final String MOCKED_TIME = System.getProperty("MOCKED.TIME"); // Classes that should mock System.currentTimeMillis() private static final String[] CLASSES = Stream.concat( Stream.of("Currency.class"), Arrays.stream(Currency.class.getDeclaredClasses()) .map(c -> "Currency$" + c.getSimpleName() + ".class") ).toArray(String[]::new); // "check" invocation only runs the main method (and not any tests) to determine if the // future time checking is correct public static void main(String[] args) { if (MOCKED_TIME.equals("check")) { // Check that the module patch class files exist checkModulePatchExists(); // Check time is mocked // Override for PK=PKR in test/currency.properties is PKZ - simple // Override for PW=USD in test/currency.properties is MWP - special assertEquals("PKZ", Currency.getInstance(Locale.of("", "PK")).getCurrencyCode(), "Mocked time / module patch not working"); assertEquals("MWP", Currency.getInstance(Locale.of("", "PW")).getCurrencyCode(), "Mocked time / module patch not working"); // Properly working. Do nothing and move to third invocation } else { throw new RuntimeException( "Incorrect usage of ValidateISO4217. Main method invoked without proper system property value"); } } @BeforeAll static void setUp() throws Exception { checkUsage(); setUpPatchedClasses(); setUpTestingData(); } // Enforce correct usage of ValidateISO4217 static void checkUsage() { if (MOCKED_TIME == null || (!MOCKED_TIME.equals("setup") && !MOCKED_TIME.equals("true"))) { throw new RuntimeException( "Incorrect usage of ValidateISO4217. Missing \"MOCKED.TIME\" system property"); } if (MOCKED_TIME.equals("true")) { checkModulePatchExists(); } } static void checkModulePatchExists() { // Check that the module patch class files exist for (String className : CLASSES) { var file = new File(MODULE_PATCH_LOCATION + className); assertTrue(file.isFile(), "Module patch class files missing"); } } // Patch the relevant classes required for module patch static void setUpPatchedClasses() throws IOException { if (MOCKED_TIME.equals("setup")) { new File(MODULE_PATCH_LOCATION).mkdirs(); for (String s : CLASSES) { patchClass(s); } } } // Mock calls of System.currentTimeMillis() within Currency to Long.MAX_VALUE. // This effectively ensures that we are always past any cut-over dates. private static void patchClass(String name) throws IOException { CodeTransform codeTransform = (codeBuilder, e) -> { switch (e) { case InvokeInstruction i when i.owner().asInternalName().equals("java/lang/System") && i.name().equalsString("currentTimeMillis") -> codeBuilder.loadConstant(Long.MAX_VALUE); // mock default -> codeBuilder.accept(e); } }; MethodTransform methodTransform = MethodTransform.transformingCode(codeTransform); ClassTransform classTransform = ClassTransform.transformingMethods(methodTransform); ClassFile cf = ClassFile.of(); byte[] newBytes = cf.transformClass(cf.parse( Files.readAllBytes(Paths.get(URI.create("jrt:/java.base/java/util/" + name)))), classTransform); String patchedClass = MODULE_PATCH_LOCATION + name; var file = new File(patchedClass); try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(newBytes); } } // Sets up the following test data: // ISO4217Codes, additionalCodes, testCurrencies, codes, currenciesNotYetDefined static void setUpTestingData() throws Exception { // These functions laterally setup 'testCurrencies' and 'codes' // at the same time setUpISO4217Codes(); setUpAdditionalCodes(); setUpOtherCurrencies(); setUpNotYetDefined(); } // Parse the ISO4217 file and populate ISO4217Codes and testCurrencies. private static void setUpISO4217Codes() throws Exception{ try (FileReader fr = new FileReader(dataFile); BufferedReader in = new BufferedReader(fr)) { String line; while ((line = in.readLine()) != null) { if (line.length() == 0 || line.charAt(0) == '#') { // Skip comments and empty lines continue; } StringTokenizer tokens = new StringTokenizer(line, "\t"); String country = tokens.nextToken(); if (country.length() != 2) { // Skip invalid countries continue; } // If the country is valid, process the additional columns processColumns(tokens, country); } } } private static void processColumns(StringTokenizer tokens, String country) throws ParseException { String currency; String numeric; String minorUnit; int tokensCount = tokens.countTokens(); if (tokensCount < 3) { // Ill-defined columns currency = ""; numeric = "0"; minorUnit = "0"; } else { // Fully defined columns currency = tokens.nextToken(); numeric = tokens.nextToken(); minorUnit = tokens.nextToken(); testCurrencies.add(Currency.getInstance(currency)); // Check for the cut-over if a currency is changing if (tokensCount > 3) { if (format == null) { createDateFormat(); } // If the cut-over already passed, use the new currency for ISO4217Codes if (format.parse(tokens.nextToken()).getTime() < currentTimeMillis()) { currency = tokens.nextToken(); numeric = tokens.nextToken(); minorUnit = tokens.nextToken(); testCurrencies.add(Currency.getInstance(currency)); } else { // Add all future currencies to the set. // We process it later once 'testCurrencies' is complete // to only include ones that should not be defined yet. currenciesNotYetDefined.add(tokens.nextToken()); } } } int index = toIndex(country); ISO4217Codes.add(Arguments.of(country, currency, Integer.parseInt(numeric), Integer.parseInt(minorUnit), index)); codes[index] = DEFINED; } // Generates a unique index for an alpha-2 country private static int toIndex(String country) { return ((country.charAt(0) - 'A') * ALPHA_NUM + country.charAt(1) - 'A'); } private static void createDateFormat() { format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); format.setLenient(false); } // Process 'extraCodes', turning them into JUnit arguments and populate // both additionalCodes and testCurrencies. private static void setUpAdditionalCodes() { for (String[] extraCode : extraCodes) { int index = toIndex(extraCode[0]); if (extraCode[1].length() != 0) { additionalCodes.add(Arguments.of(extraCode[0], extraCode[1], Integer.parseInt(extraCode[2]), Integer.parseInt(extraCode[3]), index)); testCurrencies.add(Currency.getInstance(extraCode[1])); } else { codes[index] = SKIPPED; // For example, Antarctica } } } // The previous set-up method populated most of testCurrencies. This // method finishes populating the list with 'otherCodes'. private static void setUpOtherCurrencies() { // Add otherCodes StringTokenizer st = new StringTokenizer(otherCodes, "-"); while (st.hasMoreTokens()) { testCurrencies.add(Currency.getInstance(st.nextToken())); } } // Future currencies that are already defined as ISO 4217 codes should be // removed. For example, in CW=ANG;2025-04-01-04-00-00;EUR "EUR" would be // removed as it is an already valid ISO 4217 code private static void setUpNotYetDefined() { var allFutureCurrencies = testCurrencies .stream() .map(Currency::getCurrencyCode) .collect(Collectors.toSet()); currenciesNotYetDefined.removeIf(allFutureCurrencies::contains); } // Check that the data file is up-to-date @Test public void dataVersionTest() { CheckDataVersion.check(); } /** * Tests the JDK's ISO4217 data and ensures the values for getNumericCode(), * getDefaultFractionDigits(), and getCurrencyCode() are as expected. */ @ParameterizedTest @MethodSource({"ISO4217CodesProvider", "additionalCodesProvider"}) public void countryCurrencyTest(String country, String currencyCode, int numericCode, int digits, int index) { currencyTest(currencyCode, numericCode, digits); countryTest(country, currencyCode); assertNotEquals(codes[index], TESTED, "Error: Re-testing a previously defined code, possible duplication"); codes[index] = TESTED; } // Test a Currency built from currencyCode private static void currencyTest(String currencyCode, int numericCode, int digits) { Currency currency = Currency.getInstance(currencyCode); assertEquals(currency.getNumericCode(), numericCode); assertEquals(currency.getDefaultFractionDigits(), digits); } // Test a Currency built from country private static void countryTest(String country, String currencyCode) { Locale loc = Locale.of("", country); Currency currency = Currency.getInstance(loc); assertEquals(currency.getCurrencyCode(), currencyCode); } private static List<Arguments> ISO4217CodesProvider() { return ISO4217Codes; } private static List<Arguments> additionalCodesProvider() { return additionalCodes; } /** * Tests trying to create a Currency from an invalid alpha-2 country either * throws an IllegalArgumentException or returns null. The test data * supplied is every possible combination of AA -> ZZ. */ @Test public void twoLetterCodesTest() { for (String country : codeCombos()) { if (codes[toIndex(country)] == UNDEFINED) { // if a code is undefined / 0, creating a Currency from it // should throw an IllegalArgumentException assertThrows(IllegalArgumentException.class, () -> Currency.getInstance(Locale.of("", country)), "Error: This should be an undefined code and throw IllegalArgumentException: " + country); } else if (codes[toIndex(country)] == SKIPPED) { // if a code is marked as skipped / 2, creating a Currency from it // should return null assertNull(Currency.getInstance(Locale.of("", country)), "Error: Currency.getInstance() for this locale should return null: " + country); } } } // This method generates code combos from AA to ZZ private static List<String> codeCombos() { List<String> codeCombos = new ArrayList<>(); for (int i = 0; i < ALPHA_NUM; i++) { for (int j = 0; j < ALPHA_NUM; j++) { char[] code = new char[2]; code[0] = (char) ('A' + i); code[1] = (char) ('A' + j); codeCombos.add(new String(code)); } } return codeCombos; } // Any future currencies that do not already exist before the cut-over // should not be instantiable. This scenario is when a country transfers // to a new code, that is not already a valid ISO 4217 code. For example, // what occurred in the 176 update situation. @Test public void nonDefinedFutureCurrenciesTest() { for (String curr : currenciesNotYetDefined) { assertThrows(IllegalArgumentException.class, () -> Currency.getInstance(curr), "The future cut-over currency: %s should not exist".formatted(curr)); } } // This method ensures that getAvailableCurrencies() returns // the expected amount of currencies. @Test public void getAvailableCurrenciesTest() { Set<Currency> jreCurrencies = Currency.getAvailableCurrencies(); // Ensure that testCurrencies has all the JRE currency codes assertTrue(testCurrencies.containsAll(jreCurrencies), getSetDiffs(jreCurrencies, testCurrencies)); // Implicitly checks that jreCurrencies does not contain any currencies // defined in currenciesNotYetDefined } private static String getSetDiffs(Set<Currency> jreCurrencies, Set<Currency> testCurrencies) { StringBuilder bldr = new StringBuilder(); bldr.append("Error: getAvailableCurrencies() returned unexpected currencies: "); jreCurrencies.removeAll(testCurrencies); for (Currency curr : jreCurrencies) { bldr.append(" " + curr); } bldr.append("\n"); return bldr.toString(); } // Either the current system time, or a mocked value equal to Long.MAX_VALUE static long currentTimeMillis() { var mocked = MOCKED_TIME.equals("true"); return mocked ? Long.MAX_VALUE : System.currentTimeMillis(); } }