| // Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| package com.android.tools.r8.utils; |
| |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertThat; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.naming.MemberNaming.FieldSignature; |
| import com.android.tools.r8.naming.MemberNaming.MethodSignature; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; |
| import com.android.tools.r8.utils.codeinspector.DexInstructionSubject; |
| import com.android.tools.r8.utils.codeinspector.FoundClassSubject; |
| import com.android.tools.r8.utils.codeinspector.FoundFieldSubject; |
| import com.android.tools.r8.utils.codeinspector.FoundMethodSubject; |
| import com.android.tools.r8.utils.codeinspector.InstructionSubject; |
| import com.android.tools.r8.utils.codeinspector.MethodSubject; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.BiConsumer; |
| import java.util.function.Predicate; |
| import org.junit.Ignore; |
| import org.junit.Test; |
| |
| // A comparator that inputs two apks you want to investigate, along with mappings if available. |
| // This will help you walk through apks in a fancy IDE debugging mode. |
| public class AppComparator extends TestBase { |
| // Update the following path strings to point to apks you are inspecting. |
| private static final String PATH_0 = "/path/to/workspace"; |
| private static final String PATH_1 = PATH_0 + "/R8.apk"; |
| private static final String PATH_2 = PATH_0 + "/proguard.apk"; |
| private static final String MAP_1 = PATH_0 + "/mapping-R8.txt"; |
| private static final String MAP_2 = PATH_0 + "/mapping-proguard.txt"; |
| |
| private static final boolean allowMissingClassInApp2 = true; |
| |
| private AndroidApp loadApp(String path) { |
| AndroidApp.Builder builder = AndroidApp.builder(); |
| builder.addProgramFile(Paths.get(path)); |
| return builder.build(); |
| } |
| |
| @Ignore("Comment out this to run locally.") |
| @Test |
| public void identicalTest_specificMethods() throws Exception { |
| AndroidApp app1 = loadApp(PATH_1); |
| AndroidApp app2 = loadApp(PATH_2); |
| |
| CodeInspector inspect1 = new CodeInspector(app1, Paths.get(MAP_1)); |
| CodeInspector inspect2 = new CodeInspector(app2, Paths.get(MAP_2)); |
| |
| // Define your own tester to pick methods to inspect. |
| Predicate<DexEncodedMethod> methodTester = encodedMethod -> { |
| return encodedMethod.method.name.toString().equals("run") |
| && encodedMethod.method.getArity() == 0; |
| }; |
| |
| inspect1.forAllClasses(clazz1 -> { |
| clazz1.forAllMethods(method1 -> { |
| if (methodTester.test(method1.getMethod())) { |
| ClassSubject clazz2 = inspect2.clazz(clazz1.getOriginalName()); |
| if (!clazz2.isPresent()) { |
| String classNotFound = |
| String.format("Class %s not found in app2", clazz1.getOriginalName()); |
| if (allowMissingClassInApp2) { |
| System.out.println(classNotFound); |
| return; |
| } |
| assertThat(classNotFound, clazz2, isPresent()); |
| } |
| MethodSubject method2 = clazz2.method(method1.getOriginalSignature()); |
| if (!method2.isPresent()) { |
| method2 = clazz2.method(method1.getFinalSignature()); |
| } |
| if (!method2.isPresent()) { |
| assertThat(String.format("Method %s not found in app2", method1.getFinalSignature()), |
| method2, isPresent()); |
| } |
| if (method1.getMethod().shouldNotHaveCode()) { |
| assertTrue(method2.getMethod().shouldNotHaveCode()); |
| return; |
| } |
| if (!identicalCode(method1, method2)) { |
| System.out.println("Found different method body: "); |
| System.out.println(method1.getMethod().codeToString()); |
| System.out.println(method2.getMethod().codeToString()); |
| } |
| } |
| }); |
| }); |
| } |
| |
| @Ignore("Comment out this to run locally.") |
| @Test |
| public void identicalTest_wholeApp() throws Exception { |
| // Set to false to not compare the code, but only the structure. |
| boolean compareInstructions = true; |
| |
| AndroidApp app1 = loadApp(PATH_1); |
| AndroidApp app2 = loadApp(PATH_2); |
| |
| CodeInspector inspect1 = new CodeInspector(app1); |
| CodeInspector inspect2 = new CodeInspector(app2); |
| |
| class Pair<T> { |
| private T first; |
| private T second; |
| |
| private void set(boolean selectFirst, T value) { |
| if (selectFirst) { |
| first = value; |
| } else { |
| second = value; |
| } |
| } |
| } |
| |
| // Collect all classes from both inspectors, indexed by finalDescriptor. |
| Map<String, Pair<FoundClassSubject>> allClasses = new HashMap<>(); |
| |
| BiConsumer<CodeInspector, Boolean> collectClasses = (inspector, selectFirst) -> { |
| inspector.forAllClasses( |
| clazz -> { |
| String finalDescriptor = clazz.getFinalDescriptor(); |
| allClasses.compute( |
| finalDescriptor, |
| (k, v) -> { |
| if (v == null) { |
| v = new Pair<>(); |
| } |
| v.set(selectFirst, clazz); |
| return v; |
| }); |
| }); |
| }; |
| |
| collectClasses.accept(inspect1, true); |
| collectClasses.accept(inspect2, false); |
| |
| for (Map.Entry<String, Pair<FoundClassSubject>> classEntry : allClasses.entrySet()) { |
| String className = classEntry.getKey(); |
| FoundClassSubject class1 = classEntry.getValue().first; |
| FoundClassSubject class2 = classEntry.getValue().second; |
| |
| assert class1 != null || class2 != null; |
| |
| assertNotNull(String.format("Class %s is missing from the first app.", className), class1); |
| assertNotNull(String.format("Class %s is missing from the second app.", className), class2); |
| |
| // Collect all fields for this class from both apps. |
| Map<FieldSignature, Pair<FoundFieldSubject>> allFields = new HashMap<>(); |
| |
| BiConsumer<FoundClassSubject, Boolean> collectFields = (classSubject, selectFirst) -> { |
| classSubject.forAllFields( |
| f -> { |
| FieldSignature fs = f.getFinalSignature(); |
| allFields.compute( |
| fs, |
| (k, v) -> { |
| if (v == null) { |
| v = new Pair<>(); |
| } |
| v.set(selectFirst, f); |
| return v; |
| }); |
| }); |
| }; |
| |
| collectFields.accept(class1, true); |
| collectFields.accept(class2, false); |
| |
| for (Map.Entry<FieldSignature, Pair<FoundFieldSubject>> fieldEntry : allFields.entrySet()) { |
| FieldSignature signature = fieldEntry.getKey(); |
| FoundFieldSubject field1 = fieldEntry.getValue().first; |
| FoundFieldSubject field2 = fieldEntry.getValue().second; |
| assert field1 != null || field2 != null; |
| |
| assertNotNull( |
| String.format( |
| "Field %s of class %s is missing from the first app.", signature, className), |
| field1); |
| assertNotNull( |
| String.format( |
| "Field %s of class %s is missing from the second app.", signature, className), |
| field2); |
| } |
| |
| // Collect all methods for this class from both apps. |
| Map<MethodSignature, Pair<FoundMethodSubject>> allMethods = new HashMap<>(); |
| |
| BiConsumer<FoundClassSubject, Boolean> collectMethods = (classSubject, selectFirst) -> { |
| classSubject.forAllMethods( |
| m -> { |
| MethodSignature fs = m.getFinalSignature(); |
| allMethods.compute( |
| fs, |
| (k, v) -> { |
| if (v == null) { |
| v = new Pair<>(); |
| } |
| v.set(selectFirst, m); |
| return v; |
| }); |
| }); |
| }; |
| |
| collectMethods.accept(class1, true); |
| collectMethods.accept(class2, false); |
| |
| List<FoundMethodSubject> methodsNotMatching = new ArrayList<>(); |
| for (Map.Entry<MethodSignature, Pair<FoundMethodSubject>> methodEntry : |
| allMethods.entrySet()) { |
| MethodSignature signature = methodEntry.getKey(); |
| FoundMethodSubject method1 = methodEntry.getValue().first; |
| FoundMethodSubject method2 = methodEntry.getValue().second; |
| assert method1 != null || method2 != null; |
| |
| assertNotNull( |
| String.format( |
| "Method %s of class %s is missing from the first app.", signature, className), |
| method1); |
| assertNotNull( |
| String.format( |
| "Method %s of class %s is missing from the second app.", signature, className), |
| method2); |
| |
| if (method1.getMethod().shouldNotHaveCode()) { |
| assertTrue(method2.getMethod().shouldNotHaveCode()); |
| continue; |
| } |
| |
| // Even compare every single instruction. Adjust for your own purpose or comment out. |
| if (compareInstructions) { |
| if (!identicalCode(method1, method2)) { |
| System.out.println("Full method for " + method1.toString() + ": "); |
| methodsNotMatching.add(method1); |
| methodsNotMatching.add(method2); |
| } |
| } |
| } |
| if (!methodsNotMatching.isEmpty()) { |
| System.out.println("-------------------------------------"); |
| for (int i = 0; i < methodsNotMatching.size(); i += 2) { |
| FoundMethodSubject method1 = methodsNotMatching.get(i); |
| FoundMethodSubject method2 = methodsNotMatching.get(i + 1); |
| System.out.println("Full method for " + method1.toString() + ": "); |
| System.out.println(method1.getMethod().codeToString()); |
| System.out.println(method2.getMethod().codeToString()); |
| } |
| } |
| } |
| } |
| |
| private boolean identicalCode(MethodSubject method1, MethodSubject method2) { |
| Iterator<InstructionSubject> it1 = method1.iterateInstructions(); |
| Iterator<InstructionSubject> it2 = method2.iterateInstructions(); |
| boolean identical = true; |
| while (it1.hasNext()) { |
| assertTrue(it2.hasNext()); |
| InstructionSubject instr1 = it1.next(); |
| InstructionSubject instr2 = it2.next(); |
| assertEquals( |
| instr1 instanceof DexInstructionSubject, |
| instr2 instanceof DexInstructionSubject); |
| if (!instr1.equals(instr2)) { |
| if (identical) { |
| System.out.println("DIFF in " + method1.toString() + ":"); |
| } |
| System.out.println("< " + instr1); |
| System.out.println("> " + instr2); |
| identical = false; |
| } |
| } |
| return identical; |
| } |
| } |