blob: ea5ca6696d5f284606ac271ee29023d7064c0ddb [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.deployer;
import com.android.tools.deploy.proto.Deploy;
import com.android.tools.deployer.model.ApkEntry;
import com.android.tools.deployer.model.DexClass;
import com.android.tools.r8.ByteDataView;
import com.android.tools.r8.D8;
import com.android.tools.r8.D8Command;
import com.android.tools.r8.DexIndexedConsumer;
import com.android.tools.r8.DiagnosticsHandler;
import com.android.tools.r8.origin.Origin;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.zip.CRC32;
import org.junit.Assert;
import org.junit.Test;
/**
* Mainly focus on unit testing split and checksum functions in the dex splitter.
*
* <p>Interaction with database and such will be left for {@link DexComparatorTest}.
*/
public class D8DexSplitterTest {
/** Verify that we are using D8's .class crc checksum when available. */
@Test
public void testD8EncodedClassCrc() throws Exception {
// Any random class would do but we are going to do this inception style.
Class target = this.getClass();
// We will keep class that matches .class file crc of the target.
long keepCrc = crc(buildClass(target));
ApkEntry input = new InMemoryDexFile(true, target);
D8InMemoryDexSplitter splitter = new D8InMemoryDexSplitter();
Collection<DexClass> result = splitter.split(input, d -> d.checksum == keepCrc);
Assert.assertNotNull(findClass(result, target).code);
}
/** Do a typical split, cache and diff operation without encoded checksum. */
@Test
public void testSplitWithoutD8EncodeChecksum() {
Class target = SimpleJavaClass.class;
// Split it once, simulating a database cache on deploy
ApkEntry input = new InMemoryDexFile(false, target);
final Map<String, Long> crcs = new HashMap<>();
D8InMemoryDexSplitter splitter = new D8InMemoryDexSplitter();
Collection<DexClass> result =
splitter.split(
input,
clz -> {
crcs.put(clz.name, clz.checksum);
return false;
});
Assert.assertNull(findClass(result, target).code);
// Throw in some random class, it should affect the outcome.
new InMemoryDexFile(false, target, this.getClass());
splitter = new D8InMemoryDexSplitter();
result = splitter.split(input, clz -> crcs.get(target.getName()).equals(clz.checksum));
Assert.assertNotNull(findClass(result, target).code);
}
/** Do a typical split, cache and diff operation with encoded checksum. */
@Test
public void testSplitWithD8EncodeChecksum() {
Class target = SimpleJavaClass.class;
// Split it once, simulating a database cache on deploy
ApkEntry input = new InMemoryDexFile(true, target);
final Map<String, Long> crcs = new HashMap<>();
D8InMemoryDexSplitter splitter = new D8InMemoryDexSplitter();
Collection<DexClass> result =
splitter.split(
input,
clz -> {
crcs.put(clz.name, clz.checksum);
return false;
});
Assert.assertNull(findClass(result, target).code);
// Throw in some random class, it should affect the outcome.
new InMemoryDexFile(true, target, this.getClass());
splitter = new D8InMemoryDexSplitter();
result = splitter.split(input, clz -> crcs.get(target.getName()).equals(clz.checksum));
Assert.assertNotNull(findClass(result, target).code);
}
@Test
public void testStaticInitValues() {
Class<StaticPrimitiveClass> target = StaticPrimitiveClass.class;
ApkEntry input = new InMemoryDexFile(true, target);
D8InMemoryDexSplitter splitter = new D8InMemoryDexSplitter();
Collection<DexClass> result = splitter.split(input, d -> true);
DexClass splittedTarget = findClass(result, target);
Assert.assertNotNull(splittedTarget);
Deploy.ClassDef.FieldReInitState state = null;
state = findVariableState(splittedTarget.variableStates, "int1");
Assert.assertNotNull(state);
Assert.assertEquals("I", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(1, Integer.parseInt(state.getValue()));
state = findVariableState(splittedTarget.variableStates, "boolFalse");
Assert.assertNotNull(state);
Assert.assertEquals("Z", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(false, Boolean.parseBoolean(state.getValue()));
state = findVariableState(splittedTarget.variableStates, "byte3");
Assert.assertNotNull(state);
Assert.assertEquals("B", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(3, Byte.parseByte(state.getValue()));
state = findVariableState(splittedTarget.variableStates, "charK");
Assert.assertNotNull(state);
Assert.assertEquals("C", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals("k", state.getValue());
state = findVariableState(splittedTarget.variableStates, "double15");
Assert.assertNotNull(state);
Assert.assertEquals("D", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(15.0, Double.parseDouble(state.getValue()), 0);
state = findVariableState(splittedTarget.variableStates, "float13");
Assert.assertNotNull(state);
Assert.assertEquals("F", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(13.0, Float.parseFloat(state.getValue()), 0);
state = findVariableState(splittedTarget.variableStates, "long17");
Assert.assertNotNull(state);
Assert.assertEquals("J", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(17l, Long.parseLong(state.getValue()));
state = findVariableState(splittedTarget.variableStates, "short22");
Assert.assertNotNull(state);
Assert.assertEquals("S", state.getType());
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(22, Short.parseShort(state.getValue()));
state = findVariableState(splittedTarget.variableStates, "notStatic");
Assert.assertNotNull(state);
Assert.assertFalse(state.getStaticVar());
state = findVariableState(splittedTarget.variableStates, "notFinal");
Assert.assertNotNull(state);
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.UNKNOWN, state.getState());
state = findVariableState(splittedTarget.variableStates, "notFound");
Assert.assertNull(state);
state = findVariableState(splittedTarget.variableStates, "invokedFunction");
Assert.assertNotNull(state);
Assert.assertTrue(state.getStaticVar());
Assert.assertEquals(
Deploy.ClassDef.FieldReInitState.VariableState.CONSTANT, state.getState());
Assert.assertEquals(0, Integer.parseInt(state.getValue()));
}
/** Return the first varaible state with a given variable name. */
private static Deploy.ClassDef.FieldReInitState findVariableState(
ImmutableList<Deploy.ClassDef.FieldReInitState> states, String name) {
for (Deploy.ClassDef.FieldReInitState state : states) {
if (state.getName().equals(name)) {
return state;
}
}
return null;
}
private static class D8InMemoryDexSplitter extends D8DexSplitter {
@Override
protected byte[] readDex(ApkEntry entry) {
InMemoryDexFile dex =
(InMemoryDexFile) entry; // This test will not work with actual files.
return buildDex(dex.encodedChecksum, dex.classes);
}
}
private static class InMemoryDexFile extends ApkEntry {
private Class[] classes;
private boolean encodedChecksum;
private InMemoryDexFile(boolean encodedChecksum, Class... classes) {
super("classes.dex", 0, null);
this.classes = classes;
this.encodedChecksum = encodedChecksum;
}
}
private static long crc(byte[] data) {
CRC32 crc = new CRC32();
crc.update(data);
return crc.getValue();
}
// Local dex class by simple name.
private static DexClass findClass(Collection<DexClass> collection, Class c) {
for (DexClass dexClass : collection) {
if (dexClass.name.equals(c.getName())) {
return dexClass;
}
}
Assert.fail(c.getName() + " was not found in collection.");
return null;
}
private static byte[] buildClass(Class clazz) throws IOException {
return ByteStreams.toByteArray(clazz.getResourceAsStream(clazz.getSimpleName() + ".class"));
}
public static byte[] buildDex(boolean checksum, Class... classes) {
DexBytesConsumer dexBytesConsumer = new DexBytesConsumer();
try {
D8Command.Builder builder =
D8Command.builder()
.setIncludeClassesChecksum(checksum)
.setProgramConsumer(dexBytesConsumer)
.setMinApiLevel(28)
.setDisableDesugaring(true);
for (Class c : classes) {
builder.addClassProgramData(buildClass(c), Origin.unknown());
}
D8.run(builder.build());
} catch (Exception e) {
Assert.fail(e.toString());
}
return dexBytesConsumer.dexData;
}
private static class DexBytesConsumer implements DexIndexedConsumer {
private byte[] dexData = null;
@Override
public void accept(
int fileIndex,
ByteDataView data,
Set<String> descriptors,
DiagnosticsHandler handler) {
Assert.assertEquals(0, fileIndex);
Assert.assertNull(dexData);
dexData = data.copyByteData();
}
@Override
public void finished(DiagnosticsHandler handler) {
Assert.assertNotNull(dexData);
}
}
}