Version 1.2.34.
Merges in changes needed for certain bytecode rewritten tests that
enable assertions programatically and produces invalid locals
information.
Merge: Strip invalid locals information instead of failing compilation.
CL: https://r8-review.googlesource.com/c/r8/+/24240
Merge: Keep assertion code when required.
CL: https://r8-review.googlesource.com/c/r8/+/23921
Change-Id: I6dc8f0785e3c03814c099adbe8825e9267df0746
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 9fcca89..709e4f7 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -611,6 +611,13 @@
internal.proguardCompatibilityRulesOutput = proguardCompatibilityRulesOutput;
internal.dataResourceConsumer = internal.programConsumer.getDataResourceConsumer();
+ // Default is to remove Java assertion code as Dalvik and Art does not reliable support
+ // Java assertions. When generation class file output always keep the Java assertions code.
+ assert internal.disableAssertions;
+ if (internal.isGeneratingClassFiles()) {
+ internal.disableAssertions = false;
+ }
+
// EXPERIMENTAL flags.
assert !internal.forceProguardCompatibility;
internal.forceProguardCompatibility = forceProguardCompatibility;
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 5d9b912..2629bf7 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.33";
+ public static final String LABEL = "1.2.34";
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 026c2b5..0d47537 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -601,6 +601,7 @@
// Stores information about instance methods and constructors for
// class inliner, null value indicates that the method is not eligible.
private ClassInlinerEligibility classInlinerEligibility = null;
+ private boolean initializerEnablingJavaAssertions = false;
private OptimizationInfo() {
// Intentionally left empty.
@@ -645,6 +646,14 @@
return this.classInlinerEligibility;
}
+ private void setInitializerEnablingJavaAssertions() {
+ this.initializerEnablingJavaAssertions = true;
+ }
+
+ public boolean isInitializerEnablingJavaAssertions() {
+ return initializerEnablingJavaAssertions;
+ }
+
public long getReturnedConstant() {
assert returnsConstant();
return returnedConstant;
@@ -747,6 +756,10 @@
ensureMutableOI().setClassInlinerEligibility(eligibility);
}
+ synchronized public void setInitializerEnablingJavaAssertions() {
+ ensureMutableOI().setInitializerEnablingJavaAssertions();
+ }
+
synchronized public void markForceInline() {
ensureMutableOI().markForceInline();
}
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 65c59ae..ccbdf67 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
@@ -103,6 +103,7 @@
private final CovariantReturnTypeAnnotationTransformer covariantReturnTypeAnnotationTransformer;
private final OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
+ private final OptimizationFeedback simpleOptimizationFeedback = new OptimizationFeedbackSimple();
private DexString highestSortingString;
private IRConverter(
@@ -349,24 +350,34 @@
ExecutorService executor) throws ExecutionException {
List<Future<?>> futures = new ArrayList<>();
for (DexProgramClass clazz : classes) {
- futures.add(
- executor.submit(
- () -> {
- clazz.forEachMethodThrowing(this::convertMethodToDex);
- return null; // we want a Callable not a Runnable to be able to throw
- }));
+ futures.add(executor.submit(() -> convertMethodsToDex(clazz)));
}
ThreadUtils.awaitFutures(futures);
}
- void convertMethodToDex(DexEncodedMethod method) {
+ private void convertMethodsToDex(DexProgramClass clazz) {
+ // When converting all methods on a class always convert <clinit> first.
+ for (DexEncodedMethod method : clazz.directMethods()) {
+ if (method.isClassInitializer()) {
+ convertMethodToDex(method);
+ break;
+ }
+ }
+ clazz.forEachMethod(method -> {
+ if (!method.isClassInitializer()) {
+ convertMethodToDex(method);
+ }
+ });
+ }
+
+ private void convertMethodToDex(DexEncodedMethod method) {
assert options.isGeneratingDex();
if (method.getCode() != null) {
boolean matchesMethodFilter = options.methodMatchesFilter(method);
if (matchesMethodFilter) {
if (!(options.passthroughDexCode && method.getCode().isDexCode())) {
// We do not process in call graph order, so anything could be a leaf.
- rewriteCode(method, ignoreOptimizationFeedback, x -> true, CallSiteInformation.empty(),
+ rewriteCode(method, simpleOptimizationFeedback, x -> true, CallSiteInformation.empty(),
Outliner::noProcessing);
}
updateHighestSortingStrings(method);
@@ -659,7 +670,7 @@
codeRewriter.removeSwitchMaps(code);
}
if (options.disableAssertions) {
- codeRewriter.disableAssertions(code);
+ codeRewriter.disableAssertions(appInfo, method, code, feedback);
}
if (options.enableNonNullTracking && nonNullTracker != null) {
nonNullTracker.addNonNull(code);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
index 99bca1c..b67d12e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/JarState.java
@@ -339,6 +339,12 @@
}
int start = source.getOffset(node.start);
int end = source.getOffset(node.end);
+ // If the locals information is invalid the node start or end could be a label that does
+ // not exist in the program.
+ if (start == -1 || end == -1) {
+ throw new InvalidDebugInfoException(
+ "Locals information for '" + node.name + "' has undefined start or end point.");
+ }
// Ensure that there is an entry at the starting point of the local.
{
LocalsAtOffset atStart;
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 16a77c8..0c9fb42 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
@@ -17,4 +17,5 @@
void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark);
void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
+ void setInitializerEnablingJavaAssertions(DexEncodedMethod method);
}
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 7303b6b..254c699 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
@@ -50,4 +50,8 @@
DexEncodedMethod method, ClassInlinerEligibility eligibility) {
method.setClassInlinerEligibility(eligibility);
}
+ @Override
+ public void setInitializerEnablingJavaAssertions(DexEncodedMethod method) {
+ method.setInitializerEnablingJavaAssertions();
+ }
}
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 6915a2f..98a289b 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
@@ -35,4 +35,8 @@
public void setClassInlinerEligibility(
DexEncodedMethod method, ClassInlinerEligibility eligibility) {
}
+
+ @Override
+ public void setInitializerEnablingJavaAssertions(DexEncodedMethod method) {
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
new file mode 100644
index 0000000..12456f4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
@@ -0,0 +1,59 @@
+// Copyright (c) 2017, 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.conversion;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.ClassInlinerEligibility;
+import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+
+public class OptimizationFeedbackSimple implements OptimizationFeedback {
+
+ @Override
+ public void methodReturnsArgument(DexEncodedMethod method, int argument) {
+ // Ignored.
+ }
+
+ @Override
+ public void methodReturnsConstant(DexEncodedMethod method, long value) {
+ // Ignored.
+ }
+
+ @Override
+ public void methodNeverReturnsNull(DexEncodedMethod method) {
+ // Ignored.
+ }
+
+ @Override
+ public void methodNeverReturnsNormally(DexEncodedMethod method) {
+ // Ignored.
+ }
+
+ @Override
+ public void markProcessed(DexEncodedMethod method, Constraint state) {
+ // Just as processed, don't provide any inlining constraints.
+ method.markProcessed(Constraint.NEVER);
+ }
+
+ @Override
+ public void markCheckNullReceiverBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
+ // Ignored.
+ }
+
+ @Override
+ public void markTriggerClassInitBeforeAnySideEffect(DexEncodedMethod method, boolean mark) {
+ // Ignored.
+ }
+
+ @Override
+ public void setClassInlinerEligibility(
+ DexEncodedMethod method, ClassInlinerEligibility eligibility) {
+ // Ignored.
+ }
+
+ @Override
+ public void setInitializerEnablingJavaAssertions(DexEncodedMethod method) {
+ method.setInitializerEnablingJavaAssertions();
+ }
+}
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 bc083c9..672fb4f 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
@@ -1041,7 +1041,29 @@
* }
* </pre>
*/
- public void disableAssertions(IRCode code) {
+ public void disableAssertions(
+ AppInfo appInfo, DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+ if (method.isClassInitializer()) {
+ if (!hasJavacClinitAssertionCode(code)) {
+ return;
+ }
+ // Mark the clinit as having code to turn on assertions.
+ feedback.setInitializerEnablingJavaAssertions(method);
+ } else {
+ // If the clinit of this class did not have have code to turn on assertions don't try to
+ // remove assertion code from the method.
+ DexClass clazz = appInfo.definitionFor(method.method.holder);
+ if (clazz == null) {
+ return;
+ }
+ DexEncodedMethod clinit = clazz.getClassInitializer();
+ if (clinit == null
+ || !clinit.isProcessed()
+ || !clinit.getOptimizationInfo().isInitializerEnablingJavaAssertions()) {
+ return;
+ }
+ }
+
InstructionIterator iterator = code.instructionIterator();
while (iterator.hasNext()) {
Instruction current = iterator.next();
@@ -1064,6 +1086,78 @@
}
}
+ private boolean isClassDesiredAssertionStatusInvoke(Instruction instruction) {
+ return instruction.isInvokeMethod()
+ && instruction.asInvokeMethod().getInvokedMethod()
+ == dexItemFactory.classMethods.desiredAssertionStatus;
+ }
+
+ private boolean isAssertionsDisabledFieldPut(Instruction instruction) {
+ return instruction.isStaticPut()
+ && instruction.asStaticPut().getField().name == dexItemFactory.assertionsDisabled;
+ }
+
+ private boolean isNotDebugInstruction(Instruction instruction) {
+ return !instruction.isDebugInstruction();
+ }
+
+ private Value blockWithSingleConstNumberAndGoto(BasicBlock block) {
+ InstructionIterator iterator = block.iterator();
+ Instruction constNumber = iterator.nextUntil(this::isNotDebugInstruction);
+ if (constNumber == null || !constNumber.isConstNumber()) {
+ return null;
+ }
+ Instruction exit = iterator.nextUntil(this::isNotDebugInstruction);
+ return exit != null && exit.isGoto() ? constNumber.outValue() : null;
+ }
+
+ private Value blockWithAssertionsDisabledFieldPut(BasicBlock block) {
+ InstructionIterator iterator = block.iterator();
+ Instruction fieldPut = iterator.nextUntil(this::isNotDebugInstruction);
+ return fieldPut != null
+ && isAssertionsDisabledFieldPut(fieldPut) ? fieldPut.inValues().get(0) : null;
+ }
+
+ private boolean hasJavacClinitAssertionCode(IRCode code) {
+ InstructionIterator iterator = code.instructionIterator();
+ Instruction current = iterator.nextUntil(this::isClassDesiredAssertionStatusInvoke);
+ if (current == null) {
+ return false;
+ }
+
+ Value DesiredAssertionStatus = current.outValue();
+ assert iterator.hasNext();
+ current = iterator.next();
+ if (!current.isIf()
+ || !current.asIf().isZeroTest()
+ || current.asIf().inValues().get(0) != DesiredAssertionStatus) {
+ return false;
+ }
+
+ If theIf = current.asIf();
+ BasicBlock trueTarget = theIf.getTrueTarget();
+ BasicBlock falseTarget = theIf.fallthroughBlock();
+ if (trueTarget == falseTarget) {
+ return false;
+ }
+
+ Value trueValue = blockWithSingleConstNumberAndGoto(trueTarget);
+ Value falseValue = blockWithSingleConstNumberAndGoto(falseTarget);
+ if (trueValue == null
+ || falseValue == null
+ || (trueTarget.exit().asGoto().getTarget() != falseTarget.exit().asGoto().getTarget())) {
+ return false;
+ }
+
+ BasicBlock target = trueTarget.exit().asGoto().getTarget();
+ Value storeValue = blockWithAssertionsDisabledFieldPut(target);
+ return storeValue != null
+ && storeValue.isPhi()
+ && storeValue.asPhi().getOperands().size() == 2
+ && storeValue.asPhi().getOperands().contains(trueValue)
+ && storeValue.asPhi().getOperands().contains(falseValue);
+ }
+
// Check if the static put is a constant derived from the class holding the method.
// This checks for java.lang.Class.getName and java.lang.Class.getSimpleName.
private boolean isClassNameConstant(DexEncodedMethod method, StaticPut put) {
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/ChromuimAssertionHookMock.java b/src/test/java/com/android/tools/r8/rewrite/assertions/ChromuimAssertionHookMock.java
new file mode 100644
index 0000000..772ef09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/ChromuimAssertionHookMock.java
@@ -0,0 +1,11 @@
+// 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.rewrite.assertions;
+
+public class ChromuimAssertionHookMock {
+ public static void assertFailureHandler(AssertionError assertion) {
+ System.out.println("Got AssertionError " + assertion);
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java b/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java
index cc56790..49fe85e 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/ClassWithAssertions.java
@@ -16,7 +16,9 @@
}
int getX() {
+ System.out.println("1");
assert condition();
+ System.out.println("2");
return x;
}
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java b/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
index 3c724c0..c118bad 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/RemoveAssertionsTest.java
@@ -4,17 +4,139 @@
package com.android.tools.r8.rewrite.assertions;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8Command;
import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.utils.AndroidApp;
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 com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import java.util.function.Consumer;
+import java.util.function.Function;
import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+// This ASM class visitor has been adapted from
+// https://chromium.googlesource.com/chromium/src/+/164e81fcd0828b40f5496e9025349ea728cde7f5/build/android/bytecode/java/org/chromium/bytecode/AssertionEnablerClassAdapter.java
+// See b/110887293.
+
+/**
+ * An ClassVisitor for replacing Java ASSERT statements with a function by modifying Java bytecode.
+ *
+ * We do this in two steps, first step is to enable assert.
+ * Following bytecode is generated for each class with ASSERT statements:
+ * 0: ldc #8 // class CLASSNAME
+ * 2: invokevirtual #9 // Method java/lang/Class.desiredAssertionStatus:()Z
+ * 5: ifne 12
+ * 8: iconst_1
+ * 9: goto 13
+ * 12: iconst_0
+ * 13: putstatic #2 // Field $assertionsDisabled:Z
+ * Replaces line #13 to the following:
+ * 13: pop
+ * Consequently, $assertionsDisabled is assigned the default value FALSE.
+ * This is done in the first if statement in overridden visitFieldInsn. We do this per per-assert.
+ *
+ * Second step is to replace assert statement with a function:
+ * The followed instructions are generated by a java assert statement:
+ * getstatic #3 // Field $assertionsDisabled:Z
+ * ifne 118 // Jump to instruction as if assertion if not enabled
+ * ...
+ * ifne 19
+ * new #4 // class java/lang/AssertionError
+ * dup
+ * ldc #5 // String (don't have this line if no assert message given)
+ * invokespecial #6 // Method java/lang/AssertionError.
+ * athrow
+ * Replace athrow with:
+ * invokestatic #7 // Method org/chromium/base/JavaExceptionReporter.assertFailureHandler
+ * goto 118
+ * JavaExceptionReporter.assertFailureHandler is a function that handles the AssertionError,
+ * 118 is the instruction to execute as if assertion if not enabled.
+ */
+class AssertionEnablerClassAdapter extends ClassVisitor {
+ AssertionEnablerClassAdapter(ClassVisitor visitor) {
+ super(Opcodes.ASM6, visitor);
+ }
+
+ @Override
+ public MethodVisitor visitMethod(final int access, final String name, String desc,
+ String signature, String[] exceptions) {
+ return new RewriteAssertMethodVisitor(
+ Opcodes.ASM5, super.visitMethod(access, name, desc, signature, exceptions));
+ }
+
+ static class RewriteAssertMethodVisitor extends MethodVisitor {
+ static final String ASSERTION_DISABLED_NAME = "$assertionsDisabled";
+ static final String INSERT_INSTRUCTION_NAME = "assertFailureHandler";
+ static final String INSERT_INSTRUCTION_DESC =
+ Type.getMethodDescriptor(Type.VOID_TYPE, Type.getObjectType("java/lang/AssertionError"));
+ static final boolean INSERT_INSTRUCTION_ITF = false;
+
+ boolean mStartLoadingAssert;
+ Label mGotoLabel;
+
+ public RewriteAssertMethodVisitor(int api, MethodVisitor mv) {
+ super(api, mv);
+ }
+
+ @Override
+ public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+ if (opcode == Opcodes.PUTSTATIC && name.equals(ASSERTION_DISABLED_NAME)) {
+ super.visitInsn(Opcodes.POP); // enable assert
+ } else if (opcode == Opcodes.GETSTATIC && name.equals(ASSERTION_DISABLED_NAME)) {
+ mStartLoadingAssert = true;
+ super.visitFieldInsn(opcode, owner, name, desc);
+ } else {
+ super.visitFieldInsn(opcode, owner, name, desc);
+ }
+ }
+
+ @Override
+ public void visitJumpInsn(int opcode, Label label) {
+ if (mStartLoadingAssert && opcode == Opcodes.IFNE && mGotoLabel == null) {
+ mGotoLabel = label;
+ }
+ super.visitJumpInsn(opcode, label);
+ }
+
+ @Override
+ public void visitInsn(int opcode) {
+ if (!mStartLoadingAssert || opcode != Opcodes.ATHROW) {
+ super.visitInsn(opcode);
+ } else {
+ super.visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ ChromuimAssertionHookMock.class.getCanonicalName().replace('.', '/'),
+ INSERT_INSTRUCTION_NAME,
+ INSERT_INSTRUCTION_DESC,
+ INSERT_INSTRUCTION_ITF);
+ super.visitJumpInsn(Opcodes.GOTO, mGotoLabel);
+ mStartLoadingAssert = false;
+ mGotoLabel = null;
+ }
+ }
+ }
+}
public class RemoveAssertionsTest extends TestBase {
@@ -38,4 +160,73 @@
clazz.method(new MethodSignature(Constants.CLASS_INITIALIZER_NAME, "void", new String[]{}));
assertTrue(!clinit.isPresent());
}
+
+ private Path buildTestToCf(Consumer<InternalOptions> consumer) throws Exception {
+ Path outputJar = temp.getRoot().toPath().resolve("output.jar");
+ R8Command command =
+ ToolHelper.prepareR8CommandBuilder(readClasses(ClassWithAssertions.class))
+ .setMode(CompilationMode.DEBUG)
+ .setOutput(outputJar, OutputMode.ClassFile)
+ .build();
+ ToolHelper.runR8(command, consumer);
+ return outputJar;
+ }
+
+ @Test
+ public void testCfOutput() throws Exception {
+ String main = ClassWithAssertions.class.getCanonicalName();
+ ProcessResult result;
+ // Assertion is hit.
+ result = ToolHelper.runJava(buildTestToCf(options -> {}), "-ea", main, "0");
+ assertEquals(1, result.exitCode);
+ assertEquals("1\n", result.stdout);
+ // Assertion is not hit.
+ result = ToolHelper.runJava(buildTestToCf(options -> {}), "-ea", main, "1");
+ assertEquals(0, result.exitCode);
+ assertEquals("1\n2\n", result.stdout);
+ // Assertion is hit, but removed.
+ result = ToolHelper.runJava(
+ buildTestToCf(
+ options -> options.disableAssertions = true), "-ea", main, "0");
+ assertEquals(0, result.exitCode);
+ assertEquals("1\n2\n", result.stdout);
+ }
+
+ private byte[] identity(byte[] classBytes) {
+ return classBytes;
+ }
+
+ private byte[] chromiumAssertionEnabler(byte[] classBytes) {
+ ClassWriter writer = new ClassWriter(0);
+ new ClassReader(classBytes).accept(new AssertionEnablerClassAdapter(writer), 0);
+ return writer.toByteArray();
+ }
+
+ private AndroidApp runRegress110887293(Function<byte[], byte[]> rewriter) throws Exception {
+ return ToolHelper.runR8(
+ R8Command.builder()
+ .addClassProgramData(
+ rewriter.apply(ToolHelper.getClassAsBytes(ClassWithAssertions.class)),
+ Origin.unknown())
+ .addClassProgramData(
+ ToolHelper.getClassAsBytes(ChromuimAssertionHookMock.class), Origin.unknown())
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .setMode(CompilationMode.DEBUG)
+ .build());
+ }
+
+ @Test
+ public void regress110887293() throws Exception {
+ AndroidApp app;
+ // Assertions removed for default assertion code.
+ app = runRegress110887293(this::identity);
+ assertEquals("1\n2\n", runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "0"));
+ assertEquals("1\n2\n", runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "1"));
+ // Assertions not removed when default assertion code is not present.
+ app = runRegress110887293(this::chromiumAssertionEnabler);
+ assertEquals(
+ "1\nGot AssertionError java.lang.AssertionError\n2\n",
+ runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "0"));
+ assertEquals("1\n2\n", runOnArt(app, ClassWithAssertions.class.getCanonicalName(), "1"));
+ }
}