1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package libcore; 18 19 import java.io.IOException; 20 import java.io.PrintStream; 21 import java.nio.file.Path; 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.Collections; 25 import java.util.Comparator; 26 import java.util.LinkedHashMap; 27 import java.util.List; 28 import java.util.Locale; 29 import java.util.Map; 30 import java.util.Objects; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 34 /** 35 * Helps compare openjdk_java_files contents against upstream file contents. 36 * 37 * Outputs a tab-separated table comparing each openjdk_java_files entry 38 * against OpenJDK upstreams. This can help verify updates to later upstreams 39 * or focus attention towards files that may have been missed in a previous 40 * update (http://b/36461944) or are otherwise surprising (http://b/36429512). 41 * 42 * - Identifies each file as identical to, different from or missing from 43 * each upstream; diffs are not produced. 44 * - Optionally, copies all openjdk_java_files from the default upstream 45 * (eg. OpenJDK8u121-b13) to a new directory, for easy directory comparison 46 * using e.g. kdiff3, which allows inspecting detailed diffs. 47 * - The ANDROID_BUILD_TOP environment variable must be set to point to the 48 * AOSP root directory (parent of libcore). 49 * 50 * To check out upstreams OpenJDK 7u40, 8u60, 8u121-b13, 8u222-b01 and 9+181, run: 51 * 52 * mkdir ~/openjdk 53 * cd ~/openjdk 54 * export OPENJDK_HOME=$PWD 55 * hg clone http://hg.openjdk.java.net/jdk7u/jdk7u40/ 7u40 56 * (cd !$ ; sh get_source.sh) 57 * hg clone http://hg.openjdk.java.net/jdk8u/jdk8u 8u121-b13 58 * (cd !$ ; hg update -r jdk8u121-b13 && sh get_source.sh && sh common/bin/hgforest.sh update -r jdk8u121-b13) 59 * hg clone http://hg.openjdk.java.net/jdk8u/jdk8u60/ 8u60 60 * (cd !$ ; sh get_source.sh) 61 * hg clone http://hg.openjdk.java.net/jdk8u/jdk8u 8u222-b01 62 * (cd !$ ; hg update -r jdk8u222-b01 && sh get_source.sh && sh common/bin/hgforest.sh update -r jdk8u222-b01) 63 * hg clone http://hg.openjdk.java.net/jdk9/jdk9/ 9+181 64 * (cd !$ ; hg update -r jdk-9+181 && sh get_source.sh && sh common/bin/hgforest.sh update -r jdk-9+181) 65 * 66 * To get the 9b113+ upstream, follow the instructions from the commit 67 * message of AOSP libcore commit 29957558cf0db700bfaae360a80c42dc3871d0e5 68 * at https://android-review.googlesource.com/c/304056/ 69 * 70 * To get OpenJDK head: hg clone http://hg.openjdk.java.net/jdk/jdk/ head 71 */ 72 public class CompareUpstreams { 73 74 /** 75 * Whether to compare against snapshots based on (a) the output of {@link CopyUpstreamFiles}, 76 * as opposed to (b) directly against checked-out upstream source {@link Repository}s. 77 * 78 * Because the snapshots are currently kept on x20 which is slow to access, (b) run much 79 * faster (a few seconds vs. 30 minutes), but it requires the checked-out and compiled 80 * upstream repositories to exist which is not the case for everyone / not easily achievable 81 * (OpenJDK 8 requires an old C++ compiler to build). 82 */ 83 public static final boolean COMPARE_AGAINST_UPSTREAM_SNAPSHOT = true; 84 85 private final StandardRepositories standardRepositories; 86 CompareUpstreams(StandardRepositories standardRepositories)87 public CompareUpstreams(StandardRepositories standardRepositories) { 88 this.standardRepositories = Objects.requireNonNull(standardRepositories); 89 } 90 androidChangedComments(List<String> lines)91 private static Map<String, Integer> androidChangedComments(List<String> lines) { 92 List<String> problems = new ArrayList<>(); 93 Map<String, Integer> result = new LinkedHashMap<>(); 94 Pattern pattern = Pattern.compile( 95 "// (BEGIN |END |)Android-((?:changed|added|removed|note)(?:: )?.*)$"); 96 for (String line : lines) { 97 Matcher matcher = pattern.matcher(line); 98 if (matcher.find()) { 99 String type = matcher.group(1); 100 if (type.equals("END")) { 101 continue; 102 } 103 String match = matcher.group(2); 104 if (match.isEmpty()) { 105 match = "[empty comment]"; 106 } 107 Integer oldCount = result.get(match); 108 if (oldCount == null) { 109 oldCount = 0; 110 } 111 result.put(match, oldCount + 1); 112 } else if (line.contains("Android-")) { 113 problems.add(line); 114 } 115 } 116 if (!problems.isEmpty()) { 117 throw new IllegalArgumentException(problems.toString()); 118 } 119 return result; 120 } 121 androidChangedCommentsSummary(List<String> lines)122 private static String androidChangedCommentsSummary(List<String> lines) { 123 Map<String, Integer> map = androidChangedComments(lines); 124 List<String> comments = new ArrayList<>(map.keySet()); 125 Collections.sort(comments, Comparator.comparing(map::get).reversed()); 126 List<String> result = new ArrayList<>(); 127 for (String comment : comments) { 128 int count = map.get(comment); 129 if (count == 1) { 130 result.add(comment); 131 } else { 132 result.add(comment + " (x" + count + ")"); 133 } 134 } 135 return escapeTsv(String.join("\n", result)); 136 } 137 escapeTsv(String value)138 private static String escapeTsv(String value) { 139 if (value.contains("\t")) { 140 throw new IllegalArgumentException(value); // tsv doesn't support escaping tabs 141 } 142 return "\"" + value.replace("\"", "\"\"") + "\""; 143 } 144 printTsv(PrintStream out, List<String> values)145 private static void printTsv(PrintStream out, List<String> values) { 146 out.println(String.join("\t", values)); 147 } 148 149 /** 150 * Prints tab-separated values comparing ojluni files vs. each 151 * upstream, for each of the rel_paths, suitable for human 152 * analysis in a spreadsheet. 153 * This includes whether the corresponding upstream file is 154 * missing, identical, or by how many lines it differs, and 155 * a guess as to the correct upstream based on minimal line 156 * difference (ties broken in favor of upstreams that occur 157 * earlier in the list). 158 */ run(PrintStream out, List<Path> relPaths)159 private void run(PrintStream out, List<Path> relPaths) throws IOException { 160 // upstreams are in decreasing order of preference 161 List<String> headers = new ArrayList<>(); 162 headers.addAll(Arrays.asList( 163 "rel_path", "expected_upstream", "guessed_upstream", "changes", "vs. expected")); 164 for (Repository upstream : standardRepositories.historicUpstreams()) { 165 headers.add(upstream.name()); 166 } 167 headers.add("diff"); 168 printTsv(out, headers); 169 170 Path snapshotRoot = COMPARE_AGAINST_UPSTREAM_SNAPSHOT 171 ? Util.pathFromEnvOrThrow("OJLUNI_UPSTREAMS") 172 : null; 173 174 for (Path relPath : relPaths) { 175 Repository expectedUpstream = standardRepositories.referenceUpstream(relPath); 176 out.print(relPath + "\t"); 177 Path ojluniFile = standardRepositories.ojluni().absolutePath(relPath); 178 List<String> linesB = Util.readLines(ojluniFile); 179 int bestDistance = Integer.MAX_VALUE; 180 Repository guessedUpstream = null; 181 List<Repository> upstreams = new ArrayList<>(); 182 upstreams.add(expectedUpstream); 183 upstreams.addAll(standardRepositories.historicUpstreams()); 184 List<String> comparisons = new ArrayList<>(upstreams.size()); 185 for (Repository upstream : upstreams) { 186 final String comparison; 187 final Path upstreamFile; 188 if (COMPARE_AGAINST_UPSTREAM_SNAPSHOT) { 189 Path maybePath = snapshotRoot 190 .resolve(upstream.name()) 191 .resolve(relPath); 192 upstreamFile = maybePath.toFile().exists() ? maybePath : null; 193 } else { 194 upstreamFile = upstream.absolutePath(relPath); 195 } 196 if (upstreamFile == null) { 197 comparison = "missing"; 198 } else { 199 List<String> linesA = Util.readLines(upstreamFile); 200 int distance = Util.editDistance(linesA, linesB); 201 if (distance == 0) { 202 comparison = "identical"; 203 } else { 204 double percentDifferent = 100.0 * distance / Math 205 .max(linesA.size(), linesB.size()); 206 comparison = String 207 .format(Locale.US, "%.1f%% different (%d lines)", percentDifferent, 208 distance); 209 } 210 if (distance < bestDistance) { 211 bestDistance = distance; 212 guessedUpstream = upstream; 213 } 214 } 215 comparisons.add(comparison); 216 } 217 String changedCommentsSummary = androidChangedCommentsSummary(linesB); 218 219 String diffCommand = ""; 220 if (!comparisons.get(0).equals("identical")) { 221 Path expectedUpstreamPath = expectedUpstream.pathFromRepository(relPath); 222 if (expectedUpstreamPath != null) { 223 diffCommand = "${ANDROID_BUILD_TOP}/libcore/tools/upstream/upstream-diff " 224 + "-r ojluni," + expectedUpstream.name() + " " + relPath; 225 } else { 226 diffCommand = "FILE MISSING"; 227 } 228 } 229 List<String> values = new ArrayList<>(); 230 values.add(expectedUpstream.name()); 231 values.add(guessedUpstream == null ? "" : guessedUpstream.name()); 232 values.add(changedCommentsSummary); 233 values.addAll(comparisons); 234 values.add(diffCommand); 235 printTsv(out, values); 236 } 237 } 238 run()239 public void run() throws IOException { 240 List<Path> relPaths = standardRepositories.ojluni().loadRelPathsFromBlueprint(); 241 run(System.out, relPaths); 242 } 243 main(String[] args)244 public static void main(String[] args) throws IOException { 245 StandardRepositories standardRepositories = StandardRepositories.fromEnv(); 246 CompareUpstreams action = new CompareUpstreams(standardRepositories); 247 action.run(); 248 } 249 } 250