blob: 9b1afb2be4959c26f67baa70fd3a4d084764d60e [file] [log] [blame]
/*
* Copyright (C) 2016 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.jack.coverage;
import static org.junit.Assert.assertEquals;
import com.google.common.collect.Sets;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.android.jack.test.TestsProperties;
import com.android.jack.test.junit.KnownIssue;
import com.android.jack.test.toolchain.AbstractTestTools;
import com.android.jack.test.toolchain.JackApiV03Toolchain;
import com.android.jack.test.toolchain.JackApiV04Toolchain;
import com.android.jack.test.toolchain.JackBasedToolchain;
import com.android.jack.test.toolchain.JackCliToolchain;
import com.android.jack.util.NamingTools;
import com.android.sched.util.file.CannotChangePermissionException;
import com.android.sched.util.file.CannotCreateFileException;
import com.android.sched.util.file.WrongPermissionException;
import junit.framework.Assert;
import org.junit.Test;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
public class CoverageTests {
@Nonnull
private static final String JACOCO_RUNTIME_PACKAGE = "org.jacoco.agent.rt.internal_04864a1";
@Nonnull private static final String COVERAGE_TEST_PACKAGE = "com.android.jack.coverage";
@Test
public void testSingleClass() throws Exception {
final String testPackageName = getTestPackageName("test001");
JsonArray classes = compileAndReadJson(testPackageName);
// We expect only one class
Assert.assertNotNull(classes);
Assert.assertEquals(1, classes.size());
JsonObject testClass = classes.get(0).getAsJsonObject();
Assert.assertNotNull(testClass);
Assert.assertEquals(
getClassNameForJson(testPackageName + ".AbstractClass"),
getClassName(testClass));
// Check its methods.
JsonArray methods = testClass.get("methods").getAsJsonArray();
Assert.assertEquals(3, methods.size()); // abstract and native methods are excluded.
class NameAndDesc {
@Nonnull public final String name;
@Nonnull public final String desc;
public NameAndDesc(@Nonnull String name, @Nonnull String desc) {
this.name = name;
this.desc = desc;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof NameAndDesc) {
NameAndDesc other = (NameAndDesc) obj;
return name.equals(other.name) && desc.equals(other.desc);
} else {
return false;
}
}
@Override
public int hashCode() {
String concat = name + desc;
return concat.hashCode();
}
}
Set<NameAndDesc> expectedMethods = new HashSet<NameAndDesc>();
expectedMethods.add(new NameAndDesc(NamingTools.INIT_NAME, "()V"));
expectedMethods.add(new NameAndDesc("staticMethod", "()V"));
expectedMethods.add(new NameAndDesc("instanceMethod", "()V"));
for (JsonElement methodElt : methods) {
String name = methodElt.getAsJsonObject().get("name").getAsString();
String desc = methodElt.getAsJsonObject().get("desc").getAsString();
Assert.assertTrue(expectedMethods.contains(new NameAndDesc(name, desc)));
}
}
@Nonnull
private static String getClassName(JsonArray classArray, int index) {
return getClassName(classArray.get(index).getAsJsonObject());
}
@Nonnull
private static String getClassName(JsonObject classObject) {
return classObject.get("name").getAsString();
}
@Test
public void testSingleInterface() throws Exception {
JsonArray classes = compileAndReadJson(getTestPackageName("test002"));
// Interface must be skipped
Assert.assertEquals(0, classes.size());
}
@Test
public void testIncludeAll() throws Exception {
String testPackageName = getTestPackageName("test003");
JsonArray classes = compileAndReadJson(testPackageName, "*", null);
// Full coverage: Foo and Bar classes.
Assert.assertEquals(2, classes.size());
Collection<? extends String> actualClassNames = collectClassNames(classes);
Set<String> expectedClassNames = Sets.newHashSet(
getClassNameForJson(testPackageName + ".foo.Foo"),
getClassNameForJson(testPackageName + ".foo.bar.Bar"));
Assert.assertTrue(actualClassNames.containsAll(expectedClassNames));
}
@Test
public void testIncludeSingle() throws Exception {
String testPackageName = getTestPackageName("test003");
String includeFilter = testPackageName + ".foo.bar.Bar";
JsonArray classes = compileAndReadJson(testPackageName, includeFilter, null);
// Partial coverage: only Bar class.
Assert.assertEquals(1, classes.size());
Collection<? extends String> actualClassNames = collectClassNames(classes);
Set<String> expectedClassNames = Sets.newHashSet(
getClassNameForJson(testPackageName + ".foo.bar.Bar"));
Assert.assertTrue(actualClassNames.containsAll(expectedClassNames));
}
@Test
public void testIncludeWildcard() throws Exception {
String testPackageName = getTestPackageName("test003");
String includeFilter = testPackageName + ".foo.bar.*";
JsonArray classes = compileAndReadJson(testPackageName, includeFilter, null);
// Partial coverage: only Bar class.
Assert.assertEquals(1, classes.size());
Collection<? extends String> actualClassNames = collectClassNames(classes);
Set<String> expectedClassNames = Sets.newHashSet(
getClassNameForJson(testPackageName + ".foo.bar.Bar"));
Assert.assertTrue(actualClassNames.containsAll(expectedClassNames));
}
@Test
public void testExcludeAll() throws Exception {
JsonArray classes = compileAndReadJson(getTestPackageName("test003"), null, "*");
// No coverage at all
Assert.assertEquals(0, classes.size());
}
@Test
public void testExcludeSingle() throws Exception {
String testPackageName = getTestPackageName("test003");
String excludeFilter = testPackageName + ".foo.bar.Bar";
JsonArray classes = compileAndReadJson(getTestPackageName("test003"), null, excludeFilter);
// Partial coverage: only Foo class.
Assert.assertEquals(1, classes.size());
Collection<? extends String> actualClassNames = collectClassNames(classes);
Set<String> expectedClassNames = Sets.newHashSet(
getClassNameForJson(testPackageName + ".foo.Foo"));
Assert.assertTrue(actualClassNames.containsAll(expectedClassNames));
}
@Test
public void testExcludeWildcard() throws Exception {
String testPackageName = getTestPackageName("test003");
String excludeFilter = testPackageName + ".foo.bar.*";
JsonArray classes = compileAndReadJson(testPackageName, null, excludeFilter);
// Partial coverage: only Foo class.
Assert.assertEquals(1, classes.size());
Collection<? extends String> actualClassNames = collectClassNames(classes);
Set<String> expectedClassNames = Sets.newHashSet(
getClassNameForJson(testPackageName + ".foo.Foo"));
Assert.assertTrue(actualClassNames.containsAll(expectedClassNames));
}
@Test
public void testIncludeExclude() throws Exception {
String testPackageName = getTestPackageName("test003");
String includeFilter = testPackageName + ".foo.bar.*";
String excludeFilter = testPackageName + ".foo.*";
JsonArray classes = compileAndReadJson(testPackageName, includeFilter, excludeFilter);
// No coverage at all
Assert.assertEquals(0, classes.size());
}
@Test
public void testPreDex() throws Exception {
String testPackageName = getTestPackageName("test004");
// 1 - Create a lib.
JackBasedToolchain toolchain = createJackToolchain();
File libDir = AbstractTestTools.createTempDir();
File libSrcFiles = new File(getTestRootDir(testPackageName), "lib");
toolchain.srcToLib(libDir, false, libSrcFiles);
// 2 - Compile the lib with coverage.
toolchain = createJackToolchain();
toolchain.addStaticLibs(libDir);
File coverageMetadataFile = enableCodeCoverage(toolchain, null, null);
File srcFiles = new File(getTestRootDir(testPackageName), "src");
File outDexFolder = AbstractTestTools.createTempDir();
toolchain.srcToExe(outDexFolder, false, srcFiles);
// 3 - Check types from the lib are instrumented.
JsonArray classes = loadJsonCoverageClasses(coverageMetadataFile);
JsonObject testClass =
getJsonClass(classes, getClassNameForJson(testPackageName + ".lib.LibClass"));
JsonArray probesArray = testClass.get("probes").getAsJsonArray();
Assert.assertNotNull(probesArray);
Assert.assertTrue(probesArray.size() > 0);
}
@Test
@KnownIssue // flaky due to non-determinism
public void testClassId_005() throws Exception {
String testPackageName = getTestPackageName("test005");
File testRootDir = getTestRootDir(testPackageName);
final String className = getClassNameForJson(testPackageName + ".jack.LibClass");
long classIdOne;
long classIdTwo;
long classIdThree;
JackBasedToolchain toolchain;
// Compile with coverage only
{
toolchain = createJackToolchain();
File coverageFileOne = enableCodeCoverage(toolchain, null, null);
File outDexFolderOne = AbstractTestTools.createTempDir();
toolchain.srcToExe(outDexFolderOne, false, testRootDir);
classIdOne = getClassIdOf(coverageFileOne, className);
}
// Compile with coverage only again into a different coverage file.
{
toolchain = createJackToolchain();
File coverageFileTwo = enableCodeCoverage(toolchain, null, null);
File outDexFolderTwo = AbstractTestTools.createTempDir();
toolchain.srcToExe(outDexFolderTwo, false, testRootDir);
classIdTwo = getClassIdOf(coverageFileTwo, className);
}
// Compile with coverage *and* proguard to shrink LibClass so it loses its unusedMethod.
{
toolchain = createJackToolchain();
File coverageFileThree = enableCodeCoverage(toolchain, null, null);
File proguardFile = new File(testRootDir, "proguard.flags");
toolchain.addProguardFlags(proguardFile);
File outDexFolderThree = AbstractTestTools.createTempDir();
toolchain.srcToExe(outDexFolderThree, false, testRootDir);
classIdThree = getClassIdOf(coverageFileThree, className);
}
// We should generate the same class ID for the same class.
Assert.assertEquals("Expected same class IDs", classIdOne, classIdTwo);
// We should generate different class IDs when they are different (after shrinking here).
assertNotEquals("Expected different class IDs", classIdOne, classIdThree);
}
@Test
public void testClassId_006() throws Exception {
String testPackageNameV1 = getTestPackageName("test006_v1");
File testRootDirV1 = getTestRootDir(testPackageNameV1);
String testPackageNameV2 = getTestPackageName("test006_v2");
File testRootDirV2 = getTestRootDir(testPackageNameV2);
final String className = getClassNameForJson("jack.SrcClass");
long classIdV1;
long classIdV2;
long classIdV2_2;
JackBasedToolchain toolchain;
// Compile with coverage the version v1
{
toolchain = createJackToolchain();
File coverageFileOne = enableCodeCoverage(toolchain, null, null);
File outDexFolderOne = AbstractTestTools.createTempDir();
toolchain.srcToExe(outDexFolderOne, false, testRootDirV1);
classIdV1 = getClassIdOf(coverageFileOne, className);
}
// Compile with coverage the version v2
{
toolchain = createJackToolchain();
File coverageFileTwo = enableCodeCoverage(toolchain, null, null);
File outDexFolderTwo = AbstractTestTools.createTempDir();
toolchain.srcToExe(outDexFolderTwo, false, testRootDirV2);
classIdV2 = getClassIdOf(coverageFileTwo, className);
}
// Compile with coverage the version v2 again
{
toolchain = createJackToolchain();
File coverageFileThree = enableCodeCoverage(toolchain, null, null);
File outDexFolderThree = AbstractTestTools.createTempDir();
toolchain.srcToExe(outDexFolderThree, false, testRootDirV2);
classIdV2_2 = getClassIdOf(coverageFileThree, className);
}
// We should generate different class IDs when they are different (after shrinking here).
assertNotEquals("Expected different class IDs", classIdV1, classIdV2);
assertEquals("Expected different class IDs", classIdV2, classIdV2_2);
}
private static void assertNotEquals(@Nonnull String msg, long expected, long actual) {
if (expected == actual) {
StringBuilder stringBuilder = new StringBuilder(msg);
stringBuilder.append(": expected=");
stringBuilder.append(Long.toHexString(expected));
stringBuilder.append(", actual=");
stringBuilder.append(Long.toHexString(actual));
Assert.fail(stringBuilder.toString());
}
}
private static long getClassIdOf(@Nonnull File coverageFile, @Nonnull String className)
throws IOException {
JsonArray classesArray = loadJsonCoverageClasses(coverageFile);
for (int i = 0, e = classesArray.size(); i < e; ++i) {
JsonObject classObject = classesArray.get(i).getAsJsonObject();
String jsonClassName = classObject.get("name").getAsString();
if (className.equals(jsonClassName)) {
long id = classObject.get("id").getAsLong();
return id;
}
}
throw new AssertionError("No class " + className + " in coverage file");
}
@Nonnull
private static JsonObject getJsonClass(
@Nonnull JsonArray jsonClasses, @Nonnull String className) {
for (JsonElement jsonElement : jsonClasses) {
JsonObject jsonClass = jsonElement.getAsJsonObject();
if (jsonClass.get("name").getAsString().equals(className)) {
return jsonClass;
}
}
throw new AssertionError("No class " + className);
}
@Nonnull
private static String getClassNameForJson(@Nonnull String className) {
return NamingTools.getBinaryName(className);
}
@Nonnull
private Collection<? extends String> collectClassNames(@Nonnull JsonArray classes) {
Set<String> classNames = new HashSet<String>();
for (JsonElement arrayElt: classes) {
classNames.add(getClassName(arrayElt.getAsJsonObject()));
}
return classNames;
}
@Nonnull
private static JackBasedToolchain createJackToolchain() {
JackBasedToolchain toolchain =
AbstractTestTools.getCandidateToolchain(JackBasedToolchain.class);
toolchain.addToClasspath(toolchain.getDefaultBootClasspath());
return toolchain;
}
/**
* Enable code coverage for the given toolchain.
*
* @param toolchain the toolchain to configure with code coverage
* @param includeFilter the 'include' class filter
* @param excludeFilter the 'exclude' class filter
* @return the coverage metadata file generated by the compilation
* @throws Exception if the compilation fails
*/
private static File enableCodeCoverage(
@Nonnull JackBasedToolchain toolchain,
@Nonnull String includeFilter,
@Nonnull String excludeFilter) throws Exception {
File coverageMetadataFile = createTempCoverageMetadataFile();
toolchain.addProperty("jack.coverage", "true");
toolchain.addProperty(
"jack.coverage.metadata.file", coverageMetadataFile.getAbsolutePath());
toolchain.addProperty(
"jack.coverage.jacoco.package", JACOCO_RUNTIME_PACKAGE);
if (includeFilter != null) {
toolchain.addProperty("jack.coverage.jacoco.include", includeFilter);
}
if (excludeFilter != null) {
toolchain.addProperty("jack.coverage.jacoco.exclude", excludeFilter);
}
toolchain.addToClasspath(getJacocoAgentLib());
File pluginFile = getCodeCoveragePluginFile();
List<File> pluginPath = Collections.singletonList(pluginFile);
List<String> pluginNames = Collections.singletonList("com.android.jack.coverage.CodeCoverage");
if (toolchain instanceof JackCliToolchain) {
JackCliToolchain cliToolchain = (JackCliToolchain) toolchain;
cliToolchain.setPluginPath(pluginPath);
cliToolchain.setPluginNames(pluginNames);
} else {
// TODO: need to rework API toolchain hierarchy in test framework to avoid these if/else.
if (toolchain instanceof JackApiV03Toolchain) {
JackApiV03Toolchain jackApiV03 = (JackApiV03Toolchain) toolchain;
jackApiV03.setPluginPath(pluginPath);
jackApiV03.setPluginNames(pluginNames);
} else if (toolchain instanceof JackApiV04Toolchain) {
JackApiV04Toolchain jackApiV04 = (JackApiV04Toolchain) toolchain;
jackApiV04.setPluginPath(pluginPath);
jackApiV04.setPluginNames(pluginNames);
} else {
throw new AssertionError("Unsupported toolchain: " + toolchain.getClass().getName());
}
}
return coverageMetadataFile;
}
@Nonnull
private JsonArray compileAndReadJson(@Nonnull String testPackageName)
throws CannotCreateFileException, CannotChangePermissionException, WrongPermissionException,
IOException, Exception {
return compileAndReadJson(testPackageName, null, null);
}
@Nonnull
private static File createTempCoverageMetadataFile()
throws CannotCreateFileException, CannotChangePermissionException {
return AbstractTestTools.createTempFile("coverage", ".metadata");
}
private File compileDexWithCoverage(@Nonnull File[] sourceFiles,
@CheckForNull String includeFilter,
@CheckForNull String excludeFilter,
@Nonnull File[] staticLibs) throws Exception {
File outDexFolder = AbstractTestTools.createTempDir();
JackBasedToolchain toolchain = createJackToolchain();
File coverageMetadataFile = enableCodeCoverage(toolchain, includeFilter, excludeFilter);
// Setup classpath.
toolchain.addStaticLibs(staticLibs);
toolchain.srcToExe(outDexFolder, false, sourceFiles);
return coverageMetadataFile;
}
@Nonnull
private JsonArray compileAndReadJson(
@Nonnull String testPackageName,
@CheckForNull String includeFilter,
@CheckForNull String excludeFilter)
throws CannotCreateFileException, CannotChangePermissionException, WrongPermissionException,
IOException, Exception {
File sourceDir = getTestRootDir(testPackageName);
File coverageMetadataFile = compileDexWithCoverage(new File[]{sourceDir},
includeFilter, excludeFilter, new File[0]);
return loadJsonCoverageClasses(coverageMetadataFile);
}
@Nonnull
private static JsonArray loadJsonCoverageClasses(@Nonnull File coverageMetadataFile)
throws IOException {
Assert.assertTrue(coverageMetadataFile.length() > 0);
JsonObject root = loadJson(coverageMetadataFile).getAsJsonObject();
Assert.assertNotNull(root);
String version = root.get("version").getAsString();
Assert.assertNotNull(version);
Assert.assertEquals("1.0", version);
JsonArray classes =
root.get("data").getAsJsonArray();
Assert.assertNotNull(classes);
return classes;
}
@Nonnull
private static JsonElement loadJson(@Nonnull File jsonFile) throws IOException {
JsonParser parser = new JsonParser();
FileReader reader = new FileReader(jsonFile);
try {
return parser.parse(reader);
} finally {
reader.close();
}
}
@Nonnull
private static File getJacocoAgentLib() {
return new File(
TestsProperties.getJackRootDir(), "jacoco/org.jacoco.agent.rt-0.7.5.201505241946-all.jar");
}
@Nonnull
private static File getCodeCoveragePluginFile() {
return new File(
TestsProperties.getJackRootDir(), "jack-coverage/dist/jack-coverage-plugin.jar");
}
@Nonnull
private String getTestPackageName(@Nonnull String testName) {
return COVERAGE_TEST_PACKAGE + "." + testName;
}
/**
* Workaround AbstractTestTools.getTestRootDir hardcoded for Jack tests only.
*/
@Nonnull
private static final File getTestRootDir(@Nonnull String testPackageName) {
File jackRootDir = TestsProperties.getJackRootDir();
File jackCoverageDir = new File(jackRootDir, "jack-coverage");
File jackCoverageTestsDir = new File(jackCoverageDir, "tests");
return new File(jackCoverageTestsDir, testPackageName.replace('.', File.separatorChar));
}
}