|
1 | 1 | /* |
2 | | - * Copyright (c) 2007, 2024, Oracle and/or its affiliates. All rights reserved. |
| 2 | + * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. |
3 | 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
4 | 4 | * |
5 | 5 | * This code is free software; you can redistribute it and/or modify it |
|
26 | 26 | * @bug 4691089 4819436 4942982 5104960 6544471 6627549 7066203 7195759 |
27 | 27 | * 8039317 8074350 8074351 8145952 8187946 8193552 8202026 8204269 |
28 | 28 | * 8208746 8209775 8264792 8274658 8283277 8296239 8321480 8334653 |
| 29 | + * 8354344 |
29 | 30 | * @summary Validate ISO 4217 data for Currency class. |
30 | 31 | * @modules java.base/java.util:open |
31 | 32 | * jdk.localedata |
32 | | - * @run junit ValidateISO4217 |
| 33 | + * @library /test/lib |
| 34 | + * @run junit/othervm -DMOCKED.TIME=setup ValidateISO4217 |
| 35 | + * @run main/othervm --patch-module java.base=${test.class.path} |
| 36 | + * -DMOCKED.TIME=check -Djava.util.currency.data=${test.src}/currency.properties ValidateISO4217 |
| 37 | + * @run junit/othervm --patch-module java.base=${test.class.path} |
| 38 | + * -DMOCKED.TIME=true ValidateISO4217 |
| 39 | + */ |
| 40 | + |
| 41 | +/* The run invocation order is important. The first invocation will generate |
| 42 | + * class files for Currency that mock System.currentTimeMillis() as Long.MAX_VALUE, |
| 43 | + * which is required by the subsequent invocations. The second invocation ensures that |
| 44 | + * the module patch and mocked time are functioning correctly; it does not run any tests. |
| 45 | + * The third invocation using the modded class files via a module patch allow us |
| 46 | + * to test any cut-over dates after the transition. |
| 47 | + * Valid MOCKED.TIME values are "setup", "check", and "true". |
33 | 48 | */ |
34 | 49 |
|
35 | 50 | import java.io.BufferedReader; |
36 | 51 | import java.io.File; |
| 52 | +import java.io.FileOutputStream; |
37 | 53 | import java.io.FileReader; |
| 54 | +import java.io.IOException; |
| 55 | +import java.lang.classfile.ClassFile; |
| 56 | +import java.lang.classfile.ClassTransform; |
| 57 | +import java.lang.classfile.CodeTransform; |
| 58 | +import java.lang.classfile.MethodTransform; |
| 59 | +import java.lang.classfile.instruction.InvokeInstruction; |
| 60 | +import java.net.URI; |
| 61 | +import java.nio.file.Files; |
| 62 | +import java.nio.file.Paths; |
38 | 63 | import java.text.ParseException; |
39 | 64 | import java.text.SimpleDateFormat; |
40 | 65 | import java.util.ArrayList; |
| 66 | +import java.util.Arrays; |
41 | 67 | import java.util.Currency; |
42 | 68 | import java.util.HashSet; |
43 | 69 | import java.util.List; |
44 | 70 | import java.util.Locale; |
45 | 71 | import java.util.Set; |
46 | 72 | import java.util.StringTokenizer; |
47 | 73 | import java.util.TimeZone; |
| 74 | +import java.util.stream.Stream; |
48 | 75 |
|
49 | 76 | import org.junit.jupiter.api.BeforeAll; |
50 | 77 | import org.junit.jupiter.api.Test; |
@@ -113,10 +140,102 @@ public class ValidateISO4217 { |
113 | 140 | {"XK", "EUR", "978", "2"}, // Kosovo |
114 | 141 | }; |
115 | 142 | private static SimpleDateFormat format = null; |
| 143 | + private static final String MODULE_PATCH_LOCATION = |
| 144 | + System.getProperty("test.classes") + "/java/util/"; |
| 145 | + private static final String MOCKED_TIME = System.getProperty("MOCKED.TIME"); |
| 146 | + |
| 147 | + // Classes that should mock System.currentTimeMillis() |
| 148 | + private static final String[] CLASSES = |
| 149 | + Stream.concat( |
| 150 | + Stream.of("Currency.class"), |
| 151 | + Arrays.stream(Currency.class.getDeclaredClasses()) |
| 152 | + .map(c -> "Currency$" + c.getSimpleName() + ".class") |
| 153 | + ).toArray(String[]::new); |
| 154 | + |
| 155 | + // "check" invocation only runs the main method (and not any tests) to determine if the |
| 156 | + // future time checking is correct |
| 157 | + public static void main(String[] args) { |
| 158 | + if (MOCKED_TIME.equals("check")) { |
| 159 | + // Check that the module patch class files exist |
| 160 | + checkModulePatchExists(); |
| 161 | + // Check time is mocked |
| 162 | + // Override for PK=PKR in test/currency.properties is PKZ - simple |
| 163 | + // Override for PW=USD in test/currency.properties is MWP - special |
| 164 | + assertEquals("PKZ", Currency.getInstance(Locale.of("", "PK")).getCurrencyCode(), |
| 165 | + "Mocked time / module patch not working"); |
| 166 | + assertEquals("MWP", Currency.getInstance(Locale.of("", "PW")).getCurrencyCode(), |
| 167 | + "Mocked time / module patch not working"); |
| 168 | + // Properly working. Do nothing and move to third invocation |
| 169 | + } else { |
| 170 | + throw new RuntimeException( |
| 171 | + "Incorrect usage of ValidateISO4217. Main method invoked without proper system property value"); |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + @BeforeAll |
| 176 | + static void setUp() throws Exception { |
| 177 | + checkUsage(); |
| 178 | + setUpPatchedClasses(); |
| 179 | + setUpTestingData(); |
| 180 | + } |
| 181 | + |
| 182 | + // Enforce correct usage of ValidateISO4217 |
| 183 | + static void checkUsage() { |
| 184 | + if (MOCKED_TIME == null |
| 185 | + || (!MOCKED_TIME.equals("setup") && !MOCKED_TIME.equals("true"))) { |
| 186 | + throw new RuntimeException( |
| 187 | + "Incorrect usage of ValidateISO4217. Missing \"MOCKED.TIME\" system property"); |
| 188 | + } |
| 189 | + if (MOCKED_TIME.equals("true")) { |
| 190 | + checkModulePatchExists(); |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + static void checkModulePatchExists() { |
| 195 | + // Check that the module patch class files exist |
| 196 | + for (String className : CLASSES) { |
| 197 | + var file = new File(MODULE_PATCH_LOCATION + className); |
| 198 | + assertTrue(file.isFile(), "Module patch class files missing"); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + // Patch the relevant classes required for module patch |
| 203 | + static void setUpPatchedClasses() throws IOException { |
| 204 | + if (MOCKED_TIME.equals("setup")) { |
| 205 | + new File(MODULE_PATCH_LOCATION).mkdirs(); |
| 206 | + for (String s : CLASSES) { |
| 207 | + patchClass(s); |
| 208 | + } |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + // Mock calls of System.currentTimeMillis() within Currency to Long.MAX_VALUE. |
| 213 | + // This effectively ensures that we are always past any cut-over dates. |
| 214 | + private static void patchClass(String name) throws IOException { |
| 215 | + CodeTransform codeTransform = (codeBuilder, e) -> { |
| 216 | + switch (e) { |
| 217 | + case InvokeInstruction i when |
| 218 | + i.owner().asInternalName().equals("java/lang/System") |
| 219 | + && i.name().equalsString("currentTimeMillis") -> |
| 220 | + codeBuilder.loadConstant(Long.MAX_VALUE); // mock |
| 221 | + default -> codeBuilder.accept(e); |
| 222 | + } |
| 223 | + }; |
| 224 | + MethodTransform methodTransform = MethodTransform.transformingCode(codeTransform); |
| 225 | + ClassTransform classTransform = ClassTransform.transformingMethods(methodTransform); |
| 226 | + ClassFile cf = ClassFile.of(); |
| 227 | + byte[] newBytes = cf.transformClass(cf.parse( |
| 228 | + Files.readAllBytes(Paths.get(URI.create("jrt:/java.base/java/util/" + name)))), classTransform); |
| 229 | + |
| 230 | + String patchedClass = MODULE_PATCH_LOCATION + name; |
| 231 | + var file = new File(patchedClass); |
| 232 | + try (FileOutputStream fos = new FileOutputStream(file)) { |
| 233 | + fos.write(newBytes); |
| 234 | + } |
| 235 | + } |
116 | 236 |
|
117 | 237 | // Sets up the following test data: |
118 | 238 | // ISO4217Codes, additionalCodes, testCurrencies, codes |
119 | | - @BeforeAll |
120 | 239 | static void setUpTestingData() throws Exception { |
121 | 240 | // These functions laterally setup 'testCurrencies' and 'codes' |
122 | 241 | // at the same time |
@@ -169,8 +288,8 @@ private static void processColumns(StringTokenizer tokens, String country) throw |
169 | 288 | if (format == null) { |
170 | 289 | createDateFormat(); |
171 | 290 | } |
172 | | - // If the cut-over already passed, use the new curency for ISO4217Codes |
173 | | - if (format.parse(tokens.nextToken()).getTime() < System.currentTimeMillis()) { |
| 291 | + // If the cut-over already passed, use the new currency for ISO4217Codes |
| 292 | + if (format.parse(tokens.nextToken()).getTime() < currentTimeMillis()) { |
174 | 293 | currency = tokens.nextToken(); |
175 | 294 | numeric = tokens.nextToken(); |
176 | 295 | minorUnit = tokens.nextToken(); |
@@ -320,4 +439,10 @@ private static String getSetDiffs(Set<Currency> jreCurrencies, Set<Currency> tes |
320 | 439 | bldr.append("\n"); |
321 | 440 | return bldr.toString(); |
322 | 441 | } |
| 442 | + |
| 443 | + // Either the current system time, or a mocked value equal to Long.MAX_VALUE |
| 444 | + static long currentTimeMillis() { |
| 445 | + var mocked = MOCKED_TIME.equals("true"); |
| 446 | + return mocked ? Long.MAX_VALUE : System.currentTimeMillis(); |
| 447 | + } |
323 | 448 | } |
0 commit comments