• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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