Merge "Do not translate File.separatorChar to / when reading from archives."
diff --git a/build.gradle b/build.gradle
index f379bbf..8ea2b3f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1239,6 +1239,18 @@
}
test {
+ if (project.hasProperty('generate_golden_files_to')) {
+ systemProperty 'generate_golden_files_to', project.property('generate_golden_files_to')
+ assert project.hasProperty('HEAD_sha1')
+ systemProperty 'test_git_HEAD_sha1', project.property('HEAD_sha1')
+ }
+
+ if (project.hasProperty('use_golden_files_in')) {
+ systemProperty 'use_golden_files_in', project.property('use_golden_files_in')
+ assert project.hasProperty('HEAD_sha1')
+ systemProperty 'test_git_HEAD_sha1', project.property('HEAD_sha1')
+ }
+
dependsOn getJarsFromSupportLibs
testLogging.exceptionFormat = 'full'
if (project.hasProperty('print_test_stdout')) {
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 0ba40b3..fb3b067 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
// This field is accessed from release scripts using simple pattern matching.
// Therefore, changing this field could break our release scripts.
- public static final String LABEL = "1.2.20-dev";
+ public static final String LABEL = "1.2.21-dev";
private Version() {
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 9f97f88..831edbe 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -43,12 +43,9 @@
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
import java.util.function.Consumer;
public class DexEncodedMethod extends KeyedDexItem<DexMethod> implements ResolutionResult {
@@ -582,6 +579,14 @@
return m1.method.slowCompareTo(m2.method);
}
+ public static class ClassInlinerEligibility {
+ public final boolean returnsReceiver;
+
+ public ClassInlinerEligibility(boolean returnsReceiver) {
+ this.returnsReceiver = returnsReceiver;
+ }
+ }
+
public static class OptimizationInfo {
private int returnedArgument = -1;
@@ -593,8 +598,9 @@
private boolean useIdentifierNameString = false;
private boolean checksNullReceiverBeforeAnySideEffect = false;
private boolean triggersClassInitBeforeAnySideEffect = false;
- private Set<DexField> receiverOnlyUsedForReadingFields = null;
- private Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects = null;
+ // Stores information about instance methods and constructors for
+ // class inliner, null value indicates that the method is not eligible.
+ private ClassInlinerEligibility classInlinerEligibility = null;
private OptimizationInfo() {
// Intentionally left empty.
@@ -631,13 +637,12 @@
return returnsConstant;
}
- public boolean isReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
- return receiverOnlyUsedForReadingFields != null &&
- fields.containsAll(receiverOnlyUsedForReadingFields);
+ private void setClassInlinerEligibility(ClassInlinerEligibility eligibility) {
+ this.classInlinerEligibility = eligibility;
}
- public Map<DexField, Integer> onlyInitializesFieldsWithNoOtherSideEffects() {
- return onlyInitializesFieldsWithNoOtherSideEffects;
+ public ClassInlinerEligibility getClassInlinerEligibility() {
+ return this.classInlinerEligibility;
}
public long getReturnedConstant() {
@@ -681,19 +686,6 @@
returnedConstant = value;
}
- private void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
- receiverOnlyUsedForReadingFields = fields;
- }
-
- private void markOnlyInitializesFieldsWithNoOtherSideEffects(Map<DexField, Integer> mapping) {
- if (mapping == null) {
- onlyInitializesFieldsWithNoOtherSideEffects = null;
- } else {
- onlyInitializesFieldsWithNoOtherSideEffects = mapping.isEmpty()
- ? Collections.emptyMap() : ImmutableMap.copyOf(mapping);
- }
- }
-
private void markForceInline() {
forceInline = true;
}
@@ -751,13 +743,8 @@
ensureMutableOI().markReturnsConstant(value);
}
- synchronized public void markReceiverOnlyUsedForReadingFields(Set<DexField> fields) {
- ensureMutableOI().markReceiverOnlyUsedForReadingFields(fields);
- }
-
- synchronized public void markOnlyInitializesFieldsWithNoOtherSideEffects(
- Map<DexField, Integer> mapping) {
- ensureMutableOI().markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+ synchronized public void setClassInlinerEligibility(ClassInlinerEligibility eligibility) {
+ ensureMutableOI().setClassInlinerEligibility(eligibility);
}
synchronized public void markForceInline() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index 85a2509..a5599fa 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -19,6 +19,7 @@
public class NewInstance extends Instruction {
public final DexType clazz;
+ private boolean allowSpilling = true;
public NewInstance(DexType clazz, Value dest) {
super(dest);
@@ -109,4 +110,12 @@
public boolean triggersInitializationOfClass(DexType klass) {
return clazz == klass;
}
+
+ public void markNoSpilling() {
+ allowSpilling = false;
+ }
+
+ public boolean isSpillingAllowed() {
+ return allowSpilling;
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 186839b..51850a0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -76,6 +76,10 @@
}
public void addOperands(List<Value> operands) {
+ addOperands(operands, true);
+ }
+
+ public void addOperands(List<Value> operands, boolean removeTrivialPhi) {
// Phi operands are only filled in once to complete the phi. Some phis are incomplete for a
// period of time to break cycles. When the cycle has been resolved they are completed
// exactly once by adding the operands.
@@ -91,7 +95,9 @@
if (!canBeNull) {
markNeverNull();
}
- removeTrivialPhi();
+ if (removeTrivialPhi) {
+ removeTrivialPhi();
+ }
}
public void addDebugValue(Value value) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 90a9898..81dc436 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -169,7 +169,8 @@
}
this.classInliner =
(options.enableClassInlining && options.enableInlining && inliner != null)
- ? new ClassInliner(appInfo.dexItemFactory) : null;
+ ? new ClassInliner(appInfo.dexItemFactory, options.classInliningInstructionLimit)
+ : null;
}
/**
@@ -627,6 +628,10 @@
printC1VisualizerHeader(method);
printMethod(code, "Initial IR (SSA)");
+ if (options.canHaveArtStringNewInitBug()) {
+ codeRewriter.ensureDirectStringNewToInit(code);
+ }
+
if (options.debug) {
codeRewriter.simplifyDebugLocals(code);
}
@@ -752,10 +757,7 @@
}
// Analysis must be done after method is rewritten by logArgumentTypes()
- codeRewriter.identifyReceiverOnlyUsedForReadingFields(method, code, feedback);
- if (method.isInstanceInitializer()) {
- codeRewriter.identifyOnlyInitializesFieldsWithNoOtherSideEffects(method, code, feedback);
- }
+ codeRewriter.identifyClassInlinerEligibility(method, code, feedback);
printMethod(code, "Optimized IR (SSA)");
finalizeIR(method, code, feedback);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index 26ca6b2..16a77c8 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -5,10 +5,8 @@
package com.android.tools.r8.ir.conversion;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
-import java.util.Map;
-import java.util.Set;
public interface OptimizationFeedback {
void methodReturnsArgument(DexEncodedMethod method, int argument);
@@ -18,7 +16,5 @@
void markProcessed(DexEncodedMethod method, Constraint state);
void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
- void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields);
- void markOnlyInitializesFieldsWithNoOtherSideEffects(
- DexEncodedMethod method, Map<DexField, Integer> mapping);
+ void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
index 645df64..7303b6b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -5,10 +5,8 @@
package com.android.tools.r8.ir.conversion;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
-import java.util.Map;
-import java.util.Set;
public class OptimizationFeedbackDirect implements OptimizationFeedback {
@@ -48,13 +46,8 @@
}
@Override
- public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
- method.markReceiverOnlyUsedForReadingFields(fields);
- }
-
- @Override
- public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
- Map<DexField, Integer> mapping) {
- method.markOnlyInitializesFieldsWithNoOtherSideEffects(mapping);
+ public void setClassInlinerEligibility(
+ DexEncodedMethod method, ClassInlinerEligibility eligibility) {
+ method.setClassInlinerEligibility(eligibility);
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index 97fe052..6915a2f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -5,10 +5,8 @@
package com.android.tools.r8.ir.conversion;
import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
-import java.util.Map;
-import java.util.Set;
public class OptimizationFeedbackIgnore implements OptimizationFeedback {
@@ -34,11 +32,7 @@
public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {}
@Override
- public void markReceiverOnlyUsedForReadingFields(DexEncodedMethod method, Set<DexField> fields) {
- }
-
- @Override
- public void markOnlyInitializesFieldsWithNoOtherSideEffects(DexEncodedMethod method,
- Map<DexField, Integer> mapping) {
+ public void setClassInlinerEligibility(
+ DexEncodedMethod method, ClassInlinerEligibility eligibility) {
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 3ea434a..bd3442c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
@@ -47,8 +48,6 @@
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.If;
import com.android.tools.r8.ir.code.If.Type;
-import com.android.tools.r8.ir.code.InstanceGet;
-import com.android.tools.r8.ir.code.InstancePut;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionIterator;
import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -60,6 +59,7 @@
import com.android.tools.r8.ir.code.MemberType;
import com.android.tools.r8.ir.code.NewArrayEmpty;
import com.android.tools.r8.ir.code.NewArrayFilledData;
+import com.android.tools.r8.ir.code.NewInstance;
import com.android.tools.r8.ir.code.NumericType;
import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.Position;
@@ -97,9 +97,12 @@
import it.unimi.dsi.fastutil.ints.IntLists;
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -736,120 +739,72 @@
}
}
- public void identifyReceiverOnlyUsedForReadingFields(
+ public void identifyClassInlinerEligibility(
DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
- if (!method.isNonAbstractVirtualMethod()) {
+ // Method eligibility is calculated in similar way for regular method
+ // and for the constructor. To be eligible method should only be using its
+ // receiver in the following ways:
+ //
+ // (1) as a receiver of reads/writes of instance fields of the holder class
+ // (2) as a return value
+ // (3) as a receiver of a call to the superclass initializer
+ //
+ boolean instanceInitializer = method.isInstanceInitializer();
+ if (method.accessFlags.isNative() ||
+ (!method.isNonAbstractVirtualMethod() && !instanceInitializer)) {
return;
}
- feedback.markReceiverOnlyUsedForReadingFields(method, null);
+ feedback.setClassInlinerEligibility(method, null); // To allow returns below.
- Value instance = code.getThis();
- if (instance.numberOfPhiUsers() > 0) {
- return;
- }
-
- Set<DexField> fields = Sets.newIdentityHashSet();
- for (Instruction insn : instance.uniqueUsers()) {
- if (!insn.isInstanceGet()) {
- return;
- }
- InstanceGet get = insn.asInstanceGet();
- if (get.dest() == instance || get.object() != instance) {
- return;
- }
- fields.add(get.getField());
- }
- feedback.markReceiverOnlyUsedForReadingFields(method, fields);
- }
-
- public void identifyOnlyInitializesFieldsWithNoOtherSideEffects(
- DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
- assert method.isInstanceInitializer();
-
- feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, null);
-
- if (code.hasCatchHandlers()) {
- return;
- }
-
- List<Value> args = code.collectArguments(true /* not interested in receiver */);
- Map<DexField, Integer> mapping = new IdentityHashMap<>();
Value receiver = code.getThis();
-
- InstructionIterator it = code.instructionIterator();
- while (it.hasNext()) {
- Instruction instruction = it.next();
-
- // Mark an argument.
- if (instruction.isArgument()) {
- continue;
- }
-
- // Allow super call to java.lang.Object.<init>() on 'this'.
- if (instruction.isInvokeDirect()) {
- InvokeDirect invokedDirect = instruction.asInvokeDirect();
- if (invokedDirect.getInvokedMethod() != dexItemFactory.objectMethods.constructor ||
- invokedDirect.getReceiver() != receiver) {
- return;
- }
- continue;
- }
-
- // Allow final return.
- if (instruction.isReturn()) {
- continue;
- }
-
- // Allow assignment to this class' fields. If the assigned value is an argument
- // reference update the mep. Otherwise just allow assigning any value, since all
- // invalid values should be filtered out at the definitions.
- if (instruction.isInstancePut()) {
- InstancePut instancePut = instruction.asInstancePut();
- DexField field = instancePut.getField();
- if (instancePut.object() != receiver) {
- return;
- }
-
- Value value = instancePut.value();
- if (value.isArgument() && !value.isThis()) {
- assert (args.contains(value));
- mapping.put(field, args.indexOf(value));
- } else {
- mapping.remove(field);
- }
- continue;
- }
-
- // Allow non-throwing constants.
- if (instruction.isConstInstruction()) {
- if (instruction.instructionTypeCanThrow()) {
- return;
- }
- continue;
- }
-
- // Allow goto instructions jumping to the next block.
- if (instruction.isGoto()) {
- if (instruction.getBlock().getNumber() + 1 !=
- instruction.asGoto().getTarget().getNumber()) {
- return;
- }
- continue;
- }
-
- // Allow binary and unary instructions if they don't throw.
- if (instruction.isBinop() || instruction.isUnop()) {
- if (instruction.instructionTypeCanThrow()) {
- return;
- }
- continue;
- }
-
+ if (receiver.numberOfPhiUsers() > 0) {
return;
}
- feedback.markOnlyInitializesFieldsWithNoOtherSideEffects(method, mapping);
+ boolean receiverUsedAsReturnValue = false;
+ boolean seenSuperInitCall = false;
+ for (Instruction insn : receiver.uniqueUsers()) {
+ if (insn.isReturn()) {
+ receiverUsedAsReturnValue = true;
+ continue;
+ }
+
+ if (insn.isInstanceGet() ||
+ (insn.isInstancePut() && insn.asInstancePut().object() == receiver)) {
+ DexField field = insn.asFieldInstruction().getField();
+ if (field.clazz == method.method.holder) {
+ // Since class inliner currently only supports classes directly extending
+ // java.lang.Object, we don't need to worry about fields defined in superclasses.
+ continue;
+ }
+ return;
+ }
+
+ // If this is an instance initializer allow one call
+ // to java.lang.Object.<init>() on 'this'.
+ if (instanceInitializer && insn.isInvokeDirect()) {
+ InvokeDirect invokedDirect = insn.asInvokeDirect();
+ if (invokedDirect.getInvokedMethod() == dexItemFactory.objectMethods.constructor &&
+ invokedDirect.getReceiver() == receiver &&
+ !seenSuperInitCall) {
+ seenSuperInitCall = true;
+ continue;
+ }
+ return;
+ }
+
+ // Other receiver usages make the method not eligible.
+ return;
+ }
+
+ if (instanceInitializer && !seenSuperInitCall) {
+ // Call to super constructor not found?
+ return;
+ }
+
+ feedback.setClassInlinerEligibility(
+ method, new ClassInlinerEligibility(receiverUsedAsReturnValue));
}
/**
@@ -2683,4 +2638,147 @@
// When we fall out of the loop the iterator is in the last eol block.
iterator.add(new InvokeVirtual(printLn, null, ImmutableList.of(out, empty)));
}
+
+ public static void ensureDirectStringNewToInit(IRCode code) {
+ DexItemFactory factory = code.options.itemFactory;
+ for (BasicBlock block : code.blocks) {
+ for (InstructionListIterator it = block.listIterator(); it.hasNext(); ) {
+ Instruction instruction = it.next();
+ if (instruction.isInvokeDirect()) {
+ InvokeDirect invoke = instruction.asInvokeDirect();
+ DexMethod method = invoke.getInvokedMethod();
+ if (factory.isConstructor(method)
+ && method.holder == factory.stringType
+ && invoke.getReceiver().isPhi()) {
+ NewInstance newInstance = findNewInstance(invoke.getReceiver().asPhi());
+ replaceTrivialNewInstancePhis(newInstance.outValue());
+ if (invoke.getReceiver().isPhi()) {
+ throw new CompilationError(
+ "Failed to remove trivial phis between new-instance and <init>");
+ }
+ newInstance.markNoSpilling();
+ }
+ }
+ }
+ }
+ }
+
+ private static NewInstance findNewInstance(Phi phi) {
+ Set<Phi> seen = new HashSet<>();
+ Set<Value> values = new HashSet<>();
+ recursiveAddOperands(phi, seen, values);
+ if (values.size() != 1) {
+ throw new CompilationError("Failed to identify unique new-instance for <init>");
+ }
+ Value newInstanceValue = values.iterator().next();
+ if (newInstanceValue.definition == null || !newInstanceValue.definition.isNewInstance()) {
+ throw new CompilationError("Invalid defining value for call to <init>");
+ }
+ return newInstanceValue.definition.asNewInstance();
+ }
+
+ private static void recursiveAddOperands(Phi phi, Set<Phi> seen, Set<Value> values) {
+ for (Value operand : phi.getOperands()) {
+ if (!operand.isPhi()) {
+ values.add(operand);
+ } else {
+ Phi phiOp = operand.asPhi();
+ if (seen.add(phiOp)) {
+ recursiveAddOperands(phiOp, seen, values);
+ }
+ }
+ }
+ }
+
+ // If an <init> call takes place on a phi the code must contain an irreducible loop between the
+ // new-instance and the <init>. Assuming the code is verifiable, new-instance must flow to a
+ // unique <init>. Here we compute the set of strongly connected phis making use of the
+ // new-instance value and replace all trivial ones by the new-instance value.
+ // This is a simplified variant of the removeRedundantPhis algorithm in Section 3.2 of:
+ // http://compilers.cs.uni-saarland.de/papers/bbhlmz13cc.pdf
+ private static void replaceTrivialNewInstancePhis(Value newInstanceValue) {
+ List<Set<Value>> components = new SCC().computeSCC(newInstanceValue);
+ for (int i = components.size() - 1; i >= 0; i--) {
+ Set<Value> component = components.get(i);
+ if (component.size() == 1 && component.iterator().next() == newInstanceValue) {
+ continue;
+ }
+ Set<Phi> trivialPhis = new HashSet<>();
+ for (Value value : component) {
+ boolean isTrivial = true;
+ Phi p = value.asPhi();
+ for (Value op : p.getOperands()) {
+ if (op != newInstanceValue && !component.contains(op)) {
+ isTrivial = false;
+ break;
+ }
+ }
+ if (isTrivial) {
+ trivialPhis.add(p);
+ }
+ }
+ for (Phi trivialPhi : trivialPhis) {
+ for (Value op : trivialPhi.getOperands()) {
+ op.removePhiUser(trivialPhi);
+ }
+ trivialPhi.replaceUsers(newInstanceValue);
+ trivialPhi.getBlock().removePhi(trivialPhi);
+ }
+ }
+ }
+
+ // Dijkstra's path-based strongly-connected components algorithm.
+ // https://en.wikipedia.org/wiki/Path-based_strong_component_algorithm
+ private static class SCC {
+
+ private int currentTime = 0;
+ private final Reference2IntMap<Value> discoverTime = new Reference2IntOpenHashMap<>();
+ private final Set<Value> unassignedSet = new HashSet<>();
+ private final Deque<Value> unassignedStack = new ArrayDeque<>();
+ private final Deque<Value> preorderStack = new ArrayDeque<>();
+ private final List<Set<Value>> components = new ArrayList<>();
+
+ public List<Set<Value>> computeSCC(Value v) {
+ assert currentTime == 0;
+ dfs(v);
+ return components;
+ }
+
+ private void dfs(Value value) {
+ discoverTime.put(value, currentTime++);
+ unassignedSet.add(value);
+ unassignedStack.push(value);
+ preorderStack.push(value);
+ for (Phi phi : value.uniquePhiUsers()) {
+ if (!discoverTime.containsKey(phi)) {
+ // If not seen yet, continue the search.
+ dfs(phi);
+ } else if (unassignedSet.contains(phi)) {
+ // If seen already and the element is on the unassigned stack we have found a cycle.
+ // Pop off everything discovered later than the target from the preorder stack. This may
+ // not coincide with the cycle as an outer cycle may already have popped elements off.
+ int discoverTimeOfPhi = discoverTime.getInt(phi);
+ while (discoverTimeOfPhi < discoverTime.getInt(preorderStack.peek())) {
+ preorderStack.pop();
+ }
+ }
+ }
+ if (preorderStack.peek() == value) {
+ // If the current element is the top of the preorder stack, then we are at entry to a
+ // strongly-connected component consisting of this element and every element above this
+ // element on the stack.
+ Set<Value> component = new HashSet<>(unassignedStack.size());
+ while (true) {
+ Value member = unassignedStack.pop();
+ unassignedSet.remove(member);
+ component.add(member);
+ if (member == value) {
+ components.add(component);
+ break;
+ }
+ }
+ preorderStack.pop();
+ }
+ }
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 7a6a846..92bda1d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -86,6 +86,7 @@
});
}
+ code.removeAllTrivialPhis();
assert code.isConsistentSSA();
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 845e17b..01dcf0d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -315,6 +315,12 @@
}
@Override
+ public boolean isValidTarget(InvokeMethod invoke, DexEncodedMethod target, IRCode inlinee) {
+ return !target.isInstanceInitializer()
+ || inliner.legalConstructorInline(method, invoke, inlinee);
+ }
+
+ @Override
public boolean exceededAllowance() {
return instructionAllowance < 0;
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index 86d40d9..b56dc21 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -61,6 +61,11 @@
}
@Override
+ public boolean isValidTarget(InvokeMethod invoke, DexEncodedMethod target, IRCode inlinee) {
+ return true;
+ }
+
+ @Override
public ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor) {
return blockIterator;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index b75597d..25cd969 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -285,7 +285,7 @@
return numOfInstructions;
}
- private boolean legalConstructorInline(DexEncodedMethod method,
+ boolean legalConstructorInline(DexEncodedMethod method,
InvokeMethod invoke, IRCode code) {
// In the Java VM Specification section "4.10.2.4. Instance Initialization Methods and
@@ -447,8 +447,7 @@
// Make sure constructor inlining is legal.
assert !target.isClassInitializer();
- if (target.isInstanceInitializer()
- && !legalConstructorInline(method, invoke, inlinee)) {
+ if (!strategy.isValidTarget(invoke, target, inlinee)) {
continue;
}
DexType downcast = createDowncastIfNeeded(strategy, invoke, target);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
index 7b88b38..2443d11 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -18,6 +18,8 @@
void ensureMethodProcessed(DexEncodedMethod target, IRCode inlinee);
+ boolean isValidTarget(InvokeMethod invoke, DexEncodedMethod target, IRCode inlinee);
+
ListIterator<BasicBlock> updateTypeInformationIfNeeded(IRCode inlinee,
ListIterator<BasicBlock> blockIterator, BasicBlock block, BasicBlock invokeSuccessor);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index 9ff44eb..7bdff33 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -9,68 +9,75 @@
import com.android.tools.r8.graph.AppInfoWithSubtyping;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstNumber;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.InstanceGet;
import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.ValueType;
import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
import com.android.tools.r8.ir.optimize.Inliner.Reason;
import com.google.common.collect.Streams;
-import java.util.Collections;
+import java.util.ArrayList;
import java.util.IdentityHashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.Set;
+import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public final class ClassInliner {
private final DexItemFactory factory;
+ private final int totalMethodInstructionLimit;
private final ConcurrentHashMap<DexType, Boolean> knownClasses = new ConcurrentHashMap<>();
- private static final Map<DexField, Integer> NO_MAPPING = new IdentityHashMap<>();
-
public interface InlinerAction {
void inline(Map<InvokeMethodWithReceiver, InliningInfo> methods);
}
- public ClassInliner(DexItemFactory factory) {
+ public ClassInliner(DexItemFactory factory, int totalMethodInstructionLimit) {
this.factory = factory;
+ this.totalMethodInstructionLimit = totalMethodInstructionLimit;
}
// Process method code and inline eligible class instantiations, in short:
//
// - collect all 'new-instance' instructions in the original code. Note that class
- // inlining, if happens, mutates code and can add 'new-instance' instructions.
- // Processing them as well is possible, but does not seem to have much value.
+ // inlining, if happens, mutates code and may add new 'new-instance' instructions.
+ // Processing them as well is possible, but does not seem to bring much value.
//
// - for each 'new-instance' we check if it is eligible for inlining, i.e:
// -> the class of the new instance is 'eligible' (see computeClassEligible(...))
- // -> the instance is initialized with 'eligible' constructor (see
- // onlyInitializesFieldsWithNoOtherSideEffects flag in method's optimization
- // info); eligible constructor also defines a set of instance fields directly
- // initialized with parameter values, called field initialization mapping below
+ // -> the instance is initialized with 'eligible' constructor (see comments in
+ // CodeRewriter::identifyClassInlinerEligibility(...))
// -> has only 'eligible' uses, i.e:
- // * it is a receiver of a field read if the field is present in the
- // field initialization mapping
- // * it is a receiver of virtual or interface call with single target being
- // a method only reading fields in the current field initialization mapping
+ // * as a receiver of a field read/write for a field defined in same class
+ // as method.
+ // * as a receiver of virtual or interface call with single target being
+ // an eligible method according to identifyClassInlinerEligibility(...);
+ // NOTE: if method receiver is used as a return value, the method call
+ // should ignore return value
//
// - inline eligible 'new-instance' instructions, i.e:
- // -> force inline methods called on the instance (which may introduce additional
- // instance field reads, but only for fields present in the current field
- // initialization mapping)
- // -> replace instance field reads with appropriate values passed to the constructor
- // according to field initialization mapping
- // -> remove constructor call
+ // -> force inline methods called on the instance (including the initializer);
+ // (this may introduce additional instance field reads/writes on the receiver)
+ // -> replace instance field reads with appropriate values calculated based on
+ // fields writes
+ // -> remove the call to superclass initializer
+ // -> remove all field writes
// -> remove 'new-instance' instructions
//
// For example:
@@ -117,7 +124,6 @@
.map(Instruction::asNewInstance)
.collect(Collectors.toList());
- nextNewInstance:
for (NewInstance newInstance : newInstances) {
Value eligibleInstance = newInstance.outValue();
if (eligibleInstance == null) {
@@ -129,177 +135,281 @@
continue;
}
- // No Phi users.
- if (eligibleInstance.numberOfPhiUsers() > 0) {
+ Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = checkInstanceUsersEligibility(
+ appInfo, method, isProcessedConcurrently, newInstance, eligibleInstance, eligibleClass);
+ if (methodCalls == null) {
continue;
}
- Set<Instruction> uniqueUsers = eligibleInstance.uniqueUsers();
+ if (getTotalEstimatedMethodSize(methodCalls) >= totalMethodInstructionLimit) {
+ continue;
+ }
- // Find an initializer invocation.
- InvokeDirect eligibleInitCall = null;
- Map<DexField, Integer> mappings = null;
- for (Instruction user : uniqueUsers) {
- if (!user.isInvokeDirect()) {
+ // Inline the class instance.
+ forceInlineAllMethodInvocations(inliner, methodCalls);
+ removeSuperClassInitializerAndFieldReads(code, newInstance, eligibleInstance);
+ removeFieldWrites(eligibleInstance, eligibleClass);
+ removeInstruction(newInstance);
+
+ // Restore normality.
+ code.removeAllTrivialPhis();
+ assert code.isConsistentSSA();
+ }
+ }
+
+ private Map<InvokeMethodWithReceiver, InliningInfo> checkInstanceUsersEligibility(
+ AppInfoWithSubtyping appInfo, DexEncodedMethod method,
+ Predicate<DexEncodedMethod> isProcessedConcurrently,
+ NewInstance newInstanceInsn, Value receiver, DexType clazz) {
+
+ // No Phi users.
+ if (receiver.numberOfPhiUsers() > 0) {
+ return null; // Not eligible.
+ }
+
+ Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
+
+ for (Instruction user : receiver.uniqueUsers()) {
+ // Field read/write.
+ if (user.isInstanceGet() ||
+ (user.isInstancePut() && user.asInstancePut().value() != receiver)) {
+ if (user.asFieldInstruction().getField().clazz == newInstanceInsn.clazz) {
+ // Eligible field read or write. Note: as long as we consider only classes eligible
+ // if they directly extend java.lang.Object we don't need to check if the field
+ // really exists in the class.
continue;
}
+ return null; // Not eligible.
+ }
- InvokeDirect candidate = user.asInvokeDirect();
- DexMethod candidateInit = candidate.getInvokedMethod();
- if (factory.isConstructor(candidateInit) &&
- candidate.inValues().lastIndexOf(eligibleInstance) == 0) {
+ // Eligible constructor call.
+ if (user.isInvokeDirect()) {
+ InliningInfo inliningInfo = isEligibleConstructorCall(appInfo, method,
+ user.asInvokeDirect(), receiver, clazz, isProcessedConcurrently);
+ if (inliningInfo != null) {
+ methodCalls.put(user.asInvokeDirect(), inliningInfo);
+ continue;
+ }
+ return null; // Not eligible.
+ }
- if (candidateInit.holder != eligibleClass) {
- // Inlined constructor call? We won't get field initialization mapping in this
- // case, but since we only support eligible classes extending java.lang.Object,
- // it's safe to assume an empty mapping.
- if (candidateInit.holder == factory.objectType) {
- mappings = Collections.emptyMap();
- }
+ // Eligible virtual method call.
+ if (user.isInvokeVirtual() || user.isInvokeInterface()) {
+ InliningInfo inliningInfo = isEligibleMethodCall(
+ appInfo, method, user.asInvokeMethodWithReceiver(),
+ receiver, clazz, isProcessedConcurrently);
+ if (inliningInfo != null) {
+ methodCalls.put(user.asInvokeMethodWithReceiver(), inliningInfo);
+ continue;
+ }
+ return null; // Not eligible.
+ }
- } else {
- // Is it a call to an *eligible* constructor?
- mappings = getConstructorFieldMappings(appInfo, candidateInit, isProcessedConcurrently);
- }
+ return null; // Not eligible.
+ }
+ return methodCalls;
+ }
- eligibleInitCall = candidate;
+ // Remove call to superclass initializer, replace field reads with appropriate
+ // values, insert phis when needed.
+ private void removeSuperClassInitializerAndFieldReads(
+ IRCode code, NewInstance newInstance, Value eligibleInstance) {
+ Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
+ for (Instruction user : eligibleInstance.uniqueUsers()) {
+ // Remove the call to superclass constructor.
+ if (user.isInvokeDirect() &&
+ user.asInvokeDirect().getInvokedMethod() == factory.objectMethods.constructor) {
+ removeInstruction(user);
+ continue;
+ }
+
+ if (user.isInstanceGet()) {
+ // Replace a field read with appropriate value.
+ replaceFieldRead(code, newInstance, user.asInstanceGet(), fieldHelpers);
+ continue;
+ }
+
+ if (user.isInstancePut()) {
+ // Skip in this iteration since these instructions are needed to
+ // properly calculate what value should field reads be replaced with.
+ continue;
+ }
+
+ throw new Unreachable("Unexpected usage left after method inlining: " + user);
+ }
+ }
+
+ private void removeFieldWrites(Value receiver, DexType clazz) {
+ for (Instruction user : receiver.uniqueUsers()) {
+ if (!user.isInstancePut()) {
+ throw new Unreachable("Unexpected usage left after field reads removed: " + user);
+ }
+ if (user.asInstancePut().getField().clazz != clazz) {
+ throw new Unreachable("Unexpected field write left after field reads removed: " + user);
+ }
+ removeInstruction(user);
+ }
+ }
+
+ private int getTotalEstimatedMethodSize(Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) {
+ int totalSize = 0;
+ for (InliningInfo info : methodCalls.values()) {
+ totalSize += info.target.getCode().estimatedSizeForInlining();
+ }
+ return totalSize;
+ }
+
+ private void replaceFieldRead(IRCode code, NewInstance newInstance,
+ InstanceGet fieldRead, Map<DexField, FieldValueHelper> fieldHelpers) {
+
+ Value value = fieldRead.outValue();
+ if (value != null) {
+ FieldValueHelper helper = fieldHelpers.computeIfAbsent(
+ fieldRead.getField(), field -> new FieldValueHelper(field, code, newInstance));
+ Value newValue = helper.getValueForFieldRead(fieldRead.getBlock(), fieldRead);
+ value.replaceUsers(newValue);
+ for (FieldValueHelper fieldValueHelper : fieldHelpers.values()) {
+ fieldValueHelper.replaceValue(value, newValue);
+ }
+ assert value.numberOfAllUsers() == 0;
+ }
+ removeInstruction(fieldRead);
+ }
+
+ // Describes and caches what values are supposed to be used instead of field reads.
+ private static final class FieldValueHelper {
+ final DexField field;
+ final IRCode code;
+ final NewInstance newInstance;
+
+ private Value defaultValue = null;
+ private final Map<BasicBlock, Value> ins = new IdentityHashMap<>();
+ private final Map<BasicBlock, Value> outs = new IdentityHashMap<>();
+
+ private FieldValueHelper(DexField field, IRCode code, NewInstance newInstance) {
+ this.field = field;
+ this.code = code;
+ this.newInstance = newInstance;
+ }
+
+ void replaceValue(Value oldValue, Value newValue) {
+ for (Entry<BasicBlock, Value> entry : ins.entrySet()) {
+ if (entry.getValue() == oldValue) {
+ entry.setValue(newValue);
+ }
+ }
+ for (Entry<BasicBlock, Value> entry : outs.entrySet()) {
+ if (entry.getValue() == oldValue) {
+ entry.setValue(newValue);
+ }
+ }
+ }
+
+ Value getValueForFieldRead(BasicBlock block, Instruction valueUser) {
+ assert valueUser != null;
+ Value value = getValueDefinedInTheBlock(block, valueUser);
+ return value != null ? value : getOrCreateInValue(block);
+ }
+
+ private Value getOrCreateOutValue(BasicBlock block) {
+ Value value = outs.get(block);
+ if (value != null) {
+ return value;
+ }
+
+ value = getValueDefinedInTheBlock(block, null);
+ if (value == null) {
+ // No value defined in the block.
+ value = getOrCreateInValue(block);
+ }
+
+ assert value != null;
+ outs.put(block, value);
+ return value;
+ }
+
+ private Value getOrCreateInValue(BasicBlock block) {
+ Value value = ins.get(block);
+ if (value != null) {
+ return value;
+ }
+
+ List<BasicBlock> predecessors = block.getPredecessors();
+ if (predecessors.size() == 1) {
+ value = getOrCreateOutValue(predecessors.get(0));
+ ins.put(block, value);
+ } else {
+ // Create phi, add it to the block, cache in ins map for future use.
+ Phi phi = new Phi(code.valueNumberGenerator.next(),
+ block, ValueType.fromDexType(field.type), null);
+ ins.put(block, phi);
+
+ List<Value> operands = new ArrayList<>();
+ for (BasicBlock predecessor : block.getPredecessors()) {
+ operands.add(getOrCreateOutValue(predecessor));
+ }
+ // Add phi, but don't remove trivial phis; since we cache the phi
+ // we just created for future use we should delay removing trivial
+ // phis until we are done with replacing fields reads.
+ phi.addOperands(operands, false);
+ value = phi;
+ }
+
+ assert value != null;
+ return value;
+ }
+
+ private Value getValueDefinedInTheBlock(BasicBlock block, Instruction stopAt) {
+ InstructionListIterator iterator = stopAt == null ?
+ block.listIterator(block.getInstructions().size()) : block.listIterator(stopAt);
+
+ Instruction valueProducingInsn = null;
+ while (iterator.hasPrevious()) {
+ Instruction instruction = iterator.previous();
+ assert instruction != null;
+
+ if (instruction == newInstance ||
+ (instruction.isInstancePut() &&
+ instruction.asInstancePut().getField() == field &&
+ instruction.asInstancePut().object() == newInstance.outValue())) {
+ valueProducingInsn = instruction;
break;
}
}
- if (mappings == null) {
- continue;
+ if (valueProducingInsn == null) {
+ return null;
+ }
+ if (valueProducingInsn.isInstancePut()) {
+ return valueProducingInsn.asInstancePut().value();
}
- // Check all regular users.
- Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
-
- for (Instruction user : uniqueUsers) {
- if (user == eligibleInitCall) {
- continue /* next user */;
- }
-
- if (user.isInstanceGet()) {
- InstanceGet instanceGet = user.asInstanceGet();
- if (mappings.containsKey(instanceGet.getField())) {
- continue /* next user */;
- }
-
- // Not replaceable field read.
- continue nextNewInstance;
- }
-
- if (user.isInvokeVirtual() || user.isInvokeInterface()) {
- InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
- if (invoke.inValues().lastIndexOf(eligibleInstance) > 0) {
- continue nextNewInstance; // Instance must only be passes as a receiver.
- }
-
- DexEncodedMethod singleTarget =
- findSingleTarget(appInfo, invoke, eligibleClass);
- if (singleTarget == null) {
- continue nextNewInstance;
- }
- if (isProcessedConcurrently.test(singleTarget)) {
- continue nextNewInstance;
- }
- if (method == singleTarget) {
- continue nextNewInstance; // Don't inline itself.
- }
-
- if (!singleTarget.getOptimizationInfo()
- .isReceiverOnlyUsedForReadingFields(mappings.keySet())) {
- continue nextNewInstance; // Target must be trivial.
- }
-
- if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
- // We won't be able to inline it here.
-
- // Note that there may be some false negatives here since the method may
- // reference private fields of its class which are supposed to be replaced
- // with arguments after inlining. We should try and improve it later.
-
- // Using -allowaccessmodification mitigates this.
- continue nextNewInstance;
- }
-
- methodCalls.put(invoke, new InliningInfo(singleTarget, eligibleClass));
- continue /* next user */;
- }
-
- continue nextNewInstance; // Unsupported user.
+ assert newInstance == valueProducingInsn;
+ if (defaultValue == null) {
+ // If we met newInstance it means that default value is supposed to be used.
+ defaultValue = code.createValue(ValueType.fromDexType(field.type));
+ ConstNumber defaultValueInsn = new ConstNumber(defaultValue, 0);
+ defaultValueInsn.setPosition(newInstance.getPosition());
+ LinkedList<Instruction> instructions = block.getInstructions();
+ instructions.add(instructions.indexOf(newInstance) + 1, defaultValueInsn);
+ defaultValueInsn.setBlock(block);
}
-
- // Force-inline of method invocation if any.
- inlineAllCalls(inliner, methodCalls);
- assert assertOnlyConstructorAndFieldReads(eligibleInstance, eligibleInitCall, mappings);
-
- // Replace all field reads with arguments passed to the constructor.
- patchFieldReads(eligibleInstance, eligibleInitCall, mappings);
- assert assertOnlyConstructor(eligibleInstance, eligibleInitCall);
-
- // Remove constructor call and new-instance instructions.
- removeInstruction(eligibleInitCall);
- removeInstruction(newInstance);
- code.removeAllTrivialPhis();
+ return defaultValue;
}
}
- private void inlineAllCalls(
+ private void forceInlineAllMethodInvocations(
InlinerAction inliner, Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) {
if (!methodCalls.isEmpty()) {
inliner.inline(methodCalls);
}
}
- private void patchFieldReads(
- Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
- for (Instruction user : instance.uniqueUsers()) {
- if (!user.isInstanceGet()) {
- continue;
- }
- InstanceGet fieldRead = user.asInstanceGet();
-
- // Replace the field read with
- assert mappings.containsKey(fieldRead.getField());
- Value arg = invokeMethod.inValues().get(1 + mappings.get(fieldRead.getField()));
- assert arg != null;
- Value value = fieldRead.outValue();
- if (value != null) {
- value.replaceUsers(arg);
- assert value.numberOfAllUsers() == 0;
- }
-
- // Remove instruction.
- removeInstruction(fieldRead);
- }
- }
-
private void removeInstruction(Instruction instruction) {
instruction.inValues().forEach(v -> v.removeUser(instruction));
instruction.getBlock().removeInstruction(instruction);
}
- private boolean assertOnlyConstructorAndFieldReads(
- Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
- for (Instruction user : instance.uniqueUsers()) {
- if (user != invokeMethod &&
- !(user.isInstanceGet() && mappings.containsKey(user.asFieldInstruction().getField()))) {
- throw new Unreachable("Not all calls are inlined!");
- }
- }
- return true;
- }
-
- private boolean assertOnlyConstructor(Value instance, InvokeDirect invokeMethod) {
- for (Instruction user : instance.uniqueUsers()) {
- if (user != invokeMethod) {
- throw new Unreachable("Not all field reads are substituted!");
- }
- }
- return true;
- }
-
private DexEncodedMethod findSingleTarget(
AppInfo appInfo, InvokeMethodWithReceiver invoke, DexType instanceType) {
@@ -322,24 +432,94 @@
return null;
}
- private Map<DexField, Integer> getConstructorFieldMappings(
- AppInfo appInfo, DexMethod init, Predicate<DexEncodedMethod> isProcessedConcurrently) {
- assert isClassEligible(appInfo, init.holder);
+ private InliningInfo isEligibleConstructorCall(
+ AppInfoWithSubtyping appInfo,
+ DexEncodedMethod method,
+ InvokeDirect initInvoke,
+ Value receiver,
+ DexType inlinedClass,
+ Predicate<DexEncodedMethod> isProcessedConcurrently) {
+
+ // Must be a constructor of the exact same class.
+ DexMethod init = initInvoke.getInvokedMethod();
+ if (!factory.isConstructor(init)) {
+ return null;
+ }
+ // Must be a constructor called on the receiver.
+ if (initInvoke.inValues().lastIndexOf(receiver) != 0) {
+ return null;
+ }
+
+ assert init.holder == inlinedClass
+ : "Inlined constructor? [invoke: " + initInvoke +
+ ", expected class: " + inlinedClass + "]";
DexEncodedMethod definition = appInfo.definitionFor(init);
- if (definition == null) {
- return NO_MAPPING;
+ if (definition == null || isProcessedConcurrently.test(definition)) {
+ return null;
}
- if (isProcessedConcurrently.test(definition)) {
- return NO_MAPPING;
+ if (!definition.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
+ // We won't be able to inline it here.
+
+ // Note that there may be some false negatives here since the method may
+ // reference private fields of its class which are supposed to be replaced
+ // with arguments after inlining. We should try and improve it later.
+
+ // Using -allowaccessmodification mitigates this.
+ return null;
}
- if (definition.accessFlags.isAbstract() || definition.accessFlags.isNative()) {
- return NO_MAPPING;
+ return definition.getOptimizationInfo().getClassInlinerEligibility() != null
+ ? new InliningInfo(definition, inlinedClass) : null;
+ }
+
+ private InliningInfo isEligibleMethodCall(
+ AppInfoWithSubtyping appInfo,
+ DexEncodedMethod method,
+ InvokeMethodWithReceiver invoke,
+ Value receiver,
+ DexType inlinedClass,
+ Predicate<DexEncodedMethod> isProcessedConcurrently) {
+
+ if (invoke.inValues().lastIndexOf(receiver) > 0) {
+ return null; // Instance passed as an argument.
}
- return definition.getOptimizationInfo().onlyInitializesFieldsWithNoOtherSideEffects();
+ DexEncodedMethod singleTarget =
+ findSingleTarget(appInfo, invoke, inlinedClass);
+ if (singleTarget == null || isProcessedConcurrently.test(singleTarget)) {
+ return null;
+ }
+ if (method == singleTarget) {
+ return null; // Don't inline itself.
+ }
+
+ ClassInlinerEligibility eligibility =
+ singleTarget.getOptimizationInfo().getClassInlinerEligibility();
+ if (eligibility == null) {
+ return null;
+ }
+
+ // If the method returns receiver and the return value is actually
+ // used in the code the method is not eligible.
+ if (eligibility.returnsReceiver &&
+ invoke.outValue() != null && invoke.outValue().numberOfAllUsers() > 0) {
+ return null;
+ }
+
+ if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
+ // We won't be able to inline it here.
+
+ // Note that there may be some false negatives here since the method may
+ // reference private fields of its class which are supposed to be replaced
+ // with arguments after inlining. We should try and improve it later.
+
+ // Using -allowaccessmodification mitigates this.
+ return null;
+ }
+
+ return new InliningInfo(singleTarget, inlinedClass);
}
private boolean isClassEligible(AppInfo appInfo, DexType clazz) {
@@ -354,7 +534,7 @@
}
// Class is eligible for this optimization. Eligibility implementation:
- // - not an abstract or interface
+ // - is not an abstract class or interface
// - directly extends java.lang.Object
// - does not declare finalizer
// - does not trigger any static initializers
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 0976087..d2c7701 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -1819,6 +1819,10 @@
// we allow ourselves to look at monitor spill candidates as well. Registers holding objects
// used as monitors should not be spilled if we can avoid it. Spilling them can lead
// to Art lock verification issues.
+ // Also, at this point we still don't allow splitting any string new-instance instructions
+ // that have been explicitly blocked. Doing so could lead to a behavioral bug on some ART
+ // runtimes (b/80118070). To remove this restriction, we would need to know when the call to
+ // <init> has definitely happened, and would be safe to split the value after that point.
if (candidate == REGISTER_CANDIDATE_NOT_FOUND) {
candidate = getLargestValidCandidate(
unhandledInterval, registerConstraint, needsRegisterPair, usePositions, Type.MONITOR);
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
index c8e6e4c..bf65310 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
@@ -522,6 +522,13 @@
return usedInMonitorOperations;
}
+ public boolean isNewStringInstanceDisallowingSpilling() {
+ // Due to b/80118070 some String new-instances must not be spilled.
+ return value.definition != null
+ && value.definition.isNewInstance()
+ && !value.definition.asNewInstance().isSpillingAllowed();
+ }
+
public int numberOfUsesWithConstraint() {
int count = 0;
for (LiveIntervalsUse use : getUses()) {
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
index 459114b..eff8bea 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterPositions.java
@@ -23,6 +23,7 @@
private int[] backing;
private final BitSet registerHoldsConstant;
private final BitSet registerHoldsMonitor;
+ private final BitSet registerHoldsNewStringInstanceDisallowingSpilling;
public RegisterPositions(int limit) {
this.limit = limit;
@@ -32,6 +33,7 @@
}
registerHoldsConstant = new BitSet(limit);
registerHoldsMonitor = new BitSet(limit);
+ registerHoldsNewStringInstanceDisallowingSpilling = new BitSet(limit);
}
public boolean hasType(int index, Type type) {
@@ -41,7 +43,9 @@
case CONST_NUMBER:
return holdsConstant(index);
case OTHER:
- return !holdsMonitor(index) && !holdsConstant(index);
+ return !holdsMonitor(index)
+ && !holdsConstant(index)
+ && !holdsNewStringInstanceDisallowingSpilling(index);
case ANY:
return true;
default:
@@ -55,6 +59,10 @@
private boolean holdsMonitor(int index) { return registerHoldsMonitor.get(index); }
+ private boolean holdsNewStringInstanceDisallowingSpilling(int index) {
+ return registerHoldsNewStringInstanceDisallowingSpilling.get(index);
+ }
+
public void set(int index, int value) {
if (index >= backing.length) {
grow(index + 1);
@@ -66,6 +74,8 @@
set(index, value);
registerHoldsConstant.set(index, intervals.isConstantNumberInterval());
registerHoldsMonitor.set(index, intervals.usedInMonitorOperation());
+ registerHoldsNewStringInstanceDisallowingSpilling.set(
+ index, intervals.isNewStringInstanceDisallowingSpilling());
}
public int get(int index) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 68796ac..5f0ec45 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -101,6 +101,7 @@
public boolean enableInlining =
!Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null;
public boolean enableClassInlining = true;
+ public int classInliningInstructionLimit = 50;
public int inliningInstructionLimit = 5;
public boolean enableSwitchMapRemoval = true;
public final OutlineOptions outline = new OutlineOptions();
@@ -620,4 +621,12 @@
public boolean canHaveDex2OatLinkedListBug() {
return minApiLevel < AndroidApiLevel.N.getLevel();
}
+
+ // Art 7.0.0 and later Art JIT may perform an invalid optimization if a string new-instance does
+ // not flow directly to the init call.
+ //
+ // See b/78493232 and b/80118070.
+ public boolean canHaveArtStringNewInitBug() {
+ return minApiLevel <= AndroidApiLevel.P.getLevel();
+ }
}
diff --git a/src/test/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index 9cc2b48..8038c98 100644
--- a/src/test/java/com/android/tools/r8/AsmTestBase.java
+++ b/src/test/java/com/android/tools/r8/AsmTestBase.java
@@ -64,6 +64,10 @@
Assert.assertEquals(javaResult.stdout, d8Result.stdout);
Assert.assertEquals(javaResult.stdout, r8Result.stdout);
Assert.assertEquals(javaResult.stdout, r8ShakenResult.stdout);
+ Assert.assertEquals(0, javaResult.exitCode);
+ Assert.assertEquals(0, d8Result.exitCode);
+ Assert.assertEquals(0, r8Result.exitCode);
+ Assert.assertEquals(0, r8ShakenResult.exitCode);
}
protected void ensureR8FailsWithCompilationError(String main, byte[]... classes)
diff --git a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
index ede7e53..e01373c 100644
--- a/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/D8RunExamplesAndroidOTest.java
@@ -15,6 +15,7 @@
import org.hamcrest.core.IsInstanceOf;
import org.hamcrest.core.StringContains;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Test;
import org.junit.internal.matchers.ThrowableMessageMatcher;
@@ -156,16 +157,20 @@
// TODO check compilation warnings are correctly reported
// Missing interface B is causing the wrong code to be executed.
- if (ToolHelper.artSupported()) {
- thrown.expect(AssertionError.class);
- execute(
- "testMissingInterfaceDesugared2AndroidK",
- "desugaringwithmissingclasstest2.Main",
- new Path[] {
- lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
- },
- new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+ if (!ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
+ return;
}
+ if (ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
+ thrown.expect(AssertionError.class);
+ }
+ execute(
+ "testMissingInterfaceDesugared2AndroidK",
+ "desugaringwithmissingclasstest2.Main",
+ new Path[] {
+ lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
+ },
+ new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+
}
@Test
@@ -256,17 +261,20 @@
Path testDex = test.build();
// TODO check compilation warnings are correctly reported
+ Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
// Missing interface B is causing the wrong method to be executed.
- if (ToolHelper.artSupported()) {
+ if (ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
thrown.expect(AssertionError.class);
- execute(
- "testCallToMissingSuperInterfaceDesugaredAndroidK",
- "desugaringwithmissingclasstest3.Main",
- new Path[] {
- lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
- },
- new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
}
+ execute(
+ "testCallToMissingSuperInterfaceDesugaredAndroidK",
+ "desugaringwithmissingclasstest3.Main",
+ new Path[] {
+ lib1.getInputJar(), lib2.getInputJar(), lib3.getInputJar(), test.getInputJar()
+ },
+ new Path[] {lib1Dex, lib2Dex, lib3Dex, testDex});
+
}
@Test
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index ca6feed..2032866 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.ToolHelper.DexVm.Kind;
import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dex.Marker.Tool;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.shaking.ProguardRuleParserException;
@@ -22,6 +23,7 @@
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
@@ -1010,8 +1012,7 @@
// Test depends on exception produced for missing method or similar cases, but
// after class inlining removes class instantiations and references the exception
// is not produced.
- "042-new-instance",
- "075-verification-error"
+ "435-new-instance"
);
private static List<String> hasMissingClasses = ImmutableList.of(
@@ -1363,6 +1364,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
public R8RunArtTestsTest(String name, DexTool toolchain) {
this.name = name;
this.toolchain = toolchain;
@@ -1554,6 +1558,8 @@
if (disableClassInlining) {
options.enableClassInlining = false;
}
+ // Make sure we don't depend on this settings.
+ options.classInliningInstructionLimit = 10000;
options.lineNumberOptimization = LineNumberOptimization.OFF;
// Some tests actually rely on missing classes for what they test.
options.ignoreMissingClasses = hasMissingClasses;
@@ -1768,7 +1774,7 @@
specification.disableInlining, specification.disableClassInlining,
specification.hasMissingClasses);
- if (!ToolHelper.artSupported()) {
+ if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
return;
}
@@ -1789,9 +1795,11 @@
boolean compileOnly = System.getProperty("jctf_compile_only", "0").equals("1");
if (compileOnly || specification.skipArt) {
- // verify dex code instead of running it
- Path oatFile = temp.getRoot().toPath().resolve("all.oat");
- ToolHelper.runDex2Oat(processedFile.toPath(), oatFile);
+ if (ToolHelper.isDex2OatSupported()) {
+ // verify dex code instead of running it
+ Path oatFile = temp.getRoot().toPath().resolve("all.oat");
+ ToolHelper.runDex2Oat(processedFile.toPath(), oatFile);
+ }
return;
}
@@ -1963,7 +1971,7 @@
specification.hasMissingClasses);
}
- if (!specification.skipArt && ToolHelper.artSupported()) {
+ if (!specification.skipArt && (ToolHelper.artSupported() || ToolHelper.dealsWithGoldenFiles())) {
File originalFile;
File processedFile;
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index 62dd4af..fe5ae72 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.utils.ExceptionUtils;
import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
@@ -21,6 +22,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -69,6 +71,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
private final Input input;
private final CompilerUnderTest compiler;
private final CompilationMode mode;
@@ -183,9 +188,8 @@
@Test
public void outputIsIdentical() throws IOException, InterruptedException, ExecutionException {
- if (!ToolHelper.artSupported()) {
- return;
- }
+ Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
DexVm vm = ToolHelper.getDexVm();
diff --git a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
index fc373d2..0f3cadc 100644
--- a/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunSmaliTestsTest.java
@@ -8,6 +8,7 @@
import com.android.tools.r8.ToolHelper.DexVm;
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.nio.file.Path;
@@ -91,6 +92,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
@Parameters(name = "{0}")
public static Collection<String[]> data() {
return Arrays.asList(new String[][]{
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
index bb8c686..83df845 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidNTest.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.nio.file.Path;
@@ -73,7 +74,7 @@
build(inputFile, out);
- if (!ToolHelper.artSupported()) {
+ if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
return;
}
@@ -118,6 +119,8 @@
@Rule public ExpectedException thrown = ExpectedException.none();
+ @Rule public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
@Test
public void staticInterfaceMethods() throws Throwable {
test("staticinterfacemethods", "interfacemethods", "StaticInterfaceMethods")
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
index 9fdbf0a..2c6a72a 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidOTest.java
@@ -21,6 +21,7 @@
import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
@@ -47,6 +48,7 @@
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestWatcher;
public abstract class RunExamplesAndroidOTest
<B extends BaseCommand.Builder<? extends BaseCommand, B>> {
@@ -148,7 +150,7 @@
build(inputFile, out);
- if (!ToolHelper.artSupported()) {
+ if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
return;
}
@@ -270,6 +272,9 @@
@Rule
public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
boolean failsOn(Map<ToolHelper.DexVm.Version, List<String>> failsOn, String name) {
Version vmVersion = ToolHelper.getDexVm().getVersion();
return failsOn.containsKey(vmVersion)
@@ -553,14 +558,14 @@
throws IOException {
boolean expectedToFail = expectedToFail(testName);
- if (expectedToFail) {
+ if (expectedToFail && !ToolHelper.compareAgaintsGoldenFiles()) {
thrown.expect(Throwable.class);
}
String output = ToolHelper.runArtNoVerificationErrors(
Arrays.stream(dexes).map(path -> path.toString()).collect(Collectors.toList()),
qualifiedMainClass,
null);
- if (!expectedToFail && !skipRunningOnJvm(testName)) {
+ if (!expectedToFail && !skipRunningOnJvm(testName) && !ToolHelper.compareAgaintsGoldenFiles()) {
ToolHelper.ProcessResult javaResult =
ToolHelper.runJava(ImmutableList.copyOf(jars), qualifiedMainClass);
assertEquals("JVM run failed", javaResult.exitCode, 0);
diff --git a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
index bc898bd..182fe80 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesAndroidPTest.java
@@ -19,6 +19,7 @@
import com.android.tools.r8.utils.DexInspector.InstructionSubject;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
@@ -143,7 +144,7 @@
build(inputFile, out);
- if (!ToolHelper.artSupported()) {
+ if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
return;
}
@@ -193,6 +194,9 @@
@Rule
public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
boolean failsOn(Map<DexVm.Version, List<String>> failsOn, String name) {
DexVm.Version vmVersion = ToolHelper.getDexVm().getVersion();
return failsOn.containsKey(vmVersion)
diff --git a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
index 281b0da..284ed68 100644
--- a/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
+++ b/src/test/java/com/android/tools/r8/RunExamplesJava9Test.java
@@ -19,6 +19,7 @@
import com.android.tools.r8.utils.DexInspector.InstructionSubject;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.OffOrAuto;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
@@ -143,7 +144,7 @@
build(inputFile, out);
- if (!ToolHelper.artSupported()) {
+ if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
return;
}
@@ -224,6 +225,9 @@
@Rule
public ExpectedException thrown = ExpectedException.none();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
boolean failsOn(Map<DexVm.Version, List<String>> failsOn, String name) {
DexVm.Version vmVersion = ToolHelper.getDexVm().getVersion();
return failsOn.containsKey(vmVersion)
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 331af0a..add86c2 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -26,6 +26,7 @@
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.PreloadedClassFileProvider;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
@@ -59,6 +60,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
/**
* Check if tests should also run Proguard when applicable.
*/
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 0a4416a..000076e 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -23,18 +23,23 @@
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.AndroidAppConsumers;
import com.android.tools.r8.utils.DefaultDiagnosticsHandler;
+import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.Reporter;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
+import com.google.gson.Gson;
import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -702,6 +707,10 @@
return true;
}
+ public static boolean isDex2OatSupported() {
+ return !isWindows();
+ }
+
public static Path getClassPathForTests() {
return Paths.get(BUILD_DIR, "classes", "test");
}
@@ -1021,17 +1030,197 @@
return runArtRaw(files, mainClass, extras, null);
}
+ // Index used to name directory aimed at storing dex files and process result
+ // for one invokation of runArtRaw() in order to avoid conflicts in case of
+ // multiple calls within the same test.
+ private static int testOutputPathIndex = 0;
+
public static ProcessResult runArtRaw(List<String> files, String mainClass,
Consumer<ArtCommandBuilder> extras, DexVm version)
throws IOException {
- ArtCommandBuilder builder =
- version != null ? new ArtCommandBuilder(version) : new ArtCommandBuilder();
- files.forEach(builder::appendClasspath);
- builder.setMainClass(mainClass);
- if (extras != null) {
- extras.accept(builder);
+
+ ArtCommandBuilder builder =
+ version != null ? new ArtCommandBuilder(version) : new ArtCommandBuilder();
+ files.forEach(builder::appendClasspath);
+ builder.setMainClass(mainClass);
+ if (extras != null) {
+ extras.accept(builder);
+ }
+
+ ProcessResult processResult = null;
+
+ // Whenever we start a new test method we reset the index count.
+ String reset_output_index = System.getProperty("reset_output_index");
+ if (reset_output_index != null) {
+ System.clearProperty("reset_output_index");
+ testOutputPathIndex = 0;
+ } else {
+ assert testOutputPathIndex >= 0;
+ testOutputPathIndex++;
+ }
+
+ String goldenFilesDirInProp = System.getProperty("use_golden_files_in");
+ if (goldenFilesDirInProp != null) {
+ File goldenFileDir = new File(goldenFilesDirInProp);
+ assert goldenFileDir.isDirectory();
+ processResult = compareAgainstGoldenFiles(
+ files.stream().map(f -> new File(f)).collect(Collectors.toList()), goldenFileDir);
+ if (processResult.exitCode == 0) {
+ processResult = readProcessResult(goldenFileDir);
+ }
+ } else {
+ processResult = runArtProcessRaw(builder);
+ }
+
+ String goldenFilesDirToProp = System.getProperty("generate_golden_files_to");
+ if (goldenFilesDirToProp != null) {
+ File goldenFileDir = new File(goldenFilesDirToProp);
+ assert goldenFileDir.isDirectory();
+ storeAsGoldenFiles(files.stream().map(f -> new File(f)).collect(Collectors.toList()),
+ goldenFileDir);
+ storeProcessResult(processResult, goldenFileDir);
+ }
+
+ return processResult;
+ }
+
+ private static Path findNonConflictingDestinationFilePath(Path testOutputPath) {
+ int index = 0;
+ Path destFilePath;
+ do {
+ destFilePath = Paths.get(testOutputPath.toString(),
+ "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION);
+ index++;
+ } while (destFilePath.toFile().exists());
+
+ return destFilePath;
+ }
+
+ private static Path getTestOutputPath(File destDir) throws IOException {
+ assert destDir.exists();
+ assert destDir.isDirectory();
+
+ String testClassName = System.getProperty("test_class_name");
+ String testName = System.getProperty("test_name");
+ String headSha1 = System.getProperty("test_git_HEAD_sha1");
+
+ assert testClassName != null;
+ assert testName != null;
+ assert headSha1 != null;
+
+ return Files.createDirectories(
+ Paths.get(destDir.getAbsolutePath(), headSha1, testClassName, testName + "-" + String
+ .format("%03d", testOutputPathIndex)));
+ }
+
+ private static List<File> unzipDexFilesArchive(File zipFile) throws IOException {
+ File tmpDir = Files.createTempDirectory("r8-test-").toFile();
+ tmpDir.deleteOnExit();
+ return ZipUtils.unzip(zipFile.getAbsolutePath(), tmpDir);
+ }
+
+ private static void storeAsGoldenFiles(List<File> files, File destDir) throws IOException {
+ Path testOutputPath = getTestOutputPath(destDir);
+
+ for (File f : files) {
+ Path filePath = f.toPath();
+ // TODO(jmhenaff): Check it's been produced by D8/R8?
+ List<File> testFiles = Collections.singletonList(f);
+ if (FileUtils.isArchive(filePath)) {
+ testFiles = unzipDexFilesArchive(f);
+ }
+ for (File testFile : testFiles) {
+ Path testFilePath = testFile.toPath();
+ if (FileUtils.isDexFile(testFilePath)) {
+ Path destFile = findNonConflictingDestinationFilePath(testOutputPath);
+ FileUtils.writeToFile(destFile, null, Files.readAllBytes(testFilePath));
+ }
+ }
}
- return runArtProcessRaw(builder);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void storeProcessResult(ProcessResult processResult, File dest)
+ throws IOException {
+ Gson gson = new Gson();
+ Path testOutputPath = getTestOutputPath(dest);
+ try (FileWriter fw = new FileWriter(new File(testOutputPath.toFile(), "processResult.json"))) {
+ gson.toJson(processResult, ProcessResult.class, fw);
+ }
+ }
+
+ private static ProcessResult readProcessResult(File dest) throws IOException {
+ File processResultFile = new File(getTestOutputPath(dest).toFile(), "processResult.json");
+ Gson gson = new Gson();
+ try (FileReader fr = new FileReader(processResultFile)) {
+ return gson.fromJson(fr, ProcessResult.class);
+ }
+ }
+
+ private static ProcessResult compareAgainstGoldenFiles(List<File> files, File destDir)
+ throws IOException {
+ Path testOutputPath = getTestOutputPath(destDir);
+
+ int index = 0;
+ String stdErr = "";
+ boolean passed = true;
+ for (File f : files) {
+ Path filePath = f.toPath();
+
+ List<File> testFiles = Collections.singletonList(f);
+ if (FileUtils.isArchive(filePath)) {
+ testFiles = unzipDexFilesArchive(f);
+ }
+
+ for (File testFile : testFiles) {
+ Path testFilePath = testFile.toPath();
+ // TODO(jmhenaff): Check it's been produced by D8/R8?
+ if (FileUtils.isDexFile(testFilePath)) {
+ File goldenFile = Paths.get(testOutputPath.toString(),
+ "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile();
+ if (!goldenFile.exists()) {
+ String fileDesc = "'" + testFile.getAbsolutePath() + "'";
+ if (FileUtils.isZipFile(filePath)) {
+ fileDesc += " (extracted from '" + f.getAbsolutePath() + "')";
+ }
+ stdErr += "Cannot find golden file '" + goldenFile.getAbsolutePath()
+ + "' to compare against test file " + fileDesc + "\n";
+ passed = false;
+ } else if (!com.google.common.io.Files.equal(testFile, goldenFile)) {
+ String fileDesc = "'" + testFile.getAbsolutePath() + "'";
+ if (FileUtils.isZipFile(filePath)) {
+ fileDesc += " (extracted from '" + f.getAbsolutePath() + "')";
+ }
+ stdErr +=
+ "File " + fileDesc + " differs from golden file '" + goldenFile.getAbsolutePath()
+ + "'\n";
+ passed = false;
+ }
+ index++;
+ }
+ }
+ }
+ // Ensure we processed as many files as there are golden files
+ File goldenFile = Paths.get(testOutputPath.toString(),
+ "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile();
+ if (goldenFile.exists()) {
+ stdErr += "Less dex files have been produced: there is at least one more golden file ('"
+ + goldenFile.getAbsolutePath() + "'\n";
+ passed = false;
+ }
+ return new ProcessResult(passed ? 0 : -1, "", stdErr);
+ }
+
+ public static boolean dealsWithGoldenFiles() {
+ return compareAgaintsGoldenFiles() || generateGoldenFiles();
+ }
+
+ public static boolean compareAgaintsGoldenFiles() {
+ return System.getProperty("use_golden_files_in") != null;
+ }
+
+ public static boolean generateGoldenFiles() {
+ return System.getProperty("generate_golden_files_to") != null;
}
public static ProcessResult runArtNoVerificationErrorsRaw(String file, String mainClass)
@@ -1086,7 +1275,7 @@
}
private static ProcessResult runArtProcessRaw(ArtCommandBuilder builder) throws IOException {
- Assume.assumeTrue(ToolHelper.artSupported());
+ Assume.assumeTrue(artSupported() || dealsWithGoldenFiles());
ProcessResult result;
if (builder.isForDevice()) {
try {
@@ -1161,7 +1350,7 @@
}
public static void runDex2Oat(Path file, Path outFile, DexVm vm) throws IOException {
- Assume.assumeTrue(ToolHelper.artSupported());
+ Assume.assumeTrue(ToolHelper.isDex2OatSupported());
// TODO(jmhenaff): find a way to run this on windows (push dex and run on device/emulator?)
Assume.assumeTrue(!ToolHelper.isWindows());
assert Files.exists(file);
diff --git a/src/test/java/com/android/tools/r8/VmTestRunner.java b/src/test/java/com/android/tools/r8/VmTestRunner.java
index 978cbe7..a3e76a4 100644
--- a/src/test/java/com/android/tools/r8/VmTestRunner.java
+++ b/src/test/java/com/android/tools/r8/VmTestRunner.java
@@ -68,7 +68,7 @@
@Override
protected boolean isIgnored(FrameworkMethod child) {
// Do not run VM tests if running VMs is not even supported.
- if (!ToolHelper.artSupported()) {
+ if (!ToolHelper.artSupported() && !ToolHelper.dealsWithGoldenFiles()) {
return true;
}
if (super.isIgnored(child)) {
diff --git a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
index a24e95a..7d88649 100644
--- a/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/AlwaysNullGetItemTestRunner.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import java.nio.file.Path;
import org.junit.Rule;
import org.junit.Test;
@@ -25,6 +26,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
@Test
public void test() throws Exception {
ProcessResult runInput =
diff --git a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
index e43ffd7..6069b4c 100644
--- a/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/MethodHandleTestRunner.java
@@ -31,6 +31,7 @@
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
+
@RunWith(Parameterized.class)
public class MethodHandleTestRunner extends TestBase {
static final Class<?> CLASS = MethodHandleTest.class;
@@ -143,8 +144,8 @@
if (runInput.exitCode != runDex.exitCode) {
System.out.println(runDex.stderr);
}
- assertEquals(runInput.stdout, runDex.stdout);
assertEquals(runInput.exitCode, runDex.exitCode);
+ assertEquals(runInput.stdout, runDex.stdout);
}
private void build(ProgramConsumer programConsumer) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index 25356ae..d41ee84 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -16,6 +16,7 @@
import com.android.tools.r8.naming.MemberNaming.Signature;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
@@ -113,6 +114,9 @@
@Rule
public TestName testName = new TestName();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
protected static final boolean supportsDefaultMethod(DebugTestConfig config) {
return config.isCfRuntime()
|| ToolHelper.getMinApiLevelForDexVm().getLevel() >= AndroidApiLevel.N.getLevel();
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
index d0ad1df..6f85b18 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DebugInfoTestBase.java
@@ -15,6 +15,7 @@
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.AndroidAppConsumers;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Path;
@@ -31,6 +32,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
static AndroidApp compileWithD8(Class... classes) throws IOException, CompilationFailedException {
D8Command.Builder builder = D8Command.builder();
for (Class clazz : classes) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
index 6760bdd..cb6df88 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningRegressionTests.java
@@ -35,7 +35,7 @@
}
private void buildAndTest(String folder, String mainClass) throws Exception {
- Assume.assumeTrue(ToolHelper.artSupported());
+ Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
Path proguardRules = Paths.get(ToolHelper.EXAMPLES_DIR, folder, "keep-rules.txt");
Path jarFile =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index e9319d2..d36e6ad 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -16,6 +16,11 @@
import com.android.tools.r8.VmTestRunner;
import com.android.tools.r8.code.NewInstance;
import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.ir.optimize.classinliner.builders.BuildersTestClass;
+import com.android.tools.r8.ir.optimize.classinliner.builders.ControlFlow;
+import com.android.tools.r8.ir.optimize.classinliner.builders.Pair;
+import com.android.tools.r8.ir.optimize.classinliner.builders.PairBuilder;
+import com.android.tools.r8.ir.optimize.classinliner.builders.Tuple;
import com.android.tools.r8.ir.optimize.classinliner.trivial.ClassWithFinal;
import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceAB;
import com.android.tools.r8.ir.optimize.classinliner.trivial.CycleReferenceBA;
@@ -75,8 +80,7 @@
collectNewInstanceTypes(clazz, "testConstructorMapping1"));
assertEquals(
- Collections.singleton(
- "com.android.tools.r8.ir.optimize.classinliner.trivial.ReferencedFields"),
+ Collections.emptySet(),
collectNewInstanceTypes(clazz, "testConstructorMapping2"));
assertEquals(
@@ -120,6 +124,60 @@
assertFalse(inspector.clazz(CycleReferenceBA.class).isPresent());
}
+ @Test
+ public void testBuilders() throws Exception {
+ byte[][] classes = {
+ ToolHelper.getClassAsBytes(BuildersTestClass.class),
+ ToolHelper.getClassAsBytes(BuildersTestClass.Pos.class),
+ ToolHelper.getClassAsBytes(Tuple.class),
+ ToolHelper.getClassAsBytes(Pair.class),
+ ToolHelper.getClassAsBytes(PairBuilder.class),
+ ToolHelper.getClassAsBytes(ControlFlow.class),
+ };
+ String main = BuildersTestClass.class.getCanonicalName();
+ ProcessResult javaOutput = runOnJava(main, classes);
+ assertEquals(0, javaOutput.exitCode);
+
+ AndroidApp app = runR8(buildAndroidApp(classes), BuildersTestClass.class);
+
+ DexInspector inspector = new DexInspector(app);
+ ClassSubject clazz = inspector.clazz(BuildersTestClass.class);
+
+ assertEquals(
+ Sets.newHashSet(
+ "com.android.tools.r8.ir.optimize.classinliner.builders.Pair",
+ "java.lang.StringBuilder"),
+ collectNewInstanceTypes(clazz, "testSimpleBuilder"));
+
+ // Note that Pair created instances were also inlined in the following method since
+ // we use 'System.out.println(pX.toString())', if we used 'System.out.println(pX)'
+ // as in the above method, the instance of pair would be passed to println() which
+ // would make it not eligible for inlining.
+ assertEquals(
+ Collections.singleton("java.lang.StringBuilder"),
+ collectNewInstanceTypes(clazz, "testSimpleBuilderWithMultipleBuilds"));
+
+ assertFalse(inspector.clazz(PairBuilder.class).isPresent());
+
+ assertEquals(
+ Collections.singleton("java.lang.StringBuilder"),
+ collectNewInstanceTypes(clazz, "testBuilderConstructors"));
+
+ assertFalse(inspector.clazz(Tuple.class).isPresent());
+
+ assertEquals(
+ Collections.singleton("java.lang.StringBuilder"),
+ collectNewInstanceTypes(clazz, "testWithControlFlow"));
+
+ assertFalse(inspector.clazz(ControlFlow.class).isPresent());
+
+ assertEquals(
+ Collections.emptySet(),
+ collectNewInstanceTypes(clazz, "testWithMoreControlFlow"));
+
+ assertFalse(inspector.clazz(BuildersTestClass.Pos.class).isPresent());
+ }
+
private Set<String> collectNewInstanceTypes(
ClassSubject clazz, String methodName, String... params) {
assertNotNull(clazz);
@@ -135,7 +193,11 @@
+ "-dontobfuscate\n"
+ "-allowaccessmodification";
- AndroidApp compiled = compileWithR8(app, config, o -> o.enableClassInlining = true);
+ AndroidApp compiled = compileWithR8(app, config,
+ o -> {
+ o.enableClassInlining = true;
+ o.classInliningInstructionLimit = 10000;
+ });
// Materialize file for execution.
Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/BuildersTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/BuildersTestClass.java
new file mode 100644
index 0000000..1516e14
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/BuildersTestClass.java
@@ -0,0 +1,95 @@
+// 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.ir.optimize.classinliner.builders;
+
+public class BuildersTestClass {
+ private static int ID = 0;
+
+ private static int nextInt() {
+ return ID++;
+ }
+
+ private static String next() {
+ return Integer.toString(nextInt());
+ }
+
+ public static void main(String[] args) {
+ BuildersTestClass test = new BuildersTestClass();
+ test.testSimpleBuilder();
+ test.testSimpleBuilderWithMultipleBuilds();
+ test.testBuilderConstructors();
+ test.testWithControlFlow();
+ test.testWithMoreControlFlow();
+ }
+
+ private synchronized void testSimpleBuilder() {
+ System.out.println(
+ new PairBuilder<String, String>().setFirst("f-" + next()).build().toString());
+ testSimpleBuilder2();
+ testSimpleBuilder3();
+ }
+
+ private synchronized void testSimpleBuilder2() {
+ System.out.println(
+ new PairBuilder<String, String>().setSecond("s-" + next()).build().toString());
+ }
+
+ private synchronized void testSimpleBuilder3() {
+ System.out.println(new PairBuilder<String, String>()
+ .setFirst("f-" + next()).setSecond("s-" + next()).build().toString());
+ }
+
+ private synchronized void testSimpleBuilderWithMultipleBuilds() {
+ PairBuilder<String, String> builder = new PairBuilder<>();
+ Pair p1 = builder.build();
+ System.out.println(p1.toString());
+ builder.setFirst("f-" + next());
+ Pair p2 = builder.build();
+ System.out.println(p2.toString());
+ builder.setSecond("s-" + next());
+ Pair p3 = builder.build();
+ System.out.println(p3.toString());
+ }
+
+ private synchronized void testBuilderConstructors() {
+ System.out.println(new Tuple().toString());
+ System.out.println(new Tuple(true, (byte) 77, (short) 9977, '#', 42,
+ 987654321123456789L, -12.34f, 43210.98765, "s-" + next() + "-s").toString());
+ }
+
+ private synchronized void testWithControlFlow() {
+ ControlFlow flow = new ControlFlow(-1, 2, 7);
+ for (int k = 0; k < 25; k++) {
+ if (k % 3 == 0) {
+ flow.foo(k);
+ } else if (k % 3 == 1) {
+ flow.bar(nextInt(), nextInt(), nextInt(), nextInt());
+ }
+ }
+ System.out.println("flow = " + flow.toString());
+ }
+
+ private synchronized void testWithMoreControlFlow() {
+ String str = "1234567890";
+ Pos pos = new Pos();
+ while (pos.y < str.length()) {
+ pos.x = pos.y;
+ pos.y = pos.x;
+
+ if (str.charAt(pos.x) != '*') {
+ if ('0' <= str.charAt(pos.y) && str.charAt(pos.y) <= '9') {
+ while (pos.y < str.length() && '0' <= str.charAt(pos.y) && str.charAt(pos.y) <= '9') {
+ pos.y++;
+ }
+ }
+ }
+ }
+ }
+
+ public static class Pos {
+ public int x = 0;
+ public int y = 0;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/ControlFlow.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/ControlFlow.java
new file mode 100644
index 0000000..7b95dc1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/ControlFlow.java
@@ -0,0 +1,54 @@
+// 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.ir.optimize.classinliner.builders;
+
+public class ControlFlow {
+ int a;
+ int b;
+ int c = 1234;
+ int d;
+ String s = ">";
+
+ ControlFlow(int b, int c, int d) {
+ this.s += this.a++ + ">";
+ this.s += this.b + ">";
+ this.b = b;
+ this.s += this.b + ">";
+ this.s += this.c + ">";
+ this.c += c;
+ this.s += this.c + ">";
+ this.s += (this.d = d) + ">";
+ }
+
+ public void foo(int count) {
+ for (int i = 0; i < count; i++) {
+ switch (i % 4) {
+ case 0:
+ this.s += ++this.a + ">";
+ break;
+ case 1:
+ this.c += this.b;
+ this.s += this.c + ">";
+ break;
+ case 2:
+ this.d += this.d++ + this.c++ + this.b++ + this.a++;
+ this.s += this.d + ">";
+ break;
+ }
+ }
+ }
+
+ public void bar(int a, int b, int c, int d) {
+ this.a += a;
+ this.b += b;
+ this.c += c;
+ this.d += d;
+ }
+
+ @Override
+ public String toString() {
+ return s;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Pair.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Pair.java
new file mode 100644
index 0000000..af45cab
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Pair.java
@@ -0,0 +1,22 @@
+// 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.ir.optimize.classinliner.builders;
+
+public class Pair<F, S> {
+ public final F first;
+ public final S second;
+
+ public Pair(F first, S second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @Override
+ public String toString() {
+ return "Pair(" +
+ (first == null ? "<null>" : first) + ", " +
+ (second == null ? "<null>" : second) + ")";
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/PairBuilder.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/PairBuilder.java
new file mode 100644
index 0000000..0c80c53
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/PairBuilder.java
@@ -0,0 +1,29 @@
+// 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.ir.optimize.classinliner.builders;
+
+public class PairBuilder<F, S> {
+ public F first;
+ public S second = null;
+
+ public PairBuilder<F, S> setFirst(F first) {
+ System.out.println("[before] first = " + this.first);
+ this.first = first;
+ System.out.println("[after] first = " + this.first);
+ return this;
+ }
+
+ public PairBuilder<F, S> setSecond(S second) {
+ System.out.println("[before] second = " + this.second);
+ this.second = second;
+ System.out.println("[after] second = " + this.second);
+ return this;
+ }
+
+ public Pair<F, S> build() {
+ return new Pair<>(first, second);
+ }
+}
+
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Tuple.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Tuple.java
new file mode 100644
index 0000000..3a77cc2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/builders/Tuple.java
@@ -0,0 +1,39 @@
+// 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.ir.optimize.classinliner.builders;
+
+public class Tuple {
+ public boolean z;
+ public byte b;
+ public short s;
+ public char c;
+ public int i;
+ public long l;
+ public float f;
+ public double d;
+ public Object o;
+
+ Tuple() {
+ }
+
+ Tuple(boolean z, byte b, short s, char c, int i, long l, float f, double d, Object o) {
+ this.z = z;
+ this.b = b;
+ this.s = s;
+ this.c = c;
+ this.i = i;
+ this.l = l;
+ this.f = f;
+ this.d = d;
+ this.o = o;
+ }
+
+ @Override
+ public String toString() {
+ return "Tuple1(" + z + ", " + b + ", " + s + ", " +
+ ((int) c) + ", " + i + ", " + l + ", " + f + ", " +
+ d + ", " + (o == null ? "<null>" : o) + ")";
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
index 57d694d..86c15a2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/InvokeInterfaceToInvokeVirtualTest.java
@@ -87,4 +87,4 @@
assertEquals(0, artOutput.exitCode);
assertEquals(javaOutput.stdout.trim(), artOutput.stdout.trim());
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java b/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
index bdedf43..3028525 100644
--- a/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
+++ b/src/test/java/com/android/tools/r8/jdwp/RunJdwpTests.java
@@ -16,6 +16,7 @@
import com.android.tools.r8.ToolHelper.DexVm.Version;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.File;
@@ -30,6 +31,7 @@
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.ClassRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -267,6 +269,9 @@
@ClassRule
public static TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
@BeforeClass
public static void compileLibraries() throws Exception {
// Selects appropriate jar according to min api level for the selected runtime.
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index c175e04..f99934e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -223,7 +223,7 @@
protected void runTest(String folder, String mainClass, String extraProguardRules,
Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
- Assume.assumeTrue(ToolHelper.artSupported());
+ Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
String proguardRules = buildProguardRules(mainClass);
if (extraProguardRules != null) {
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index c8c8aec..73d1e50 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -10,7 +10,9 @@
import com.android.tools.r8.utils.DexInspector;
import com.android.tools.r8.utils.DexInspector.ClassSubject;
import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.InternalOptions;
import java.util.Collections;
+import java.util.function.Consumer;
import org.junit.Test;
public class R8KotlinDataClassTest extends AbstractR8KotlinTestBase {
@@ -33,13 +35,15 @@
private static final MethodSignature COPY_DEFAULT_METHOD =
TEST_DATA_CLASS.getCopyDefaultSignature();
+ private Consumer<InternalOptions> disableClassInliner = o -> o.enableClassInlining = false;
+
@Test
public void test_dataclass_gettersOnly() throws Exception {
final String mainClassName = "dataclass.MainGettersOnlyKt";
final MethodSignature testMethodSignature =
new MethodSignature("testDataClassGetters", "void", Collections.emptyList());
final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
- runTest("dataclass", mainClassName, extraRules, (app) -> {
+ runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
@@ -74,7 +78,7 @@
final MethodSignature testMethodSignature =
new MethodSignature("testAllDataClassComponentFunctions", "void", Collections.emptyList());
final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
- runTest("dataclass", mainClassName, extraRules, (app) -> {
+ runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
@@ -109,7 +113,7 @@
final MethodSignature testMethodSignature =
new MethodSignature("testSomeDataClassComponentFunctions", "void", Collections.emptyList());
final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
- runTest("dataclass", mainClassName, extraRules, (app) -> {
+ runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
@@ -144,7 +148,7 @@
final MethodSignature testMethodSignature =
new MethodSignature("testDataClassCopy", "void", Collections.emptyList());
final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
- runTest("dataclass", mainClassName, extraRules, (app) -> {
+ runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
@@ -159,7 +163,7 @@
final MethodSignature testMethodSignature =
new MethodSignature("testDataClassCopyWithDefault", "void", Collections.emptyList());
final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
- runTest("dataclass", mainClassName, extraRules, (app) -> {
+ runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
DexInspector dexInspector = new DexInspector(app);
ClassSubject dataClass = checkClassIsKept(dexInspector, TEST_DATA_CLASS.getClassName());
diff --git a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
index e2dfb6d..46a439d 100644
--- a/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
+++ b/src/test/java/com/android/tools/r8/memberrebinding/MemberRebindingTest.java
@@ -16,6 +16,7 @@
import com.android.tools.r8.utils.DexInspector.InstructionSubject;
import com.android.tools.r8.utils.DexInspector.InvokeInstructionSubject;
import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Path;
@@ -26,6 +27,7 @@
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -60,6 +62,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
public MemberRebindingTest(TestConfiguration configuration) {
this.kind = configuration.kind;
originalDex = configuration.getDexPath();
@@ -319,9 +324,8 @@
@Test
public void memberRebindingTest() throws IOException, InterruptedException, ExecutionException {
- if (!ToolHelper.artSupported()) {
- return;
- }
+ Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
+
String out = temp.getRoot().getCanonicalPath();
Path processed = Paths.get(out, "classes.dex");
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java
index 2e4d6cf..fa4e3e1 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232.java
@@ -7,11 +7,9 @@
import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
import com.android.tools.r8.CompilationFailedException;
import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import org.junit.Assume;
import org.junit.Test;
public class Regress78493232 extends AsmTestBase {
@@ -20,7 +18,6 @@
public void test() throws Exception {
// Run test on JVM and ART(x86) to ensure expected behavior.
// Running the same test on an ARM JIT causes errors.
- Assume.assumeTrue(ToolHelper.getDexVm() != DexVm.ART_7_0_0_HOST); // b/78866151
ensureSameOutput(
Regress78493232Dump.CLASS_NAME,
Regress78493232Dump.dump(),
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
new file mode 100644
index 0000000..bfb0674
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Dump_WithPhi.java
@@ -0,0 +1,341 @@
+// 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.regress.b78493232;
+
+import com.android.tools.r8.utils.DescriptorUtils;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+public class Regress78493232Dump_WithPhi implements Opcodes {
+
+ public static final String CLASS_NAME = "regress78493232.Test";
+ public static final String CLASS_DESC = DescriptorUtils.javaTypeToDescriptor(CLASS_NAME);
+ public static final String CLASS_INTERNAL = DescriptorUtils.descriptorToInternalName(CLASS_DESC);
+
+ public static final String UTILS_CLASS_NAME = Regress78493232Utils.class.getCanonicalName();
+ public static final String UTILS_CLASS_DESC =
+ DescriptorUtils.javaTypeToDescriptor(UTILS_CLASS_NAME);
+ public static final String UTILS_CLASS_INTERNAL =
+ DescriptorUtils.descriptorToInternalName(UTILS_CLASS_DESC);
+
+ static final int iterations = 1000;
+
+ // Arguments that have been seen when issue occurred.
+ static byte arg0 = 13;
+ static short arg1 = 25;
+ static int arg2 = 312;
+
+ // Static state of the class seen when the issue occurred.
+ static int staticIntA = 0;
+ static int staticIntB = 29;
+ static byte[] staticByteArray =
+ new byte[] {
+ 63, 101, 52, -33, 9, -21, 21, 51, -59, -6, 65, -27, -37, -2, -5, 1, 33, -33, 2, 13, 4, -12,
+ -53, 54, 2, -15, 46, 2, 13, 4, -3, 30, -47, 9, 0, -13, 3, -11, -10, 13, -2, 61, -69, -6, 6,
+ -1, 15, -8, 63, -22, -33, -19, 50, -35, -3, 7, 8, 2, -7, 9, -21, 21, 51, -62, 11, -13, 7,
+ 57, -37, -38, 6, -1, 15, -8, -53, 3, -19, 19, 50, -53, 3, -19, 19, 50, 9, -21, 21, 51, -59,
+ -6, 65, -27, -6, 10, -51, 21, -2, -11, -4, 11, -6, 1, 1, 11, -3, 61, -50, 50, -75, 75, -52,
+ 0, 52, -53, -9, -3, -4, 14, 2, -15, 49, -41, 11, -18, 0, 39, -35, 14, -3, -1, -13, 9, -21,
+ 21, 51, -59, -6, 65, -70, 7, -3, 12, -5, -9, 2, -13, 23, -27, 9, -11, 15, -6, 11, 11, 11, 7,
+ 16, -31, -2, 4, 7, 9, -21, 21, 51, -69, 14, 2, -18, 3, 9, -11, -5, 75, -37, -18, 2, -18, 3,
+ 13, 19, -15, -13, 10, -11, 2, -1, -7, 7, -15, 15, 5, 9, -11, 15, 13, 4, -3, -18, 3, 0, 13,
+ -9, -6, 32, -21, -4, 8, 24, -28, -3, 0, 3, -10, 9, -21, 21, 51, -59, -6, 65, -20, -55, 5,
+ 15, 36, -49, 0, 17, -24, 48, -37, -2, -5, 1, 33, -33, 2, 13, 4, -12, 9, -21, 21, 51, -59,
+ -6, 65, -24, -35, -3, 7, 22, -38, 1, 4, -5, 1, 33, -33, 2, 13, 4, -12, 1, 11, -3, 61, -50,
+ 50, -75, 75, -52, 0, 52, -52, 63, -77, 2, -15, 47, -51, 4, 15, -13, 4, 13, -11, 25, -33, 5,
+ -3, 17, -6, 2, 33, -37, -9, 13, 2, -17, 5, -3, -7, -3, 14, -3, 33, -41, 11, -18, 0, 2, -15,
+ 43, -37, -5, -1, 19, -13, 11, -2, -13, 10, -14, 3, 6, 5, 54, -65, -4, 69, -23, -41, -8, 13,
+ -9, 3, 1, 1, 8, -9, -6, 21, -4, 20, -8, 9, -21, 21, 51, -62, 11, -13, 7, 57, -21, -41, 11,
+ -18, 0, 39, -35, 14, -3, -1, -13, -3, 14, -3, 32, -33, -19, 1, 11, -3, 62, -51, 51, -76, 76,
+ -53, 0, 53, -54, 14, -15, 33, -18, 0, 1, 9, -21, 21, 51, -59, -6, 65, -22, -29, -19, 19, 24,
+ -37, -2, -5, 1, 33, -33, 2, 13, 4, -12, 2, -15, 36, -34, 3, -1, 11, -13, -2, -5, 2, -15, 51,
+ -33, -17, 4, 3, -9, 1, 15, 21, -17, -19, 12, 9, -21, 21, 51, -59, -6, 65, -24, -35, -3, 7,
+ 9, -21, 21, 51, -59, -6, 65, -24, -35, -3, 7, 33, -33, -14, 16, -15, 9, -7, -4, 5, -3, 21,
+ -3, 19, -8, 9, -19, 4, 43, -37, -6
+ };
+
+ public static byte[] dump() {
+ return dump(arg0, arg1, arg2, staticIntA, staticIntB, staticByteArray);
+ }
+
+ public static byte[] dump(
+ byte arg0, short arg1, int arg2, int staticIntA, int staticIntB, byte[] staticByteArray) {
+ ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
+ FieldVisitor fv;
+ MethodVisitor mv;
+
+ cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, CLASS_INTERNAL, null, "java/lang/Object", null);
+
+ {
+ fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "staticByteArray", "[B", null, null);
+ fv.visitEnd();
+ }
+ {
+ fv = cw.visitField(ACC_PRIVATE + ACC_STATIC, "staticIntA", "I", null, null);
+ fv.visitEnd();
+ }
+ {
+ fv = cw.visitField(ACC_PRIVATE + ACC_STATIC, "staticIntB", "I", null, null);
+ fv.visitEnd();
+ }
+
+ {
+ mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "run", "()Ljava/lang/String;", null, null);
+ mv.visitCode();
+ mv.visitIntInsn(BIPUSH, arg0);
+ mv.visitIntInsn(SIPUSH, arg1);
+ mv.visitIntInsn(SIPUSH, arg2);
+ mv.visitMethodInsn(
+ INVOKESTATIC, CLASS_INTERNAL, "methodCausingIssue", "(BSI)Ljava/lang/String;", false);
+ mv.visitInsn(ARETURN);
+ mv.visitMaxs(-1, -1);
+ mv.visitEnd();
+ }
+
+ {
+ mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "getHash", "()I", null, null);
+ mv.visitCode();
+ getHash(mv);
+ mv.visitInsn(IRETURN);
+ mv.visitMaxs(-1, -1);
+ mv.visitEnd();
+ }
+
+ {
+ mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
+ mv.visitCode();
+
+ mv.visitInsn(ICONST_0);
+ mv.visitVarInsn(ISTORE, 1);
+ Label l0 = new Label();
+ mv.visitLabel(l0);
+ mv.visitVarInsn(ILOAD, 1);
+ mv.visitLdcInsn(new Integer(iterations));
+ Label l1 = new Label();
+ mv.visitJumpInsn(IF_ICMPGE, l1);
+ mv.visitMethodInsn(INVOKESTATIC, CLASS_INTERNAL, "run", "()Ljava/lang/String;", false);
+ mv.visitVarInsn(ILOAD, 1);
+ mv.visitMethodInsn(
+ INVOKESTATIC, UTILS_CLASS_INTERNAL, "compare", "(Ljava/lang/String;I)V", false);
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntA", "I");
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntB", "I");
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticByteArray", "[B");
+ mv.visitVarInsn(ILOAD, 1);
+ mv.visitMethodInsn(INVOKESTATIC, UTILS_CLASS_INTERNAL, "compareHash", "(II[BI)V", false);
+ mv.visitIincInsn(1, 1);
+ mv.visitJumpInsn(GOTO, l0);
+ mv.visitLabel(l1);
+ mv.visitLdcInsn("Completed successfully after " + iterations + " iterations");
+ mv.visitMethodInsn(
+ INVOKESTATIC, UTILS_CLASS_INTERNAL, "println", "(Ljava/lang/String;)V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(-1, -1);
+ mv.visitEnd();
+ }
+
+ {
+ mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
+ mv.visitCode();
+ mv.visitIntInsn(BIPUSH, staticIntA);
+ mv.visitFieldInsn(PUTSTATIC, CLASS_INTERNAL, "staticIntA", "I");
+ mv.visitIntInsn(BIPUSH, staticIntB);
+ mv.visitFieldInsn(PUTSTATIC, CLASS_INTERNAL, "staticIntB", "I");
+ mv.visitLdcInsn(staticByteArray.length);
+ mv.visitIntInsn(NEWARRAY, T_BYTE);
+ for (int i = 0; i < staticByteArray.length; i++) {
+ mv.visitInsn(DUP);
+ mv.visitIntInsn(SIPUSH, i);
+ mv.visitIntInsn(BIPUSH, staticByteArray[i]);
+ mv.visitInsn(BASTORE);
+ }
+ mv.visitFieldInsn(PUTSTATIC, CLASS_INTERNAL, "staticByteArray", "[B");
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(-1, -1);
+ mv.visitEnd();
+ }
+
+ {
+ mv =
+ cw.visitMethod(
+ ACC_PRIVATE + ACC_STATIC,
+ "methodCausingIssue",
+ "(BSI)Ljava/lang/String;",
+ null,
+ null);
+ mv.visitCode();
+ Label l0 = new Label();
+ mv.visitJumpInsn(GOTO, l0);
+ mv.visitLabel(l0);
+ mv.visitIntInsn(SIPUSH, 472);
+ mv.visitVarInsn(ILOAD, 2);
+ mv.visitInsn(ISUB);
+ mv.visitVarInsn(ISTORE, 2);
+ mv.visitInsn(ICONST_0);
+ mv.visitVarInsn(ISTORE, 3);
+ mv.visitTypeInsn(NEW, "java/lang/String");
+ mv.visitVarInsn(ASTORE, 10);
+ mv.visitIntInsn(BIPUSH, 119);
+ mv.visitVarInsn(ILOAD, 0);
+ mv.visitInsn(ISUB);
+ mv.visitVarInsn(ISTORE, 0);
+ mv.visitIincInsn(1, 1);
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticByteArray", "[B");
+ mv.visitVarInsn(ASTORE, 4);
+ mv.visitVarInsn(ILOAD, 1);
+ mv.visitIntInsn(NEWARRAY, T_BYTE);
+ mv.visitVarInsn(ALOAD, 4);
+ Label l1 = new Label();
+ mv.visitJumpInsn(IFNONNULL, l1);
+ Label l2 = new Label();
+ mv.visitJumpInsn(GOTO, l2);
+ mv.visitLabel(l1);
+ Label l3 = new Label();
+ mv.visitJumpInsn(GOTO, l3);
+ mv.visitLabel(l2);
+ mv.visitVarInsn(ILOAD, 1);
+ mv.visitVarInsn(ILOAD, 2);
+ Label l4 = new Label();
+ mv.visitJumpInsn(GOTO, l4);
+ Label l5 = new Label();
+ mv.visitLabel(l5);
+ mv.visitInsn(INEG);
+ mv.visitInsn(IADD);
+ mv.visitVarInsn(ISTORE, 0);
+ mv.visitJumpInsn(GOTO, l3);
+ mv.visitLabel(l3);
+ mv.visitInsn(DUP);
+ mv.visitVarInsn(ILOAD, 3);
+ mv.visitIincInsn(3, 1);
+ mv.visitVarInsn(ILOAD, 0);
+ mv.visitInsn(I2B);
+ mv.visitInsn(BASTORE);
+ mv.visitVarInsn(ILOAD, 3);
+ mv.visitVarInsn(ILOAD, 1);
+ Label l6 = new Label();
+ mv.visitJumpInsn(IF_ICMPNE, l6);
+ Label l7 = new Label();
+ Label l7_b0 = new Label();
+ Label l7_b1 = new Label();
+ Label l7_join = new Label();
+ mv.visitJumpInsn(GOTO, l7);
+ mv.visitLabel(l6);
+ Label l8 = new Label();
+ mv.visitJumpInsn(GOTO, l8);
+ // Depending on the argument either load the new-instance string or null on the stack.
+ mv.visitLabel(l7);
+ mv.visitVarInsn(ILOAD, 0);
+ mv.visitJumpInsn(IFEQ, l7_b1);
+ mv.visitLabel(l7_b0);
+ mv.visitInsn(ACONST_NULL);
+ // At this point, NULL flows to l7_join.
+ mv.visitJumpInsn(GOTO, l7_join);
+ mv.visitLabel(l7_b1);
+ mv.visitVarInsn(ALOAD, 10);
+ // At this point, an uninitialized String flows to l7_join.
+ mv.visitLabel(l7_join);
+ // At this point, stack contains [staticByteArray, NULL/uninitialized]
+ mv.visitInsn(SWAP);
+ // At this point, stack contains [NULL/uninitialized, staticByteArray]
+ mv.visitVarInsn(ALOAD, 10);
+ // At this point, stack contains [NULL/uninitialized, staticByteArray, uninitialized]
+ mv.visitInsn(SWAP);
+ // At this point, stack contains [NULL/uninitialized, uninitialized, staticByteArray]
+ mv.visitInsn(ICONST_0);
+ // At this point, stack contains [NULL/uninitialized, uninitialized, staticByteArray, 0]
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/String", "<init>", "([BI)V", false);
+ // Just before ARETURN, stack contains [NULL/String].
+ // This will force a non-trivial phi of the new-instance on this block, ie, prior to <init>.
+ // This phi must remain since it is not trivial.
+ mv.visitInsn(ARETURN);
+ mv.visitLabel(l8);
+ mv.visitVarInsn(ILOAD, 0);
+ mv.visitVarInsn(ALOAD, 4);
+ mv.visitIincInsn(2, 1);
+ mv.visitVarInsn(ILOAD, 2);
+ mv.visitInsn(BALOAD);
+ Label l9 = new Label();
+ mv.visitJumpInsn(GOTO, l9);
+ mv.visitLabel(l4);
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntB", "I");
+ mv.visitIntInsn(BIPUSH, 125);
+ mv.visitInsn(IADD);
+ mv.visitInsn(DUP);
+ mv.visitIntInsn(SIPUSH, 128);
+ mv.visitInsn(IREM);
+ mv.visitFieldInsn(PUTSTATIC, CLASS_INTERNAL, "staticIntA", "I");
+ mv.visitInsn(ICONST_2);
+ mv.visitInsn(IREM);
+ Label l10 = new Label();
+ mv.visitJumpInsn(IFEQ, l10);
+ Label l11 = new Label();
+ mv.visitJumpInsn(GOTO, l11);
+ mv.visitLabel(l10);
+ Label l12 = new Label();
+ mv.visitJumpInsn(GOTO, l12);
+ Label l13 = new Label();
+ mv.visitLabel(l13);
+ mv.visitInsn(INEG);
+ mv.visitInsn(IADD);
+ mv.visitVarInsn(ISTORE, 0);
+ mv.visitJumpInsn(GOTO, l3);
+ mv.visitLabel(l9);
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntA", "I");
+ mv.visitIntInsn(BIPUSH, 29);
+ mv.visitInsn(IADD);
+ mv.visitInsn(DUP);
+ mv.visitIntInsn(SIPUSH, 128);
+ mv.visitInsn(IREM);
+ mv.visitFieldInsn(PUTSTATIC, CLASS_INTERNAL, "staticIntB", "I");
+ mv.visitInsn(ICONST_2);
+ mv.visitInsn(IREM);
+ Label l14 = new Label();
+ mv.visitJumpInsn(IFNE, l14);
+ Label l15 = new Label();
+ mv.visitJumpInsn(GOTO, l15);
+ mv.visitLabel(l14);
+ Label l16 = new Label();
+ mv.visitJumpInsn(GOTO, l16);
+ Label l17 = new Label();
+ mv.visitLabel(l17);
+ mv.visitInsn(INEG);
+ mv.visitInsn(IADD);
+ mv.visitVarInsn(ISTORE, 0);
+ mv.visitJumpInsn(GOTO, l3);
+ Label l18 = new Label();
+ mv.visitLabel(l18);
+ mv.visitTableSwitchInsn(0, 1, l11, new Label[] {l13, l5});
+ mv.visitLabel(l12);
+ mv.visitInsn(ICONST_1);
+ mv.visitJumpInsn(GOTO, l18);
+ mv.visitLabel(l11);
+ mv.visitInsn(ICONST_0);
+ mv.visitJumpInsn(GOTO, l18);
+ Label l19 = new Label();
+ mv.visitLabel(l19);
+ mv.visitTableSwitchInsn(0, 1, l15, new Label[] {l5, l17});
+ mv.visitLabel(l16);
+ mv.visitInsn(ICONST_0);
+ mv.visitJumpInsn(GOTO, l19);
+ mv.visitLabel(l15);
+ mv.visitInsn(ICONST_1);
+ mv.visitJumpInsn(GOTO, l19);
+ mv.visitMaxs(8, 5);
+ mv.visitEnd();
+ }
+
+ cw.visitEnd();
+
+ return cw.toByteArray();
+ }
+
+ private static void getHash(MethodVisitor mv) {
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntA", "I");
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticIntB", "I");
+ mv.visitFieldInsn(GETSTATIC, CLASS_INTERNAL, "staticByteArray", "[B");
+ mv.visitMethodInsn(INVOKESTATIC, UTILS_CLASS_INTERNAL, "getHash", "(II[B)I", false);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
index ff7da8f..ccf6467 100644
--- a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232Utils.java
@@ -31,13 +31,14 @@
public static void compare(String output, int iterations) {
String expected = "java.security.SecureRandom";
- if (output.equals(expected)) {
+ if (expected.equals(output)) {
return;
}
System.out.println(
"After " + iterations + " iterations, expected \"" +
expected + "\", but got \"" + output + "\"");
- System.exit(1);
+ // Exit with code 0 to allow test to use ensureSameOutput().
+ System.exit(0);
}
public static void compareHash(int a, int b, byte[] c, int iterations) {
@@ -53,6 +54,7 @@
System.out.println("staticIntB: " + b);
System.out.print("staticIntByteArray: ");
printByteArray(c);
- System.exit(1);
+ // Exit with code 0 to allow test to use ensureSameOutput().
+ System.exit(0);
}
}
diff --git a/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
new file mode 100644
index 0000000..3a56004
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b78493232/Regress78493232_WithPhi.java
@@ -0,0 +1,81 @@
+// 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.regress.b78493232;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.AsmTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.AndroidApp;
+import org.junit.Test;
+
+// Variant of Regress78493232, but where the new-instance is forced to flow to a non-trivial phi
+// function prior to the call to <init>.
+public class Regress78493232_WithPhi extends AsmTestBase {
+
+ @Test
+ public void test() throws Exception {
+ // Run test on JVM and ART(x86) to ensure expected behavior.
+ // Running the same test on an ARM JIT causes errors.
+
+ // TODO(b/80118070): Remove this if-statement when fixed.
+ if (ToolHelper.getDexVm().getVersion() != Version.V5_1_1) {
+ AndroidApp app =
+ buildAndroidApp(
+ Regress78493232Dump_WithPhi.dump(),
+ ToolHelper.getClassAsBytes(Regress78493232Utils.class));
+ ProcessResult javaResult =
+ runOnJava(
+ Regress78493232Dump_WithPhi.CLASS_NAME,
+ Regress78493232Dump_WithPhi.dump(),
+ ToolHelper.getClassAsBytes(Regress78493232Utils.class));
+ ProcessResult d8Result =
+ runOnArtRaw(compileWithD8(app), Regress78493232Dump_WithPhi.CLASS_NAME);
+ ProcessResult r8Result =
+ runOnArtRaw(compileWithR8(app), Regress78493232Dump_WithPhi.CLASS_NAME);
+ String proguardConfig =
+ keepMainProguardConfiguration(Regress78493232Dump_WithPhi.CLASS_NAME)
+ + "-dontobfuscate\n";
+ ProcessResult r8ShakenResult =
+ runOnArtRaw(compileWithR8(app, proguardConfig), Regress78493232Dump_WithPhi.CLASS_NAME);
+ assertEquals(
+ "After 0 iterations, expected \"java.security.SecureRandom\", but got \"null\"\n",
+ javaResult.stdout);
+ assertEquals(0, javaResult.exitCode);
+ switch (ToolHelper.getDexVm().getVersion()) {
+ case V4_0_4:
+ case V4_4_4:
+ case V7_0_0:
+ case DEFAULT:
+ assertNotEquals(-1, d8Result.stderr.indexOf("java.lang.VerifyError"));
+ assertNotEquals(-1, r8Result.stderr.indexOf("java.lang.VerifyError"));
+ assertNotEquals(-1, r8ShakenResult.stderr.indexOf("java.lang.VerifyError"));
+ assertEquals(1, d8Result.exitCode);
+ assertEquals(1, r8Result.exitCode);
+ assertEquals(1, r8ShakenResult.exitCode);
+ break;
+ case V6_0_1:
+ assertEquals("Completed successfully after 1000 iterations\n", d8Result.stdout);
+ assertEquals("Completed successfully after 1000 iterations\n", r8Result.stdout);
+ assertEquals("Completed successfully after 1000 iterations\n", r8ShakenResult.stdout);
+ assertEquals(0, d8Result.exitCode);
+ assertEquals(0, r8Result.exitCode);
+ assertEquals(0, r8ShakenResult.exitCode);
+ break;
+ case V5_1_1:
+ default:
+ throw new Unreachable();
+ }
+ return;
+ }
+ ensureSameOutput(
+ Regress78493232Dump_WithPhi.CLASS_NAME,
+ Regress78493232Dump_WithPhi.dump(),
+ ToolHelper.getClassAsBytes(Regress78493232Utils.class));
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
index 2df0612..66161e1 100644
--- a/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
+++ b/src/test/java/com/android/tools/r8/resource/DataResourceTest.java
@@ -10,6 +10,7 @@
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -23,6 +24,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
@Test
public void dataResourceTest() throws IOException, CompilationFailedException {
String packageName = "dataresource";
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
index f341969..c9488b4 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingTest.java
@@ -19,6 +19,7 @@
import com.android.tools.r8.utils.DexInspector.FoundFieldSubject;
import com.android.tools.r8.utils.DexInspector.FoundMethodSubject;
import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.TestDescriptionWatcher;
import com.google.common.collect.ImmutableList;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -76,6 +77,9 @@
@Rule
public TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
+ @Rule
+ public TestDescriptionWatcher watcher = new TestDescriptionWatcher();
+
protected TreeShakingTest(
String name, String mainClass, Frontend frontend, Backend backend, MinifyMode minify) {
this.name = name;
@@ -204,7 +208,7 @@
}
return;
}
- if (!ToolHelper.artSupported()) {
+ if (!ToolHelper.artSupported() && !ToolHelper.compareAgaintsGoldenFiles()) {
return;
}
Consumer<ArtCommandBuilder> extraArtArgs = builder -> {
diff --git a/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java b/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java
new file mode 100644
index 0000000..5295c0e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/TestDescriptionWatcher.java
@@ -0,0 +1,29 @@
+// 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 org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+/**
+ * This {@link TestWatcher} passes test description information
+ * (namely test class and method names) to the test framework
+ * via system properties.
+ */
+public class TestDescriptionWatcher extends TestWatcher {
+
+ @Override
+ protected void starting(Description description) {
+ System.setProperty("test_class_name", description.getClassName());
+ System.setProperty("test_name", description.getMethodName());
+ System.setProperty("reset_output_index", "true");
+ }
+
+ @Override
+ protected void finished(Description description) {
+ System.clearProperty("test_class_name");
+ System.clearProperty("test_name");
+ }
+
+}
diff --git a/tools/download_from_x20.py b/tools/download_from_x20.py
index 01ba4e1..5f64684 100755
--- a/tools/download_from_x20.py
+++ b/tools/download_from_x20.py
@@ -19,17 +19,10 @@
def parse_options():
return optparse.OptionParser().parse_args()
-def extract_dir(filename):
- return filename[0:len(filename) - len('.tar.gz')]
-
-def unpack_archive(filename):
- dest_dir = extract_dir(filename)
- if os.path.exists(dest_dir):
- print 'Deleting existing dir %s' % dest_dir
- shutil.rmtree(dest_dir)
- dirname = os.path.dirname(os.path.abspath(filename))
- with tarfile.open(filename, 'r:gz') as tar:
- tar.extractall(path=dirname)
+def download(src, dest):
+ print 'Downloading %s to %s' % (src, dest)
+ shutil.copyfile(src, dest)
+ utils.unpack_archive(dest)
def Main():
(options, args) = parse_options()
@@ -41,20 +34,18 @@
sha1 = input_sha.readline()
if os.path.exists(dest) and utils.get_sha1(dest) == sha1:
print 'sha1 matches, not downloading'
- dest_dir = extract_dir(dest)
+ dest_dir = utils.extract_dir(dest)
if os.path.exists(dest_dir):
print 'destination directory exists, no extraction'
else:
- unpack_archive(dest)
+ utils.unpack_archive(dest)
return
src = os.path.join(GMSCORE_DEPS, sha1)
if not os.path.exists(src):
print 'File (%s) does not exist on x20' % src
print 'Maybe pass -Pno_internal to your gradle invocation'
return 42
- print 'Downloading %s to %s' % (src, dest)
- shutil.copyfile(src, dest)
- unpack_archive(dest)
+ download(src, dest)
if __name__ == '__main__':
sys.exit(Main())
diff --git a/tools/test.py b/tools/test.py
index ed99c1d..b92c8dc 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -15,6 +15,7 @@
import utils
import uuid
import notify
+import upload_to_x20
ALL_ART_VMS = ["default", "7.0.0", "6.0.1", "5.1.1", "4.4.4", "4.0.4"]
@@ -78,6 +79,13 @@
' Note that the directory will not be cleared before the test.')
result.add_option('--java_home',
help='Use a custom java version to run tests.')
+ result.add_option('--generate_golden_files_to',
+ help='Store dex files produced by tests in the specified directory.'
+ ' It is aimed to be read on platforms with no host runtime available'
+ ' for comparison.')
+ result.add_option('--use_golden_files_in',
+ help='Download golden files hierarchy for this commit in the specified'
+ ' location and use them instead of executing on host runtime.')
return result.parse_args()
@@ -93,7 +101,7 @@
def Main():
(options, args) = ParseOptions()
- gradle_args = []
+ gradle_args = ['--stacktrace']
# Set all necessary Gradle properties and options first.
if options.verbose:
gradle_args.append('-Pprint_test_stdout')
@@ -134,7 +142,16 @@
os.makedirs(options.test_dir)
if options.java_home:
gradle_args.append('-Dorg.gradle.java.home=' + options.java_home)
-
+ if options.generate_golden_files_to:
+ gradle_args.append('-Pgenerate_golden_files_to=' + options.generate_golden_files_to)
+ if not os.path.exists(options.generate_golden_files_to):
+ os.makedirs(options.generate_golden_files_to)
+ gradle_args.append('-PHEAD_sha1=' + utils.get_HEAD_sha1())
+ if options.use_golden_files_in:
+ gradle_args.append('-Puse_golden_files_in=' + options.use_golden_files_in)
+ if not os.path.exists(options.use_golden_files_in):
+ os.makedirs(options.use_golden_files_in)
+ gradle_args.append('-PHEAD_sha1=' + utils.get_HEAD_sha1())
# Add Gradle tasks
gradle_args.append('cleanTest')
gradle_args.append('test')
@@ -146,12 +163,29 @@
# Create Jacoco report after tests.
gradle_args.append('jacocoTestReport')
+ if options.use_golden_files_in:
+ sha1 = '%s' % utils.get_HEAD_sha1()
+ with utils.ChangedWorkingDirectory(options.use_golden_files_in):
+ utils.download_file_from_cloud_storage(
+ 'gs://r8-test-results/golden-files/%s.tar.gz' % sha1,
+ '%s.tar.gz' % sha1)
+ utils.unpack_archive('%s.tar.gz' % sha1)
+
+
# Now run tests on selected runtime(s).
vms_to_test = [options.dex_vm] if options.dex_vm != "all" else ALL_ART_VMS
for art_vm in vms_to_test:
vm_kind_to_test = "_" + options.dex_vm_kind if art_vm != "default" else ""
return_code = gradle.RunGradle(gradle_args + ['-Pdex_vm=%s' % (art_vm + vm_kind_to_test)],
throw_on_failure=False)
+
+ if options.generate_golden_files_to:
+ sha1 = '%s' % utils.get_HEAD_sha1()
+ with utils.ChangedWorkingDirectory(options.generate_golden_files_to):
+ archive = utils.create_archive(sha1)
+ utils.upload_file_to_cloud_storage(archive,
+ 'gs://r8-test-results/golden-files/' + archive)
+
if return_code != 0:
if options.archive_failures and os.name != 'nt':
archive_failures()
diff --git a/tools/upload_to_x20.py b/tools/upload_to_x20.py
index 1efacf5..2d08362 100755
--- a/tools/upload_to_x20.py
+++ b/tools/upload_to_x20.py
@@ -20,11 +20,10 @@
def parse_options():
return optparse.OptionParser().parse_args()
-def create_archive(name):
- tarname = '%s.tar.gz' % name
- with tarfile.open(tarname, 'w:gz') as tar:
- tar.add(name)
- return tarname
+def uploadFile(filename, dest):
+ print 'Uploading to %s' % dest
+ shutil.copyfile(filename, dest)
+ subprocess.check_call(['chmod', '664', dest])
def Main():
(options, args) = parse_options()
@@ -34,12 +33,10 @@
if not name in os.listdir('.'):
print 'You must be standing directly below the directory you are uploading'
return 1
- filename = create_archive(name)
+ filename = utils.create_archive(name)
sha1 = utils.get_sha1(filename)
dest = os.path.join(GMSCORE_DEPS, sha1)
- print 'Uploading to %s' % dest
- shutil.copyfile(filename, dest)
- subprocess.check_call(['chmod', '664', dest])
+ uploadFile(filename, dest)
sha1_file = '%s.sha1' % filename
with open(sha1_file, 'w') as output:
output.write(sha1)
diff --git a/tools/utils.py b/tools/utils.py
index 3aab572..859da97 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -10,6 +10,7 @@
import shutil
import subprocess
import sys
+import tarfile
import tempfile
TOOLS_DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..')))
@@ -71,6 +72,12 @@
sha1.update(chunk)
return sha1.hexdigest()
+def get_HEAD_sha1():
+ cmd = ['git', 'rev-parse', 'HEAD']
+ PrintCmd(cmd)
+ with ChangedWorkingDirectory(REPO_ROOT):
+ return subprocess.check_output(cmd).strip()
+
def makedirs_if_needed(path):
try:
os.makedirs(path)
@@ -92,6 +99,29 @@
PrintCmd(cmd)
subprocess.check_call(cmd)
+def download_file_from_cloud_storage(source, destination):
+ cmd = ['gsutil.py', 'cp', source, destination]
+ PrintCmd(cmd)
+ subprocess.check_call(cmd)
+
+def create_archive(name):
+ tarname = '%s.tar.gz' % name
+ with tarfile.open(tarname, 'w:gz') as tar:
+ tar.add(name)
+ return tarname
+
+def extract_dir(filename):
+ return filename[0:len(filename) - len('.tar.gz')]
+
+def unpack_archive(filename):
+ dest_dir = extract_dir(filename)
+ if os.path.exists(dest_dir):
+ print 'Deleting existing dir %s' % dest_dir
+ shutil.rmtree(dest_dir)
+ dirname = os.path.dirname(os.path.abspath(filename))
+ with tarfile.open(filename, 'r:gz') as tar:
+ tar.extractall(path=dirname)
+
# Note that gcs is eventually consistent with regards to list operations.
# This is not a problem in our case, but don't ever use this method
# for synchronization.