undefect. CWE-407 — 63 sites patched across 27 ecosystems
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.
This commit is contained in:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
482
test/jdk/java/util/Currency/ValidateISO4217.java
Normal file
482
test/jdk/java/util/Currency/ValidateISO4217.java
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue