diff --git a/.gitignore b/.gitignore index b88717ff..dbce23d1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ local.properties # output files fork-output + +*/out diff --git a/fork-common-test-dex/app/src/androidTest/java/com/shazam/annotations/Inheritance.java b/fork-common-test-dex/app/src/androidTest/java/com/shazam/annotations/Inheritance.java new file mode 100644 index 00000000..5e20ccf6 --- /dev/null +++ b/fork-common-test-dex/app/src/androidTest/java/com/shazam/annotations/Inheritance.java @@ -0,0 +1,13 @@ +package com.shazam.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({TYPE, METHOD}) +public @interface Inheritance { +} diff --git a/fork-common-test-dex/app/src/androidTest/java/com/shazam/forktest/TestA.java b/fork-common-test-dex/app/src/androidTest/java/com/shazam/forktest/TestA.java new file mode 100644 index 00000000..cc1376c5 --- /dev/null +++ b/fork-common-test-dex/app/src/androidTest/java/com/shazam/forktest/TestA.java @@ -0,0 +1,26 @@ +package com.shazam.forktest; + +import android.Manifest; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TestA { + + private final int a; + private final int b; + private final int res; + + public TestA(int a, int b, int res) { + this.a = a; + this.b = b; + this.res = res; + } + + @Test + public void methodTest() { + assertEquals(res, a + b); + } +} + diff --git a/fork-common-test-dex/app/src/androidTest/java/com/shazam/forktest/TestB.java b/fork-common-test-dex/app/src/androidTest/java/com/shazam/forktest/TestB.java new file mode 100644 index 00000000..3f649c3d --- /dev/null +++ b/fork-common-test-dex/app/src/androidTest/java/com/shazam/forktest/TestB.java @@ -0,0 +1,11 @@ +package com.shazam.forktest; + +import com.shazam.annotations.Inheritance; + +@Inheritance +public class TestB extends TestA { + public TestB() { + super(2, 2, 4); + } +} + diff --git a/fork-common/src/main/java/com/shazam/fork/suite/TestSuiteLoader.java b/fork-common/src/main/java/com/shazam/fork/suite/TestSuiteLoader.java index f1baf09e..f32a44f9 100644 --- a/fork-common/src/main/java/com/shazam/fork/suite/TestSuiteLoader.java +++ b/fork-common/src/main/java/com/shazam/fork/suite/TestSuiteLoader.java @@ -35,6 +35,7 @@ public class TestSuiteLoader { private static final String IGNORE_ANNOTATION = "Lorg/junit/Ignore;"; private static final String REVOKE_PERMISSION_ANNOTATION = "Lcom/shazam/fork/RevokePermission;"; private static final String TEST_PROPERTIES_ANNOTATION = "Lcom/shazam/fork/TestProperties;"; + public static final String JAVA_LANG_OBJECT_SINGATURE = "Ljava/lang/Object;"; private final File instrumentationApkFile; private final DexFileExtractor dexFileExtractor; @@ -58,23 +59,30 @@ public TestSuiteLoader(File instrumentationApkFile, } public Collection loadTestSuite() throws NoTestCasesFoundException { + Collection dexFiles = dexFileExtractor.getDexFiles(instrumentationApkFile); + List testCaseEvents = extractTests(dexFiles); - List testCaseEvents = dexFileExtractor.getDexFiles(instrumentationApkFile).stream() + if (testCaseEvents.isEmpty()) { + throw new NoTestCasesFoundException("No tests cases were found in the test APK: " + instrumentationApkFile.getAbsolutePath()); + } + return testCaseEvents; + } + + private List extractTests(Collection dexFiles) { + List classDefItems = dexFiles.stream() .map(dexFile -> dexFile.ClassDefsSection.getItems()) .flatMap(Collection::stream) + .collect(toList()); + + return classDefItems.stream() .filter(c -> testClassMatcher.matchesPatterns(c.getClassType().getTypeDescriptor())) - .map(this::convertClassToTestCaseEvents) + .map(item -> convertClassToTestCaseEvents(item, classDefItems)) .flatMap(Collection::stream) .collect(toList()); - - if (testCaseEvents.isEmpty()) { - throw new NoTestCasesFoundException("No tests cases were found in the test APK: " + instrumentationApkFile.getAbsolutePath()); - } - return testCaseEvents; } @Nonnull - private List convertClassToTestCaseEvents(ClassDefItem classDefItem) { + private List convertClassToTestCaseEvents(ClassDefItem classDefItem, List classDefItems) { boolean classIncluded = false; AnnotationDirectoryItem annotationDirectoryItem = classDefItem.getAnnotations(); if (annotationDirectoryItem == null) { @@ -91,16 +99,40 @@ private List convertClassToTestCaseEvents(ClassDefItem classDefIt } } - return parseMethods(classDefItem, annotationDirectoryItem, classIncluded); + return parseMethods(classDefItem, annotationDirectoryItem, classIncluded, classDefItems); } - private List parseMethods(ClassDefItem classDefItem, AnnotationDirectoryItem annotationDirectory, boolean classIncluded) { - return annotationDirectory.getMethodAnnotations() - .stream() + private List parseMethods(ClassDefItem classDefItem, + AnnotationDirectoryItem annotationDirectory, + boolean classIncluded, + List classDefItems) { + return recursiveSearch(classDefItem, annotationDirectory, classDefItems).stream() .flatMap(method -> parseTestCaseEvents(classDefItem, annotationDirectory, method, classIncluded)) .collect(toList()); } + private List recursiveSearch(ClassDefItem classDefItem, + AnnotationDirectoryItem directoryItem, + List classDefItems) { + TypeIdItem superClassIdItem = classDefItem.getSuperclass(); + String superClassDescriptor = superClassIdItem.getTypeDescriptor(); + List list = new ArrayList<>(); + if (directoryItem != null) { + list.addAll(directoryItem.getMethodAnnotations()); + } + if (!JAVA_LANG_OBJECT_SINGATURE.equals(superClassDescriptor)) { + findClassDef(classDefItems, superClassDescriptor) + .ifPresent(a -> list.addAll(recursiveSearch(a, a.getAnnotations(), classDefItems))); + } + return list; + } + + private Optional findClassDef(List classDefItems, String identity) { + return classDefItems.stream() + .filter(a -> a.getClassType().getTypeDescriptor().equals(identity)) + .findFirst(); + } + private Stream parseTestCaseEvents(ClassDefItem classDefItem, AnnotationDirectoryItem annotationDirectoryItem, AnnotationDirectoryItem.MethodAnnotation methodAnnotation, diff --git a/fork-common/src/test/java/com/shazam/fork/suite/TestSuiteLoaderInheritanceTest.java b/fork-common/src/test/java/com/shazam/fork/suite/TestSuiteLoaderInheritanceTest.java new file mode 100644 index 00000000..f94fe509 --- /dev/null +++ b/fork-common/src/test/java/com/shazam/fork/suite/TestSuiteLoaderInheritanceTest.java @@ -0,0 +1,52 @@ +package com.shazam.fork.suite; + +import com.shazam.fork.io.DexFileExtractor; +import com.shazam.fork.model.TestCaseEvent; +import com.shazam.fork.model.TestCaseEventFactory; +import com.shazam.fork.stat.StatServiceLoader; +import com.shazam.fork.stat.TestStatsLoader; +import org.jf.dexlib.DexFile; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.net.URL; +import java.util.Collection; + +import static com.shazam.fork.io.FakeDexFileExtractor.fakeDexFileExtractor; +import static com.shazam.fork.io.Files.convertFileToDexFile; +import static com.shazam.fork.suite.FakeTestClassMatcher.fakeTestClassMatcher; +import static com.shazam.fork.suite.TestSuiteLoaderTestUtils.sameTestEventAs; +import static com.shazam.shazamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; + +public class TestSuiteLoaderInheritanceTest { + private static final File ANY_INSTRUMENTATION_APK_FILE = null; + + private final DexFileExtractor fakeDexFileExtractor = fakeDexFileExtractor().thatReturns(testDexFile()); + private final TestClassMatcher fakeTestClassMatcher = fakeTestClassMatcher().thatAlwaysMatches(); + private TestSuiteLoader testSuiteLoader; + + private DexFile testDexFile() { + URL testDexResourceUrl = this.getClass().getResource("/tests.dex"); + String testDexFile = testDexResourceUrl.getFile(); + File file = new File(testDexFile); + return convertFileToDexFile().apply(file); + } + + @Before + public void setUp() throws Exception { + testSuiteLoader = new TestSuiteLoader(ANY_INSTRUMENTATION_APK_FILE, fakeDexFileExtractor, fakeTestClassMatcher, + "com.shazam.annotations.Inheritance", + "",new TestCaseEventFactory(new TestStatsLoader(new StatServiceLoader("")))); + } + + @Test + public void testIncludeAnnotation() throws Exception { + Collection events = testSuiteLoader.loadTestSuite(); + assertThat(events.size(),is(1)); + assertThat(events, hasItem(sameTestEventAs("methodTest", "com.shazam.forktest.TestB", false))); + } +} diff --git a/fork-common/src/test/resources/app-debug.apk b/fork-common/src/test/resources/app-debug.apk index 7e514dac..1fc31d6d 100644 Binary files a/fork-common/src/test/resources/app-debug.apk and b/fork-common/src/test/resources/app-debug.apk differ diff --git a/fork-common/src/test/resources/tests.dex b/fork-common/src/test/resources/tests.dex index e854c20c..87682b34 100755 Binary files a/fork-common/src/test/resources/tests.dex and b/fork-common/src/test/resources/tests.dex differ diff --git a/fork-runner/src/main/java/com/shazam/fork/batch/TestTaskQueueProvider.kt b/fork-runner/src/main/java/com/shazam/fork/batch/TestTaskQueueProvider.kt index 546ddf37..6404506a 100644 --- a/fork-runner/src/main/java/com/shazam/fork/batch/TestTaskQueueProvider.kt +++ b/fork-runner/src/main/java/com/shazam/fork/batch/TestTaskQueueProvider.kt @@ -1,12 +1,12 @@ package com.shazam.fork.batch import com.shazam.fork.BatchStrategy -import com.shazam.fork.batch.strategies.* +import com.shazam.fork.batch.strategies.DefaultFactoryStrategy +import com.shazam.fork.batch.strategies.SplitFactoryStrategy import com.shazam.fork.batch.strategies.stat.ExpectedTimeFactoryStrategy import com.shazam.fork.batch.strategies.stat.VarianceFactoryStrategy import com.shazam.fork.batch.tasks.TestTask import com.shazam.fork.model.TestCaseEvent -import org.slf4j.LoggerFactory class TestTaskQueueProvider(private val batchStrategy: BatchStrategy) { @@ -14,9 +14,10 @@ class TestTaskQueueProvider(private val batchStrategy: BatchStrategy) { val extractedStrategy = extractStrategy(batchStrategy) val supportBatches = list.groupBy { it.permissionsToRevoke.isEmpty() } val tasks = extractedStrategy.batches(maxDevicesPerPool, supportBatches[true] ?: emptyList()) - val queue = BatchTestQueue(tasks.size) + val singleTestTasks = supportBatches[false]?.map { TestTask.SingleTestTask(it) } ?: emptyList() + val queue = BatchTestQueue(tasks.size + singleTestTasks.size) queue.addAll(tasks) - queue.addAll(supportBatches[false]?.map { TestTask.SingleTestTask(it) } ?: emptyList()) + queue.addAll(singleTestTasks) return queue } diff --git a/gradle.properties b/gradle.properties index bba8ad4f..185d5db5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ # 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. # -VERSION_NAME=3.0.0-alpha2 +VERSION_NAME=3.0.0-alpha3 GROUP=com.shazam.fork POM_LICENCE_NAME=The Apache Software License, Version 2.0