Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for test classes that extend other test classes - #13 #16

Merged
merged 5 commits into from
Jul 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## Version 2.0.0-SNAPSHOT

- Support finding superclass methods annotated with @Test for JUnit4 tests. Breaking change
to the Java interface for finding JUnit4 tests


## Version 1.1.0 (2017-07-13)

- Fixed bug where invalid tests methods in interfaces were returned (#3)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ org.gradle.configureondemand=true
org.gradle.parallel=true

GROUP_ID=com.linkedin.dextestparser
VERSION_NAME=1.1.1-SNAPSHOT
VERSION_NAME=2.0.0-SNAPSHOT
4 changes: 4 additions & 0 deletions parser/ValidTestList.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ com.linkedin.parser.test.junit3.java.JUnit3WithAnnotations#testJUnit3WithAnnotat
com.linkedin.parser.test.junit3.kotlin.KotlinJUnit3AndroidTestCase#testKotlinJUnit3AndroidTestCase
com.linkedin.parser.test.junit3.kotlin.KotlinJUnit3TestInsideStaticInnerClass$InnerClass#testKotlinJUnit3TestInsideStaticInnerClass
com.linkedin.parser.test.junit3.kotlin.KotlinJUnit3WithAnnotations#testKotlinJUnit3WithAnnotations
com.linkedin.parser.test.junit4.java.BasicJUnit4#abstractTest
com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4
com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4Second
com.linkedin.parser.test.junit4.java.BasicJUnit4#concreteTest
com.linkedin.parser.test.junit4.java.ConcreteTest#abstractTest
com.linkedin.parser.test.junit4.java.ConcreteTest#concreteTest
com.linkedin.parser.test.junit4.java.JUnit4ClassInsideInterface$InnerClass#innerClassTest
com.linkedin.parser.test.junit4.java.JUnit4TestInsideStaticInnerClass$InnerClass#innerClassTest
com.linkedin.parser.test.junit4.kotlin.KotlinJUnit4Basic#testKotlinJUnit4Basic
Expand Down
14 changes: 4 additions & 10 deletions parser/src/main/kotlin/com/linkedin/dex/parser/DexParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,12 @@ class DexParser private constructor() {
@JvmStatic fun findTestMethods(apkPath: String): List<TestMethod> {
var allItems: List<TestMethod> = emptyList()

val time = kotlin.system.measureTimeMillis {
val dexFiles = Companion.readDexFiles(apkPath)
val dexFiles = Companion.readDexFiles(apkPath)

val junit3Items = findJUnit3Tests(dexFiles).sorted()
val junit4Items = dexFiles.flatMap { it.findJUnit4Tests() }.sorted()
val junit3Items = findJUnit3Tests(dexFiles).sorted()
val junit4Items = findAllJUnit4Tests(dexFiles).sorted()

allItems = junit3Items.plus(junit4Items).sorted()

val count = allItems.count()
println("Found $count fully qualified test methods")
}
println("Finished in $time ms")
allItems = junit3Items.plus(junit4Items).sorted()

return allItems
}
Expand Down
72 changes: 67 additions & 5 deletions parser/src/main/kotlin/com/linkedin/dex/parser/JUnit4Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,55 @@
package com.linkedin.dex.parser

import com.linkedin.dex.spec.ACC_INTERFACE
import com.linkedin.dex.spec.ACC_ABSTRACT
import com.linkedin.dex.spec.AnnotationsDirectoryItem
import com.linkedin.dex.spec.ClassDefItem
import com.linkedin.dex.spec.DexFile
import com.linkedin.dex.spec.MethodIdItem

/**
* Find all methods that are annotated with JUnit4's @Test annotation
* Find all methods that are annotated with JUnit4's @Test annotation, including any test methods that
* may be inherited from superclasses or interfaces.
*/
fun DexFile.findJUnit4Tests(): List<TestMethod> {
fun findAllJUnit4Tests(dexFiles: List<DexFile>): List<TestMethod> {
val testAnnotationName = "org.junit.Test"
val classesWithAnnotations = classDefs.filter(::hasAnnotations).filterNot(::isInterface)

return createTestMethods(classesWithAnnotations, findMethodIds())
.filter { it.annotations.map {
// Map to hold all the class information we've found as we go
// From the docs:
// The classes must be ordered such that a given class's superclass and
// implemented interfaces appear in the list earlier than the referring class
val classTestMethods: MutableMap<String, ClassParsingResult> = mutableMapOf()

dexFiles.map {dexFile ->

// We include classes that do not have annotations because there may be an intermediary class without tests
// For example, TestClass1 defines a test, EmptyClass2 extends TestClass1 and defines nothing, and then TestClass2
// extends EmptyClass2, TestClass2 should also list the tests defined in TestClass1
val classesWithAnnotations = dexFile.classDefs.filterNot(::isInterface)

classesWithAnnotations.map { classDef ->
val baseTests = dexFile.createTestMethods(classDef, dexFile.findMethodIds()).filter { it.annotations.map {
it.name
}.contains(testAnnotationName) }

val superTests = createTestMethodsFromSuperMethods(dexFile.formatClassName(classDef), getSuperTestMethods(classDef, classTestMethods, dexFile))
classTestMethods[dexFile.getClassName(classDef)] = ClassParsingResult(dexFile.getSuperclassName(classDef), baseTests union superTests, !(isAbstract(classDef) || isInterface(classDef)))
}
}

return classTestMethods.values.filter { it.isConcrete }.flatMap { it.testMethods }.toList()
}

/**
* Gets the superclass' test methods, so they can be transferred into the subclass as well
*
* Because we build the parsed classes map with the full list of test methods for a given class (including super methods),
* we don't need to actually traverse up the tree here. The immediate superclass will contain all other methods in it
* already
*/
private fun getSuperTestMethods(classDefItem: ClassDefItem, classTestMethods: Map<String, ClassParsingResult>, dexFile: DexFile): Set<TestMethod> {
val superClass = dexFile.getSuperclassName(classDefItem)
return classTestMethods[superClass]?.testMethods ?: emptySet()
}

/**
Expand All @@ -30,6 +63,35 @@ private fun DexFile.findMethodIds(): (ClassDefItem, AnnotationsDirectoryItem?) -
return { _, directory -> directory?.methodAnnotations?.map { methodIds[it.methodIdx] } ?: emptyList() }
}

private fun DexFile.getClassName(classDefItem: ClassDefItem): String {
return ParseUtils.parseClassName(byteBuffer, classDefItem, typeIds, stringIds)
}

private fun DexFile.getSuperclassName(classDefItem: ClassDefItem): String {
val superClassIdx = classDefItem.superclassIdx
val typeId = typeIds[superClassIdx]

return ParseUtils.parseDescriptor(byteBuffer, typeId, stringIds)
}

/**
* Creates new TestMethod objects with the class name changed from the super class to the subclass
*/
private fun createTestMethodsFromSuperMethods(className: String, superTests: Set<TestMethod>): Set<TestMethod> {
return superTests.map {
TestMethod(className + (it.testName.substring(it.testName.indexOf('#') + 1)), it.annotations)
}.toSet()
}

private fun isInterface(classDefItem: ClassDefItem): Boolean {
return classDefItem.accessFlags and ACC_INTERFACE == ACC_INTERFACE
}

private fun isAbstract(classDefItem: ClassDefItem): Boolean {
return classDefItem.accessFlags and ACC_ABSTRACT == ACC_ABSTRACT
}

// Class to hold the information we have parsed about the classes we have already seen
// We need to hold the information for every class, since there is no way to know if a later class will subclass it or not
// We keep isConcrete as well to make filtering at the end easier
private data class ClassParsingResult(val superClassName: String, val testMethods: Set<TestMethod>, val isConcrete: Boolean)
23 changes: 17 additions & 6 deletions parser/src/main/kotlin/com/linkedin/dex/parser/TestMethod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,26 @@ fun DexFile.createTestMethods(
classes: List<ClassDefItem>,
methodIdFinder: (ClassDefItem, AnnotationsDirectoryItem?) -> List<MethodIdItem>): List<TestMethod> {
return classes.flatMap { classDef ->
val directory = getAnnotationsDirectory(classDef)
createTestMethods(classDef, methodIdFinder)
}
}

// compute these outside the method loop to avoid duplicate work
val classAnnotations = getClassAnnotationValues(directory)
/**
* Create the list of [TestMethod] contained in the given class
*
* @param [classDef] The class to search for tests
* @param [methodIdFinder] a function to determine which methods to consider as potential tests (varies between
* JUnit3 and JUnit 4)
*/
fun DexFile.createTestMethods(classDef: ClassDefItem, methodIdFinder: (ClassDefItem, AnnotationsDirectoryItem?) -> List<MethodIdItem>): List<TestMethod> {
val directory = getAnnotationsDirectory(classDef)

val methodIds = methodIdFinder.invoke(classDef, directory)
// compute these outside the method loop to avoid duplicate work
val classAnnotations = getClassAnnotationValues(directory)

methodIds.map { createTestMethod(it, directory, classDef, classAnnotations) }
}
val methodIds = methodIdFinder.invoke(classDef, directory)

return methodIds.map { createTestMethod(it, directory, classDef, classAnnotations) }
}

private fun DexFile.createTestMethod(methodId: MethodIdItem,
Expand Down
2 changes: 1 addition & 1 deletion parser/src/test/kotlin/com/linkedin/dex/DexParserShould.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class DexParserShould {
fun parseCorrectNumberOfTestMethods() {
val testMethods = DexParser.findTestNames(APK_PATH)

assertEquals(15, testMethods.size)
assertEquals(19, testMethods.size)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.linkedin.parser.test.junit4.java;

import org.junit.Test;

import static org.junit.Assert.assertTrue;

public abstract class AbstractTest {

@Test
public void abstractTest() {
assertTrue(true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import static org.junit.Assert.assertTrue;

@TestValueAnnotation(stringValue = "Hello world!")
public class BasicJUnit4 {
public class BasicJUnit4 extends ConcreteTest {

@Test
@TestValueAnnotation(stringValue = "On a method", intValue = 12345, boolValue = true, longValue = 56789L)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.linkedin.parser.test.junit4.java;

import org.junit.Test;

import static org.junit.Assert.assertTrue;

public class ConcreteTest extends AbstractTest {
@Test
public void concreteTest() {
assertTrue(true);
}
}