1 package org.unicode.cldr.unittest; 2 3 import java.io.File; 4 import java.io.IOException; 5 import java.io.PrintWriter; 6 import java.util.ArrayList; 7 import java.util.Collection; 8 import java.util.Collections; 9 import java.util.HashMap; 10 import java.util.HashSet; 11 import java.util.List; 12 import java.util.Set; 13 import java.util.TreeSet; 14 15 import org.unicode.cldr.draft.FileUtilities; 16 import org.unicode.cldr.test.ExampleGenerator; 17 import org.unicode.cldr.util.CLDRConfig; 18 import org.unicode.cldr.util.CLDRFile; 19 import org.unicode.cldr.util.CLDRPaths; 20 import org.unicode.cldr.util.Factory; 21 import org.unicode.cldr.util.LocaleIDParser; 22 import org.unicode.cldr.util.PathStarrer; 23 import org.unicode.cldr.util.RecordingCLDRFile; 24 import org.unicode.cldr.util.XMLSource; 25 26 import com.google.common.collect.Multimap; 27 import com.google.common.collect.TreeMultimap; 28 import com.ibm.icu.dev.test.TestFmwk; 29 30 public class TestExampleDependencies extends TestFmwk { 31 32 private final boolean USE_STARRED_PATHS = true; 33 private final boolean USE_RECORDING = true; 34 private final String fileExtension = USE_RECORDING ? ".java" : ".json"; 35 36 private CLDRConfig info; 37 private CLDRFile englishFile; 38 private Factory factory; 39 private Set<String> locales; 40 private String outputDir; 41 private PathStarrer pathStarrer; 42 main(String[] args)43 public static void main(String[] args) { 44 new TestExampleDependencies().run(args); 45 } 46 47 /** 48 * Test dependencies where changing the value of one path changes example-generation for another path. 49 * 50 * The goal is to optimize example caching by only regenerating examples when necessary. 51 * 52 * Reference: https://unicode-org.atlassian.net/browse/CLDR-13636 53 * 54 * @throws IOException 55 */ TestExampleGeneratorDependencies()56 public void TestExampleGeneratorDependencies() throws IOException { 57 info = CLDRConfig.getInstance(); 58 englishFile = info.getEnglish(); 59 factory = info.getCldrFactory(); 60 locales = factory.getAvailable(); 61 outputDir = CLDRPaths.GEN_DIRECTORY + "test" + File.separator; 62 pathStarrer = USE_STARRED_PATHS ? new PathStarrer().setSubstitutionPattern("*") : null; 63 64 System.out.println("..."); 65 System.out.println("Looping through " + locales.size() + " locales ..."); 66 67 if (USE_RECORDING) { 68 /* 69 * Fast method: use RecordingCLDRFile to learn which paths are checked 70 * to produce the example for a given path. This is fast enough that we 71 * can do it for all locales at once to produce a single file. 72 */ 73 useRecording(); 74 } else { 75 /* 76 * Slow method: loop through all paths, modifying the value for each path 77 * and then doing an inner loop through all paths to see whether the example 78 * changed for each other path. This is extremely slow, so we produce one file 79 * for each locale, with the intention of merging the files afterwards. 80 */ 81 useModifying(); 82 } 83 } 84 useRecording()85 private void useRecording() throws IOException { 86 final Multimap<String, String> dependencies = TreeMultimap.create(); 87 for (String localeId : locales) { 88 System.out.println(localeId); 89 addDependenciesForLocale(dependencies, localeId); 90 } 91 String fileName = "ExampleDependencies" + fileExtension; 92 System.out.println("Creating " + outputDir + fileName + " ..."); 93 writeDependenciesToFile(dependencies, outputDir, fileName); 94 } 95 addDependenciesForLocale(Multimap<String, String> dependencies, String localeId)96 private void addDependenciesForLocale(Multimap<String, String> dependencies, String localeId) { 97 RecordingCLDRFile cldrFile = makeRecordingCldrFile(localeId); 98 cldrFile.disableCaching(); 99 100 Set<String> paths = new TreeSet<>(cldrFile.getComparator()); 101 // time-consuming 102 cldrFile.forEach(paths::add); 103 104 ExampleGenerator egTest = new ExampleGenerator(cldrFile, englishFile, CLDRPaths.DEFAULT_SUPPLEMENTAL_DIRECTORY); 105 egTest.setCachingEnabled(false); // will not employ a cache -- this should save some time, since cache would be wasted 106 107 for (String pathB : paths) { 108 if (skipPathForDependencies(pathB)) { 109 continue; 110 } 111 String valueB = cldrFile.getStringValue(pathB); 112 if (valueB == null) { 113 continue; 114 } 115 String starredB = USE_STARRED_PATHS ? pathStarrer.set(pathB) : null; 116 cldrFile.clearRecordedPaths(); 117 egTest.getExampleHtml(pathB, valueB); 118 HashSet<String> pathsA = cldrFile.getRecordedPaths(); 119 for (String pathA: pathsA) { 120 if (pathA.equals(pathB) || skipPathForDependencies(pathA)) { 121 continue; 122 } 123 String starredA = USE_STARRED_PATHS ? pathStarrer.set(pathA) : null; 124 dependencies.put(USE_STARRED_PATHS ? starredA : pathA, 125 USE_STARRED_PATHS ? starredB : pathB); 126 } 127 } 128 } 129 makeRecordingCldrFile(String localeId)130 private RecordingCLDRFile makeRecordingCldrFile(String localeId) { 131 XMLSource topSource = factory.makeSource(localeId); 132 List<XMLSource> parents = getParentSources(factory, localeId); 133 XMLSource[] a = new XMLSource[parents.size()]; 134 return new RecordingCLDRFile(topSource, parents.toArray(a)); 135 } 136 useModifying()137 private void useModifying() throws IOException { 138 for (String localeId : locales) { 139 String fileName = "example_dependencies_A_" 140 + localeId 141 + (USE_STARRED_PATHS ? "_star" : "") 142 + fileExtension; 143 144 if (new File(outputDir, fileName).exists()) { 145 System.out.println("Locale: " + localeId + " -- skipping since " + 146 outputDir + fileName + " already exists"); 147 } else { 148 System.out.println("Locale: " + localeId + " -- creating " 149 + outputDir + fileName + " ..."); 150 writeOneLocale(localeId, outputDir, fileName); 151 } 152 } 153 } 154 writeOneLocale(String localeId, String outputDir, String fileName)155 private void writeOneLocale(String localeId, String outputDir, String fileName) throws IOException { 156 CLDRFile cldrFile = makeMutableResolved(factory, localeId); // time-consuming 157 cldrFile.disableCaching(); 158 159 Set<String> paths = new TreeSet<>(cldrFile.getComparator()); 160 // time-consuming 161 cldrFile.forEach(paths::add); 162 163 ExampleGenerator egBase = new ExampleGenerator(cldrFile, englishFile, CLDRPaths.DEFAULT_SUPPLEMENTAL_DIRECTORY); 164 165 HashMap<String, String> originalValues = new HashMap<>(); 166 167 getExamplesForBase(egBase, cldrFile, paths, originalValues); 168 /* 169 * Make egBase "cacheOnly" so that getExampleHtml will throw an exception if future queries 170 * are not found in the cache. Alternatively, could just make a local hashmap originalExamples, 171 * similar to originalValues. That might be more robust, require more memory, faster or slower? 172 * Should try both ways. 173 */ 174 egBase.setCacheOnly(true); 175 176 ExampleGenerator egTest = new ExampleGenerator(cldrFile, englishFile, CLDRPaths.DEFAULT_SUPPLEMENTAL_DIRECTORY); 177 egTest.setCachingEnabled(false); // will not employ a cache -- this should save some time, since cache would be wasted 178 179 CLDRFile top = cldrFile.getUnresolved(); // can mutate top 180 181 final Multimap<String, String> dependencies = TreeMultimap.create(); 182 long count = 0; 183 long skipCount = 0; 184 long dependencyCount = 0; 185 186 /* 187 * For each path (A), temporarily change its value, and then check each other path (B), 188 * to see whether changing the value for A changed the example for B. 189 */ 190 for (String pathA : paths) { 191 if (skipPathForDependencies(pathA)) { 192 ++skipCount; 193 continue; 194 } 195 String valueA = cldrFile.getStringValue(pathA); 196 if (valueA == null) { 197 continue; 198 } 199 if ((++count % 100) == 0) { 200 System.out.println(count); 201 } 202 if (count > 500000) { 203 break; 204 } 205 String starredA = USE_STARRED_PATHS ? pathStarrer.set(pathA) : null; 206 /* 207 * Modify the value for pathA in some random way 208 */ 209 String newValue = modifyValueRandomly(valueA); 210 /* 211 * cldrFile.add would lead to UnsupportedOperationException("Resolved CLDRFiles are read-only"); 212 * Instead do top.add(), which works since top.dataSource = cldrFile.dataSource.currentSource. 213 * First, need to do valueChanged to clear getSourceLocaleIDCache. 214 */ 215 cldrFile.valueChanged(pathA); 216 top.add(pathA, newValue); 217 218 /* 219 * Reality check, did we really change the value returned by cldrFile.getStringValue? 220 */ 221 String valueAX = cldrFile.getStringValue(pathA); 222 if (!valueAX.equals(newValue)) { 223 // Bad, didn't work as expected 224 System.out.println("Changing top did not change cldrFile: newValue = " + newValue 225 + "; valueAX = " + valueAX + "; valueA = " + valueA); 226 } 227 228 for (String pathB : paths) { 229 if (pathA.equals(pathB) || skipPathForDependencies(pathB)) { 230 continue; 231 } 232 /* 233 * For valueB, use originalValues.get(pathB), not cldrFile.getStringValue(pathB). 234 * They could be different if changing valueA changes valueB (probably due to aliasing). 235 * In that case, we're not interested in whether changing valueA changes valueB. We need 236 * to know whether changing valueA changes an example that was already cached, keyed by 237 * pathB and the original valueB. 238 */ 239 String valueB = originalValues.get(pathB); 240 if (valueB == null) { 241 continue; 242 } 243 pathB = pathB.intern(); 244 245 // egTest.icuServiceBuilder.setCldrFile(cldrFile); // clear caches in icuServiceBuilder; has to be public 246 String exBase = egBase.getExampleHtml(pathB, valueB); // this will come from cache (or throw cacheOnly exception) 247 String exTest = egTest.getExampleHtml(pathB, valueB); // this won't come from cache 248 if ((exTest == null) != (exBase == null)) { 249 throw new InternalError("One null but not both? " + pathA + " --- " + pathB); 250 } else if (exTest != null && !exTest.equals(exBase)) { 251 dependencies.put(USE_STARRED_PATHS ? starredA : pathA, USE_STARRED_PATHS ? pathStarrer.set(pathB).intern() : pathB); 252 ++dependencyCount; 253 } 254 } 255 /* 256 * Restore the original value, so that the changes due to this pathA don't get 257 * carried over to the next pathA. Again call valueChanged to clear getSourceLocaleIDCache. 258 */ 259 top.add(pathA, valueA); 260 cldrFile.valueChanged(pathA); 261 String valueAXX = cldrFile.getStringValue(pathA); 262 if (!valueAXX.equals(valueA)) { 263 System.out.println("Failed to restore original value: valueAXX = " + valueAXX 264 + "; valueA = " + valueA); 265 } 266 } 267 writeDependenciesToFile(dependencies, outputDir, fileName); 268 System.out.println("count = " + count + "; skipCount = " + skipCount + "; dependencyCount = " + dependencyCount); 269 } 270 271 /** 272 * Get all the examples so they'll be added to the cache for egBase. 273 * Also fill originalValues. 274 * 275 * @param egBase 276 * @param cldrFile 277 * @param paths 278 * @param originalValues 279 */ getExamplesForBase(ExampleGenerator egBase, CLDRFile cldrFile, Set<String> paths, HashMap<String, String> originalValues)280 private void getExamplesForBase(ExampleGenerator egBase, CLDRFile cldrFile, Set<String> paths, HashMap<String, String> originalValues) { 281 for (String path : paths) { 282 if (skipPathForDependencies(path)) { 283 continue; 284 } 285 String value = cldrFile.getStringValue(path); 286 if (value == null) { 287 continue; 288 } 289 originalValues.put(path, value); 290 egBase.getExampleHtml(path, value); 291 } 292 } 293 294 /** 295 * Modify the given value string for testing dependencies 296 * 297 * @param value 298 * @return the modified value, guaranteed to be different from value 299 * 300 * Note: it might be best to avoid IllegalArgumentException thrown/caught in, e.g., ICUServiceBuilder.getSymbolString; 301 * in which case this function might need path as parameter, to generate only "legal" values for specific paths. 302 */ modifyValueRandomly(String value)303 private String modifyValueRandomly(String value) { 304 /* 305 * Change 1 to 0 306 */ 307 String newValue = value.replace("1", "0"); 308 if (!newValue.equals(value)) { 309 return newValue; 310 } 311 /* 312 * Change 0 to 1 313 */ 314 newValue = value.replace("0", "1"); 315 if (!newValue.equals(value)) { 316 return newValue; 317 } 318 /* 319 * String concatenation, e.g., change "foo" to "foo1" 320 */ 321 return value + "1"; 322 } 323 324 /** 325 * Get a CLDRFile that is mutable yet shares the same dataSource as a pre-existing 326 * resolving CLDRFile for the same locale. 327 * 328 * If cldrFile is the pre-existing resolving CLDRFile, and we return topCldrFile, then 329 * we'll end up with topCldrFile.dataSource = cldrFile.dataSource.currentSource, which 330 * will be a SimpleXMLSource. 331 * 332 * @param factory 333 * @param localeId 334 * @return the CLDRFile 335 */ makeMutableResolved(Factory factory, String localeId)336 private static CLDRFile makeMutableResolved(Factory factory, String localeId) { 337 XMLSource topSource = factory.makeSource(localeId).cloneAsThawed(); // make top one modifiable 338 List<XMLSource> parents = getParentSources(factory, localeId); 339 XMLSource[] a = new XMLSource[parents.size()]; 340 return new CLDRFile(topSource, parents.toArray(a)); 341 } 342 343 /** 344 * Get the parent sources for the given localeId 345 * 346 * @param factory 347 * @param localeId 348 * @return the List of XMLSource objects 349 * 350 * Called only by makeMutableResolved 351 */ getParentSources(Factory factory, String localeId)352 private static List<XMLSource> getParentSources(Factory factory, String localeId) { 353 List<XMLSource> parents = new ArrayList<>(); 354 for (String currentLocaleID = LocaleIDParser.getParent(localeId); 355 currentLocaleID != null; 356 currentLocaleID = LocaleIDParser.getParent(currentLocaleID)) { 357 parents.add(factory.makeSource(currentLocaleID)); 358 } 359 return parents; 360 } 361 362 /** 363 * Should the given path be skipped when testing example-generator path dependencies? 364 * 365 * @param path 366 * @param isTypeA true if path is playing role of pathA not pathB 367 * @return true to skip, else false 368 */ skipPathForDependencies(String path)369 private static boolean skipPathForDependencies(String path) { 370 if (path.endsWith("/alias") || path.startsWith("//ldml/identity")) { 371 return true; 372 } 373 return false; 374 } 375 376 /** 377 * Write the given map of example-generator path dependencies to a json or java file. 378 * 379 * If this function is to be used for json and revised long-term, it would be better to use JSONObject, 380 * or write a format other than json. 381 * JSONObject isn't currently linked to cldr-unittest TestAll, package org.unicode.cldr.unittest. 382 * 383 * @param dependencies the multimap of example-generator path dependencies 384 * @param dir the directory in which to create the file 385 * @param fileName the name of the file to create 386 * 387 * @throws IOException 388 */ writeDependenciesToFile(Multimap<String, String> dependencies, String dir, String name)389 private void writeDependenciesToFile(Multimap<String, String> dependencies, String dir, String name) throws IOException { 390 PrintWriter writer = FileUtilities.openUTF8Writer(dir, name); 391 if (fileExtension.equals(".json")) { 392 writeJson(dependencies, dir, name, writer); 393 } else { 394 writeJava(dependencies, dir, name, writer); 395 } 396 } 397 writeJava(Multimap<String, String> dependencies, String dir, String name, PrintWriter writer)398 private void writeJava(Multimap<String, String> dependencies, String dir, String name, PrintWriter writer) { 399 writer.println("package org.unicode.cldr.test;"); 400 writer.println("import com.google.common.collect.ImmutableSetMultimap;"); 401 writer.println("public class ExampleDependencies {"); 402 writer.println(" public static ImmutableSetMultimap<String, String> dependencies"); 403 writer.println(" = new ImmutableSetMultimap.Builder<String, String>()"); 404 int dependenciesWritten = 0; 405 ArrayList<String> listA = new ArrayList<>(dependencies.keySet()); 406 Collections.sort(listA); 407 for (String pathA : listA) { 408 ArrayList<String> listB = new ArrayList<>(dependencies.get(pathA)); 409 Collections.sort(listB); 410 String a = "\"" + pathA.replaceAll("\"", "\\\\\"") + "\""; 411 for (String pathB : listB) { 412 String b = "\"" + pathB.replaceAll("\"", "\\\\\"") + "\""; 413 writer.println(" .put(" + a + ", " + b + ")"); 414 ++dependenciesWritten; 415 } 416 } 417 writer.println(" .build();"); 418 writer.println("}"); 419 writer.close(); 420 System.out.println("Wrote " + dependenciesWritten + " dependencies to " + dir + name); 421 } 422 writeJson(Multimap<String, String> dependencies, String dir, String name, PrintWriter writer)423 private void writeJson(Multimap<String, String> dependencies, String dir, String name, PrintWriter writer) { 424 ArrayList<String> list = new ArrayList<>(dependencies.keySet()); 425 Collections.sort(list); 426 boolean firstPathA = true; 427 int keysWritten = 0; 428 for (String pathA : list) { 429 if (firstPathA) { 430 firstPathA = false; 431 } else { 432 writer.println(","); 433 } 434 Collection<String> values = dependencies.get(pathA); 435 writer.print(" " + "\"" + pathA.replaceAll("\"", "\\\\\"") + "\"" + ": "); 436 writer.println("["); 437 boolean firstPathB = true; 438 for (String pathB : values) { 439 if (firstPathB) { 440 firstPathB = false; 441 } else { 442 writer.println(","); 443 } 444 writer.print(" " + "\"" + pathB.replaceAll("\"", "\\\\\"") + "\""); 445 } 446 writer.println(""); 447 writer.print(" ]"); 448 ++keysWritten; 449 } 450 writer.println(""); 451 writer.println("}"); 452 writer.close(); 453 System.out.println("Wrote " + keysWritten + " keys to " + dir + name); 454 } 455 } 456