├── .github └── workflows │ ├── deploy-snapshot.yml │ ├── gradle-wrapper-validation.yml │ ├── merge-check.yml │ └── publish-maven-central.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── RELEASING.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── parser ├── ValidTestList.txt ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── linkedin │ │ └── dex │ │ ├── parser │ │ ├── AnnotationUtils.kt │ │ ├── DecodedValue.kt │ │ ├── DexFileUtils.kt │ │ ├── DexParser.kt │ │ ├── FormatUtils.kt │ │ ├── JUnit3Extensions.kt │ │ ├── JUnit4Extensions.kt │ │ ├── ParseUtils.kt │ │ ├── TestAnnotation.kt │ │ └── TestMethod.kt │ │ └── spec │ │ ├── AccessFlags.kt │ │ ├── AnnotationElement.kt │ │ ├── AnnotationItem.kt │ │ ├── AnnotationOffItem.kt │ │ ├── AnnotationSetItem.kt │ │ ├── AnnotationsDirectoryItem.kt │ │ ├── ClassDataItem.kt │ │ ├── ClassDefItem.kt │ │ ├── DexException.kt │ │ ├── DexFile.kt │ │ ├── DexMagic.kt │ │ ├── EncodedAnnotation.kt │ │ ├── EncodedArray.kt │ │ ├── EncodedField.kt │ │ ├── EncodedMethod.kt │ │ ├── EncodedValue.kt │ │ ├── FieldAnnotation.kt │ │ ├── FieldIdItem.kt │ │ ├── HeaderItem.kt │ │ ├── Leb128.kt │ │ ├── MethodAnnotation.kt │ │ ├── MethodIdItem.kt │ │ ├── ParameterAnnotation.kt │ │ ├── ProtoIdItem.kt │ │ ├── StringIdItem.kt │ │ └── TypeIdItem.kt │ └── test │ ├── fixtures │ └── abstract-class-in-second-dex.apk │ └── kotlin │ └── com │ └── linkedin │ └── dex │ ├── AbstractClassInSecondDex.kt │ └── DexParserShould.kt ├── settings.gradle └── test-app ├── build.gradle └── src ├── androidTest └── java │ └── com │ └── linkedin │ └── parser │ └── test │ ├── junit3 │ ├── java │ │ ├── JUnit3ActivityInstrumentationTestCase2.java │ │ ├── JUnit3Basic.java │ │ ├── JUnit3InstrumentationTestCase.java │ │ ├── JUnit3TestInsideStaticInnerClass.java │ │ └── JUnit3WithAnnotations.java │ └── kotlin │ │ ├── KotlinJUnit3AndroidTestCase.kt │ │ ├── KotlinJUnit3TestInsideStaticInnerClass.kt │ │ └── KotlinJUnit3WithAnnotations.kt │ └── junit4 │ ├── java │ ├── AbstractTest.java │ ├── BasicJUnit4.java │ ├── ConcreteTest.java │ ├── FloatRange.java │ ├── IgnoreJUnit4TestInterface.java │ ├── InheritedAnnotation.java │ ├── InheritedClassAnnotation.java │ ├── JUnit4ClassInsideInterface.java │ ├── JUnit4TestInsideStaticInnerClass.java │ ├── NonInheritedAnnotation.java │ ├── TestEnum.java │ └── TestValueAnnotation.java │ └── kotlin │ ├── DefaultInterfaceImplementation.kt │ ├── InterfaceWithDefaultMethods.kt │ ├── KotlinJUnit4Basic.kt │ ├── KotlinJUnit4TestInsideStaticInnerClass.kt │ └── KotlinJUnit4WithAnnotations.kt └── main └── AndroidManifest.xml /.github/workflows/deploy-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Deploy snapshot 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | if: ${{ !contains(github.event.head_commit.message, 'Prepare for release') }} 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Cache Gradle Files 14 | uses: actions/cache@v2 15 | with: 16 | path: | 17 | ~/.gradle/caches/ 18 | ~/.gradle/wrapper/ 19 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 20 | restore-keys: | 21 | ${{ runner.os }}-gradle- 22 | 23 | - name: Set up Java 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: 1.8 27 | 28 | - name: Build 29 | run: ./gradlew build 30 | 31 | - name: Publish package 32 | run: ./gradlew publishAllPublicationsToSonatypeSnapshotRepository 33 | env: 34 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 35 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 36 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - '*' 9 | 10 | jobs: 11 | validation: 12 | name: "Validation" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: gradle/wrapper-validation-action@v1 17 | -------------------------------------------------------------------------------- /.github/workflows/merge-check.yml: -------------------------------------------------------------------------------- 1 | name: Merge checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | name: Build project 14 | runs-on: ubuntu-latest 15 | if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Cache Gradle Files 21 | uses: actions/cache@v2 22 | with: 23 | path: | 24 | ~/.gradle/caches/ 25 | ~/.gradle/wrapper/ 26 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 27 | restore-keys: | 28 | ${{ runner.os }}-gradle- 29 | 30 | - name: Run Gradle tasks 31 | run: ./gradlew build 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-maven-central.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to the Maven Central Repository 2 | on: 3 | release: 4 | types: [published] 5 | branches: 6 | - main 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set up Java 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 1.8 17 | 18 | - name: Build 19 | run: ./gradlew build 20 | 21 | - name: Publish package 22 | run: ./gradlew publishAllPublicationsToMavenCentralRepository 23 | env: 24 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 25 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 26 | DEX_TEST_PARSER_GPG_PRIVATE_KEY: ${{ secrets.DEX_TEST_PARSER_GPG_PRIVATE_KEY }} 27 | DEX_TEST_PARSER_GPG_PRIVATE_KEY_PASSWORD: ${{ secrets.DEX_TEST_PARSER_GPG_PRIVATE_KEY_PASSWORD }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | .classpath 4 | .gradle 5 | .idea 6 | *.iml 7 | *.ipr 8 | *.iws 9 | /build 10 | */build 11 | out/ 12 | */bin/ 13 | .DS_Store 14 | local.properties -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Version 2.3.4 (2022-03-08) 4 | - Fix BufferUnderflowException when parsing dex file [#73](https://github.com/linkedin/dex-test-parser/pull/73) 5 | - Differentiate overridden vs inherited super test methods [#71] (https://github.com/linkedin/dex-test-parser/pull/71) 6 | 7 | ## Version 2.3.3 (2021-06-04) 8 | - Fix Dokka publishing [#67](https://github.com/linkedin/dex-test-parser/pull/67) 9 | 10 | ## Version 2.3.2 (2021-06-04) 11 | - Fix issue with callable references in Kotlin 1.4 [#65](https://github.com/linkedin/dex-test-parser/pull/64) 12 | - Update Dokka to 1.4.32 [#64](https://github.com/linkedin/dex-test-parser/pull/64) 13 | - Update to AGP 4.2 and remove JCenter dependency [#63](https://github.com/linkedin/dex-test-parser/pull/63) 14 | 15 | ## Version 2.3.1 (2021-02-21) 16 | 17 | - Moved argument parsing to a new library [#60](https://github.com/linkedin/dex-test-parser/pull/60) 18 | 19 | ## Version 2.3.0 (2021-02-05) 20 | 21 | - Moved artifact publishing from JCenter to Maven Central [#53](https://github.com/linkedin/dex-test-parser/pull/53) 22 | - Add support for finding custom test annotations [#50](https://github.com/linkedin/dex-test-parser/pull/50) 23 | 24 | ## Version 2.2.1 (2020-06-11) 25 | 26 | - Fix test parsing when class and superclass are in different dex files [#45](https://github.com/linkedin/dex-test-parser/issues/45) 27 | 28 | ## Version 2.2.0 (2019-11-05) 29 | 30 | - Add support for encoded array values in annotations 31 | 32 | ## Version 2.1.1 (2019-04-23) 33 | 34 | - Fix crash when the classpath does not contain the `Inherited` annotation [#37](https://github.com/linkedin/dex-test-parser/issues/37) 35 | 36 | ## Version 2.1.0 (2019-04-04) 37 | 38 | - Add support for parsing enum annotation values [#28](https://github.com/linkedin/dex-test-parser/pull/28) 39 | - Improve DexMagic checks to support newer dex file formats [#31](https://github.com/linkedin/dex-test-parser/issues/31) 40 | - Support parsing `@Inherited` annotations [#26](https://github.com/linkedin/dex-test-parser/issues/26) 41 | - Fix crash with `Invalid LEB128 sequence` [#34](https://github.com/linkedin/dex-test-parser/issues/34) 42 | 43 | ## Version 2.0.1 (2018-12-13) 44 | 45 | - Fix reporting when using default interface methods and private methods [#20](https://github.com/linkedin/dex-test-parser/issues/20) 46 | - Fix buffer overflow when parsing test annotations [#21](https://github.com/linkedin/dex-test-parser/issues/21) 47 | 48 | ## Version 2.0.0 49 | 50 | - Support finding superclass methods annotated with @Test for JUnit4 tests. Breaking change 51 | to the Java interface for finding JUnit4 tests - [#13](https://github.com/linkedin/dex-test-parser/issues/13) 52 | - Added support for parsing and reading encoded values - [#9](https://github.com/linkedin/dex-test-parser/issues/9) 53 | - Fixed crashes when building with minSdkVersion 24+ - [#12](https://github.com/linkedin/dex-test-parser/issues/12) 54 | 55 | 56 | ## Version 1.1.0 (2017-07-13) 57 | 58 | - Fixed bug where invalid tests methods in interfaces were returned (#3) 59 | - Added support for returning all annotations that are on a test (#1) 60 | - Added Android app module with lots of regression tests! (#5) 61 | 62 | ## Version 1.0.0 63 | 64 | - Initial release. 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you would like to contribute to dex-test-parser you can do so through Github by forking the repository and sending a pull request. 4 | 5 | When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. 6 | 7 | ## Bugs 8 | 9 | Please [create an issue](https://github.com/linkedin/dex-test-parser/issues/new) and include enough information to reproduce the issue you are seeing. 10 | 11 | ## Feature requests 12 | 13 | Please [create an issue](https://github.com/linkedin/dex-test-parser/issues/new) to describe the feature and why you think it would be useful. 14 | 15 | ## Responsible Disclosure of Security Vulnerabilities 16 | 17 | Please do not file reports on Github for security issues. 18 | Please review the guidelines on at (link to more info). 19 | Reports should be encrypted using PGP (link to PGP key) and sent to 20 | [security@linkedin.com](mailto:security@linkedin.com) preferably with the title 21 | `Github linkedin/dex-test-parser - [short summary]`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2016, LinkedIn 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2017 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the BSD 2-Clause License (the "License"). 5 | See LICENSE in the project root for license information. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dex Test Parser 2 | [![Build Status](https://img.shields.io/github/workflow/status/linkedin/dex-test-parser/Merge%20checks)](https://img.shields.io/github/workflow/status/linkedin/dex-test-parser/Merge%20checks) 3 | 4 | ## Motivation 5 | 6 | dex-test-parser was inspired by the Google presentation "[Going Green: Cleaning up the Toxic Mobile Environment](https://www.youtube.com/watch?v=aHcmsK9jfGU)". 7 | 8 | ## What does it do? 9 | 10 | Given an Android instrumentation apk, dex-test-parser will parse the apk's dex files and return the fully qualified method names of all JUnit 3 and JUnit 4 test methods. 11 | 12 | Of course, you could also collect this list of method names from inside your test code by scanning the apk internally and using reflection. However, there are several reasons you may not want to do this: 13 | 14 | * Scanning the app's classpath for test methods at runtime causes any static initializers in the classes to be run immediately, which can lead to tests that behave differently than production code. 15 | * You might want to run one invocation of the `adb shell am instrument` command for each test to avoid shared state between tests and so that if one test crashes, other tests are still run. 16 | 17 | ## Download 18 | 19 | Download the latest .jar via Maven: 20 | ```xml 21 | 22 | com.linkedin.dextestparser 23 | parser 24 | 2.3.4 25 | pom 26 | 27 | ``` 28 | 29 | or Gradle: 30 | ``` 31 | compile 'com.linkedin.dextestparser:parser:2.3.4' 32 | ``` 33 | 34 | or you can manually download the jar from [Bintray](https://bintray.com/linkedin/maven/parser). 35 | 36 | ## Getting Started 37 | 38 | dex-test-parser provides a single public method that you can call from Java to get all test method names. 39 | ```java 40 | List customAnnotations = new ArrayList<>(); 41 | List testMethodNames = DexParser.findTestNames(apkPath, customAnnotations); 42 | ``` 43 | Variable customAnnotations is a list of custom tags that marks tests if you are using custom test runner for your tests. 44 | 45 | You can also use the jar directly from the command line if you prefer. This will create a file called `AllTests.txt` in the specified output directory. 46 | 47 | ``` 48 | 49 | java -jar parser.jar path/to/apk path/for/output 50 | 51 | ``` 52 | If "path/for/output" is omitted, the output will be printed into stdout. 53 | 54 | 55 | If you have custom test runner (com.company.testing.uitest.screenshot.ScreenshotTest in this example) and custom tag to annotate tests: 56 | ``` 57 | 58 | java -jar parser.jar path/to/apk path/for/output -A com.company.testing.uitest.screenshot.ScreenshotTest 59 | 60 | ``` 61 | 62 | ## Snapshots 63 | 64 | You can use snapshot builds to test the latest unreleased changes. A new snapshot is published 65 | after every merge to the main branch by the [Deploy Snapshot Github Action workflow](.github/workflows/deploy-snapshot.yml). 66 | 67 | Just add the Sonatype snapshot repository to your Gradle scripts: 68 | ```gradle 69 | repositories { 70 | maven { 71 | url "https://oss.sonatype.org/content/repositories/snapshots/" 72 | } 73 | } 74 | ``` 75 | 76 | You can find the latest snapshot version to use in the [gradle.properties](gradle.properties) file. 77 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT version. 4 | 2. Update the `CHANGELOG.md` for the impending release. 5 | 3. Update the `README.md` with the new version. 6 | 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 7 | 5. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version) 8 | 6. Update the `gradle.properties` to the next SNAPSHOT version. 9 | 7. `git commit -am "Prepare next development version."` 10 | 8. `git push && git push --tags` 11 | 9. Create a new release in the releases tab on GitHub 12 | 10. Wait for the [publish-maven-central.yml](.github/workflows/publish-maven-central.yml) action to complete. 13 | 11. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact. 14 | 15 | ## How it works 16 | 17 | The [deploy-snapshot.yml](.github/workflows/deploy-snapshot.yml) workflow runs on every 18 | push to the main branch as long as the commit message does not contain `Prepare for release`. This 19 | workflow calls Gradle to publish to the Sonatype snapshot repository. 20 | 21 | For actual releases, there is a separate [publish-maven-central.yml](.github/workflows/publish-maven-central.yml) 22 | workflow which runs after a new release is created in the GitHub UI. This will call Gradle on the 23 | tagged release commit and upload to the staging repository. After that completes, you will need to 24 | go and promote the artifacts to production. 25 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlinVersion = '1.4.30' 3 | 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 10 | classpath 'com.android.tools.build:gradle:4.2.0' 11 | } 12 | } 13 | 14 | allprojects { 15 | group = GROUP_ID 16 | version = VERSION_NAME 17 | } 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=true 2 | org.gradle.configureondemand=true 3 | org.gradle.parallel=true 4 | 5 | GROUP_ID=com.linkedin.dextestparser 6 | VERSION_NAME=2.3.5-SNAPSHOT 7 | org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/dex-test-parser/589f2520bfe63e2e36c9d550d1fd12d7c59c2028/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /parser/ValidTestList.txt: -------------------------------------------------------------------------------- 1 | com.linkedin.parser.test.junit3.java.JUnit3ActivityInstrumentationTestCase2#testJUnit3ActivityInstrumentationTestCase2 2 | com.linkedin.parser.test.junit3.java.JUnit3Basic#testJUnit3Basic 3 | com.linkedin.parser.test.junit3.java.JUnit3InstrumentationTestCase#testJUnit3InstrumentationTestCase 4 | com.linkedin.parser.test.junit3.java.JUnit3TestInsideStaticInnerClass#testJUnit3TestInsideStaticInnerClass 5 | com.linkedin.parser.test.junit3.java.JUnit3WithAnnotations#testJUnit3WithAnnotations 6 | com.linkedin.parser.test.junit3.kotlin.KotlinJUnit3AndroidTestCase#testKotlinJUnit3AndroidTestCase 7 | com.linkedin.parser.test.junit3.kotlin.KotlinJUnit3TestInsideStaticInnerClass$InnerClass#testKotlinJUnit3TestInsideStaticInnerClass 8 | com.linkedin.parser.test.junit3.kotlin.KotlinJUnit3WithAnnotations#testKotlinJUnit3WithAnnotations 9 | com.linkedin.parser.test.junit4.java.BasicJUnit4#abstractTest 10 | com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4 11 | com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4Second 12 | com.linkedin.parser.test.junit4.java.BasicJUnit4#concreteTest 13 | com.linkedin.parser.test.junit4.java.BasicJUnit4#customAnnotationTest 14 | com.linkedin.parser.test.junit4.java.BasicJUnit4#nonOverriddenConcreteTest 15 | com.linkedin.parser.test.junit4.java.ConcreteTest#abstractTest 16 | com.linkedin.parser.test.junit4.java.ConcreteTest#concreteTest 17 | com.linkedin.parser.test.junit4.java.ConcreteTest#nonOverriddenConcreteTest 18 | com.linkedin.parser.test.junit4.java.JUnit4ClassInsideInterface$InnerClass#innerClassTest 19 | com.linkedin.parser.test.junit4.java.JUnit4TestInsideStaticInnerClass$InnerClass#innerClassTest 20 | com.linkedin.parser.test.junit4.kotlin.DefaultInterfaceImplementation#testMethodShouldNotBeReported 21 | com.linkedin.parser.test.junit4.kotlin.DefaultInterfaceImplementation#testToBeOverrideShouldNotBeReportedInInterface 22 | com.linkedin.parser.test.junit4.kotlin.KotlinJUnit4Basic#testKotlinJUnit4Basic 23 | com.linkedin.parser.test.junit4.kotlin.KotlinJUnit4TestInsideStaticInnerClass$InnerClass#testKotlinJUnit4TestInsideStaticInnerClass 24 | com.linkedin.parser.test.junit4.kotlin.KotlinJUnit4WithAnnotations#testKotlinJUnit4WithAnnotations 25 | -------------------------------------------------------------------------------- /parser/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | id 'maven-publish' 4 | id 'signing' 5 | id 'org.jetbrains.kotlin.jvm' 6 | id 'org.jetbrains.dokka' version '1.4.32' 7 | id 'com.github.johnrengelman.shadow' version '6.1.0' 8 | } 9 | 10 | mainClassName = 'com.linkedin.dex.parser.DexParser' 11 | 12 | task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { 13 | classifier 'javadoc' 14 | from dokkaJavadoc.outputDirectory 15 | } 16 | 17 | java { 18 | sourceCompatibility = JavaVersion.VERSION_1_8 19 | targetCompatibility = JavaVersion.VERSION_1_8 20 | 21 | withSourcesJar() 22 | } 23 | 24 | repositories { 25 | mavenCentral() 26 | } 27 | 28 | dependencies { 29 | api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" 30 | implementation "com.github.ajalt.clikt:clikt:3.1.0" 31 | 32 | testImplementation 'junit:junit:4.13.1' 33 | } 34 | 35 | task testParsing(dependsOn: ':test-app:assembleDebugAndroidTest', type: JavaExec) { 36 | classpath sourceSets.main.runtimeClasspath 37 | main = 'com.linkedin.dex.parser.DexParser' 38 | args "${rootProject.project('test-app').buildDir}/outputs/apk/androidTest/debug/test-app-debug-androidTest.apk", "$buildDir", "-A", "com.linkedin.parser.test.junit4.java.NonInheritedAnnotation" 39 | 40 | doLast { 41 | def validTests = file('ValidTestList.txt').readLines() 42 | def parsedTests = file("$buildDir/AllTests.txt").readLines() 43 | 44 | if (validTests != parsedTests) { 45 | throw new GradleException("Parsed tests do not match expected tests: " + parsedTests) 46 | } 47 | } 48 | } 49 | check.dependsOn testParsing 50 | 51 | // Configure the jvm tests so that apk will be present and in a consistent location 52 | // When you run from the IDE, the working directory is different than when running through gradle 53 | // To make sure we can run tests from either the IDE or gradle directly, we set it to be the same as IDE settings 54 | // We also need to make sure the test apk is generated as part of the compile task, so the tests have their 55 | // dependencies fulfilled 56 | testClasses.dependsOn ':test-app:assembleDebugAndroidTest' 57 | test { 58 | workingDir project.getRootProject().getProjectDir() 59 | } 60 | 61 | publishing { 62 | publications { 63 | maven(MavenPublication) { publication -> 64 | from components.java 65 | artifact(shadowJar) { 66 | classifier "fat" 67 | } 68 | // It would be nice to be able to use withJavadocJar() instead of defining the artifact 69 | // manually, but Gradle doesn't expose a way to customize what task generates the 70 | // javadoc and we need to use Dokka for Kotlin code. 71 | artifact(javadocJar) { 72 | classifier = 'javadoc' 73 | } 74 | pom { 75 | name = 'Dex Test Parser' 76 | description = 'Find all test methods in an Android instrumentation APK' 77 | url = 'https://github.com/linkedin/dex-test-parser' 78 | licenses { 79 | license { 80 | name = 'BSD 2-Clause License' 81 | url = 'https://opensource.org/licenses/BSD-2-Clause' 82 | } 83 | } 84 | developers { 85 | developer { 86 | id = 'com.linkedin' 87 | name = 'LinkedIn Corp' 88 | } 89 | } 90 | scm { 91 | connection = 'scm:git:git://github.com/linkedin/dex-test-parser.git' 92 | developerConnection = 'scm:git:ssh://github.com:linkedin/dex-test-parser.git' 93 | url = 'https://github.com/linkedin/dex-test-parser/tree/main' 94 | } 95 | } 96 | } 97 | } 98 | repositories { 99 | def sonatypeUsername = System.getenv("SONATYPE_USER") 100 | def sonatypePassword = System.getenv("SONATYPE_PASSWORD") 101 | maven { 102 | name = "sonatypeSnapshot" 103 | url = "https://oss.sonatype.org/content/repositories/snapshots" 104 | credentials { 105 | username = sonatypeUsername 106 | password = sonatypePassword 107 | } 108 | } 109 | maven { 110 | name = "mavenCentral" 111 | url = "https://oss.sonatype.org/service/local/staging/deploy/maven2" 112 | credentials { 113 | username = sonatypeUsername 114 | password = sonatypePassword 115 | } 116 | } 117 | } 118 | } 119 | 120 | // DEX_TEST_PARSER_GPG_PRIVATE_KEY should contain the armoured private key that 121 | // starts with -----BEGIN PGP PRIVATE KEY BLOCK----- 122 | // It can be obtained with gpg --armour --export-secret-keys KEY_ID 123 | def signingKey = System.getenv("DEX_TEST_PARSER_GPG_PRIVATE_KEY") 124 | def signingPassword = System.getenv("DEX_TEST_PARSER_GPG_PRIVATE_KEY_PASSWORD") 125 | signing { 126 | required { signingKey != null && signingPassword != null } 127 | useInMemoryPgpKeys(signingKey, signingPassword) 128 | sign publishing.publications.maven 129 | } 130 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/AnnotationUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.parser 6 | 7 | import com.linkedin.dex.spec.AnnotationItem 8 | import com.linkedin.dex.spec.AnnotationSetItem 9 | import com.linkedin.dex.spec.AnnotationsDirectoryItem 10 | import com.linkedin.dex.spec.ClassDefItem 11 | import com.linkedin.dex.spec.DexFile 12 | import com.linkedin.dex.spec.MethodAnnotation 13 | import com.linkedin.dex.spec.MethodIdItem 14 | 15 | 16 | /** 17 | * Check if there are any class, field, method, or parameter annotations in the given class 18 | * 19 | * @return true if this [ClassDefItem] has any annotations, false otherwise 20 | */ 21 | fun hasAnnotations(classDefItem: ClassDefItem): Boolean { 22 | return classDefItem.annotationsOff != 0 23 | } 24 | 25 | /** 26 | * Null-safe creation of an [AnnotationsDirectoryItem] 27 | */ 28 | fun DexFile.getAnnotationsDirectory(classDefItem: ClassDefItem): AnnotationsDirectoryItem? { 29 | if (hasAnnotations(classDefItem)) { 30 | return AnnotationsDirectoryItem.create(byteBuffer, classDefItem.annotationsOff) 31 | } else { 32 | return null 33 | } 34 | } 35 | 36 | /** 37 | * @return a list of annotation objects containing all class-level annotations 38 | */ 39 | fun DexFile.getClassAnnotationValues(directory: AnnotationsDirectoryItem?): List { 40 | if (directory == null || directory.classAnnotationsOff == 0) { 41 | return emptyList() 42 | } 43 | 44 | val classAnnotationSetItem = AnnotationSetItem.create(byteBuffer, directory.classAnnotationsOff) 45 | 46 | return classAnnotationSetItem.entries.map { AnnotationItem.create(byteBuffer, it.annotationOff) }.map { getTestAnnotation(it) } 47 | } 48 | 49 | /** 50 | * @return A list of annotation objects for all the method-level annotations 51 | */ 52 | fun DexFile.getMethodAnnotationValues(methodId: MethodIdItem, annotationsDirectory: AnnotationsDirectoryItem?): List { 53 | val methodAnnotations = annotationsDirectory?.methodAnnotations ?: emptyArray() 54 | val annotationSets = methodAnnotations.filter { methodIds[it.methodIdx] == methodId } 55 | .map { (_, annotationsOff) -> 56 | AnnotationSetItem.create(byteBuffer, annotationsOff) 57 | } 58 | 59 | return annotationSets.map { 60 | it.entries.map { AnnotationItem.create(byteBuffer, it.annotationOff) }.map { getTestAnnotation(it) } 61 | }.flatten() 62 | } 63 | 64 | fun DexFile.getTestAnnotation(annotationItem: AnnotationItem): TestAnnotation { 65 | val name = formatDescriptor(ParseUtils.parseDescriptor(byteBuffer, 66 | typeIds[annotationItem.encodedAnnotation.typeIdx], stringIds)) 67 | val encodedAnnotationValues = annotationItem.encodedAnnotation.elements 68 | val values = mutableMapOf() 69 | for (encodedAnnotationValue in encodedAnnotationValues) { 70 | val value = DecodedValue.create(this, encodedAnnotationValue.value) 71 | val valueName = ParseUtils.parseValueName(byteBuffer, stringIds, encodedAnnotationValue.nameIdx) 72 | 73 | values.put(valueName, value) 74 | } 75 | 76 | val annotationClassDef = typeIdToClassDefMap[annotationItem.encodedAnnotation.typeIdx] 77 | val inherited = checkIfAnnotationIsInherited(annotationClassDef) 78 | 79 | return TestAnnotation(name, values, inherited) 80 | } 81 | 82 | private fun DexFile.checkIfAnnotationIsInherited(annotationClassDef: ClassDefItem?): Boolean { 83 | //Early return when classpath doesn't contain Ljava/lang/annotation/Inherited annotation 84 | if (inheritedAnnotationTypeIdIndex == null) return false 85 | 86 | return annotationClassDef?.let { 87 | val annotationsDirectory = getAnnotationsDirectory(annotationClassDef) 88 | if (annotationsDirectory != null && annotationsDirectory.classAnnotationsOff != 0) { 89 | val classAnnotationSetItem = AnnotationSetItem.create(byteBuffer, annotationsDirectory.classAnnotationsOff) 90 | val annotations = classAnnotationSetItem.entries.map { AnnotationItem.create(byteBuffer, it.annotationOff) } 91 | return@let annotations.any { it.encodedAnnotation.typeIdx == inheritedAnnotationTypeIdIndex } 92 | } else { 93 | false 94 | } 95 | } ?: false 96 | } 97 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/DecodedValue.kt: -------------------------------------------------------------------------------- 1 | package com.linkedin.dex.parser 2 | 3 | import com.linkedin.dex.spec.DexFile 4 | import com.linkedin.dex.spec.EncodedAnnotation 5 | import com.linkedin.dex.spec.EncodedArray 6 | import com.linkedin.dex.spec.EncodedValue 7 | import com.linkedin.dex.spec.Leb128 8 | 9 | /** 10 | * A sealed class to represent the decoded values of an EncodedValue object. 11 | */ 12 | // TODO: Add support for complex types 13 | sealed class DecodedValue { 14 | data class DecodedByte(val value: Byte) : DecodedValue() 15 | data class DecodedShort(val value: Short) : DecodedValue() 16 | data class DecodedChar(val value: Char) : DecodedValue() 17 | data class DecodedInt(val value: Int) : DecodedValue() 18 | data class DecodedLong(val value: Long) : DecodedValue() 19 | data class DecodedFloat(val value: Float) : DecodedValue() 20 | data class DecodedDouble(val value: Double) : DecodedValue() 21 | // Value is an index into the string table 22 | data class DecodedString(val value: String) : DecodedValue() 23 | 24 | data class DecodedType(val value: String) : DecodedValue() 25 | object DecodedNull : DecodedValue() 26 | data class DecodedBoolean(val value: Boolean) : DecodedValue() 27 | data class DecodedEnum(val value: String) : DecodedValue() 28 | data class DecodedArrayValue(val values: Array): DecodedValue() 29 | // TODO: DecodedField 30 | // TODO: DecodedMethod 31 | // TODO: DecodedAnnotationValue 32 | 33 | companion object { 34 | private fun readStringInPosition(dexFile: DexFile, position: Int): String { 35 | dexFile.byteBuffer.position(position) 36 | // read past unused size item 37 | Leb128.readUnsignedLeb128(dexFile.byteBuffer) 38 | return ParseUtils.parseStringBytes(dexFile.byteBuffer) 39 | } 40 | 41 | /** 42 | * Resolve an encoded value against the given dexfile 43 | */ 44 | fun create(dexFile: DexFile, encodedValue: EncodedValue): DecodedValue { 45 | when (encodedValue) { 46 | is EncodedValue.EncodedByte -> return DecodedByte(encodedValue.value) 47 | is EncodedValue.EncodedShort -> return DecodedShort(encodedValue.value) 48 | is EncodedValue.EncodedChar -> return DecodedChar(encodedValue.value) 49 | is EncodedValue.EncodedInt -> return DecodedInt(encodedValue.value) 50 | is EncodedValue.EncodedLong -> return DecodedLong(encodedValue.value) 51 | is EncodedValue.EncodedFloat -> return DecodedFloat(encodedValue.value) 52 | is EncodedValue.EncodedDouble -> return DecodedDouble(encodedValue.value) 53 | is EncodedValue.EncodedString -> { 54 | val position = dexFile.stringIds[encodedValue.value].stringDataOff 55 | return DecodedString(readStringInPosition(dexFile, position)) 56 | } 57 | is EncodedValue.EncodedType -> { 58 | val index = dexFile.typeIds[encodedValue.value].descriptorIdx 59 | val position = dexFile.stringIds[index].stringDataOff 60 | return DecodedType(readStringInPosition(dexFile, position)) 61 | } 62 | is EncodedValue.EncodedBoolean -> return DecodedBoolean(encodedValue.value) 63 | is EncodedValue.EncodedNull -> return DecodedNull 64 | is EncodedValue.EncodedEnum -> { 65 | val index = dexFile.fieldIds[encodedValue.value].nameIdx 66 | val position = dexFile.stringIds[index].stringDataOff 67 | return DecodedEnum(readStringInPosition(dexFile, position)) 68 | } 69 | is EncodedValue.EncodedArrayValue -> return DecodedArrayValue(encodedValue.value.values.map { create(dexFile, it) }.toTypedArray()) 70 | 71 | else -> return DecodedNull 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/DexFileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.linkedin.dex.parser 2 | 3 | import com.linkedin.dex.spec.ClassDataItem 4 | import com.linkedin.dex.spec.ClassDefItem 5 | import com.linkedin.dex.spec.DexFile 6 | 7 | fun DexFile.findMethodIdxs(classDefItem: ClassDefItem): List { 8 | // We need to catch the classes have an offset of 0, and so have no data in this apk to read 9 | // From the docs: "0 if there is no class data for this class. (This may be the case, for example, 10 | // if this class is a marker interface.)" 11 | if (classDefItem.classDataOff == 0) { 12 | return emptyList() 13 | } 14 | 15 | val methodIds = mutableListOf() 16 | val testClassData = ClassDataItem.create(byteBuffer, classDefItem.classDataOff) 17 | var previousMethodIdxOff = 0 18 | testClassData.virtualMethods.forEachIndexed { index, encodedMethod -> 19 | var methodIdxOff = encodedMethod.methodIdxDiff 20 | if (index != 0) { 21 | methodIdxOff += previousMethodIdxOff 22 | } 23 | previousMethodIdxOff = methodIdxOff 24 | 25 | methodIds.add(methodIdxOff) 26 | } 27 | return methodIds 28 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/DexParser.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.parser 6 | 7 | import com.github.ajalt.clikt.core.CliktCommand 8 | import com.github.ajalt.clikt.parameters.arguments.argument 9 | import com.github.ajalt.clikt.parameters.arguments.default 10 | import com.github.ajalt.clikt.parameters.arguments.help 11 | import com.github.ajalt.clikt.parameters.options.help 12 | import com.github.ajalt.clikt.parameters.options.multiple 13 | import com.github.ajalt.clikt.parameters.options.option 14 | import com.linkedin.dex.spec.DexFile 15 | import java.io.File 16 | import java.io.FileInputStream 17 | import java.nio.ByteBuffer 18 | import java.nio.file.Files 19 | import java.util.zip.ZipEntry 20 | import java.util.zip.ZipInputStream 21 | 22 | /** 23 | * Parse an apk file to find any test methods and return the set of fully qualified method names 24 | * 25 | * Main entry point to the project. 26 | * NOTE: everything in the spec package is derived from the dex file format spec: 27 | * https://source.android.com/devices/tech/dalvik/dex-format.html 28 | */ 29 | private class DexParserCommand : CliktCommand() { 30 | val apkPath: String by argument().help("path to apk file") 31 | 32 | val outputDir by argument().help("path to output dir where AllTests.txt file will be saved, if not set output will go to stdout" 33 | ).default("") 34 | 35 | val customAnnotations: List by option("-A", "--annotation").multiple().help("add custom annotation used by tests") 36 | 37 | override fun run() { 38 | val allItems = DexParser.findTestNames(apkPath, customAnnotations) 39 | if (outputDir.isEmpty()) { 40 | println(allItems.joinToString(separator = "\n")) 41 | } else { 42 | Files.write(File("$outputDir/AllTests.txt").toPath(), allItems) 43 | } 44 | } 45 | } 46 | 47 | class DexParser private constructor() { 48 | companion object { 49 | 50 | /** 51 | * Main method included for easy local testing during development 52 | */ 53 | @JvmStatic 54 | fun main(vararg args: String) { 55 | DexParserCommand().main(args) 56 | } 57 | 58 | /** 59 | * Parse the apk found at [apkPath] and return the list of test names found in the apk 60 | */ 61 | @JvmStatic 62 | @JvmOverloads 63 | fun findTestNames(apkPath: String, customAnnotations: List = emptyList()): List { 64 | return findTestMethods(apkPath, customAnnotations).map { it.testName } 65 | } 66 | 67 | /** 68 | * Parse the apk found at [apkPath] and return a list of [TestMethod] objects containing the test names 69 | * and their associated annotation names found in the apk. Note that class-level annotations are also 70 | * included in the list of annotations for a given test and merged with the list of annotations that were 71 | * explicitly applied to the test method. 72 | */ 73 | @JvmStatic 74 | @JvmOverloads 75 | fun findTestMethods(apkPath: String, customAnnotations: List = emptyList()): List { 76 | val dexFiles = readDexFiles(apkPath) 77 | 78 | val junit3Items = findJUnit3Tests(dexFiles).sorted() 79 | val junit4Items = findAllJUnit4Tests(dexFiles, customAnnotations).sorted() 80 | 81 | return (junit3Items + junit4Items).sorted() 82 | } 83 | 84 | fun readDexFiles(path: String): List { 85 | ZipInputStream(FileInputStream(File(path))).use { zip -> 86 | return zip.entries() 87 | .filter { it.name.endsWith(".dex") } 88 | .map { zip.readBytes() } 89 | .map { ByteBuffer.wrap(it) } 90 | .map { DexFile(it) } 91 | .toList() 92 | } 93 | } 94 | 95 | private fun ZipInputStream.entries(): Sequence { 96 | return object : Sequence { 97 | override fun iterator(): Iterator { 98 | return object : Iterator { 99 | var hasPeekedNext: Boolean = false 100 | var next: ZipEntry? = null 101 | 102 | override fun hasNext(): Boolean { 103 | if (!hasPeekedNext) { 104 | next = nextEntry 105 | hasPeekedNext = true 106 | } 107 | return next != null 108 | } 109 | 110 | override fun next(): ZipEntry { 111 | hasPeekedNext = false 112 | return next!! 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/FormatUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.parser 6 | 7 | import com.linkedin.dex.spec.ClassDefItem 8 | import com.linkedin.dex.spec.DexFile 9 | 10 | /** 11 | * Format a class descriptor in a human-readable format that is also compatible with Android instrumentation tests 12 | * 13 | * For example, 14 | * "Lorg/junit/Test;" 15 | * would become: 16 | * "org.junit.Test" 17 | */ 18 | fun formatDescriptor(descriptor: String): String { 19 | return descriptor 20 | // strip off the "L" prefix 21 | .substring(1) 22 | // swap out slashes for periods 23 | .replace('/', '.') 24 | // strip off the ";" 25 | .dropLast(1) 26 | } 27 | 28 | /** 29 | * Extract the fully qualified class name from a [ClassDefItem] and format it for use with Android instrumentation tests 30 | * 31 | * @see [formatDescriptor] 32 | */ 33 | fun DexFile.formatClassName(classDefItem: ClassDefItem): String { 34 | val classDescriptor = ParseUtils.parseClassName(byteBuffer, classDefItem, typeIds, stringIds) 35 | // the instrument command expects test class names and method names to be separated by a "#" 36 | return formatDescriptor(classDescriptor) + "#" 37 | } 38 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/JUnit3Extensions.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.parser 6 | 7 | import com.linkedin.dex.spec.ClassDataItem 8 | import com.linkedin.dex.spec.ClassDefItem 9 | import com.linkedin.dex.spec.DexFile 10 | import com.linkedin.dex.spec.MethodIdItem 11 | 12 | /** 13 | * All of the classes that extend JUnit3's TestCase class and are included in the Android SDK. 14 | * This set is needed because tests inside the apk we're searching may extend any of these and since 15 | * these are part of the Android SDK, they won't be included in the apk we're searching (so they're root nodes) 16 | */ 17 | private val defaultDescriptors = setOf( 18 | "Ljunit/framework/TestCase;", 19 | "Landroid/test/ActivityInstrumentationTestCase;", 20 | "Landroid/test/ActivityInstrumentationTestCase2;", 21 | "Landroid/test/ActivityTestCase;", 22 | "Landroid/test/ActivityUnitTestCase;", 23 | "Landroid/test/AndroidTestCase;", 24 | "Landroid/test/ApplicationTestCase;", 25 | "Landroid/test/FailedToCreateTests;", 26 | "Landroid/test/InstrumentationTestCase;", 27 | "Landroid/test/LoaderTestCase;", 28 | "Landroid/test/ProviderTestCase;", 29 | "Landroid/test/ProviderTestCase2;", 30 | "Landroid/test/ServiceTestCase;", 31 | "Landroid/test/SingleLaunchActivityTestCase;", 32 | "Landroid/test/SyncBaseInstrumentation;" 33 | ) 34 | 35 | /** 36 | * Recursively search through the list of DexFiles to find all JUnit3 tests 37 | * 38 | * This function is O(n!), but in practice this is okay because test apks will have a very small number of DexFiles 39 | */ 40 | fun findJUnit3Tests(dexFiles: List): Set { 41 | return findJUnit3Tests(dexFiles, mutableSetOf(), defaultDescriptors.toMutableSet()).first 42 | } 43 | 44 | private fun findJUnit3Tests(dexFiles: List, testNames: MutableSet, 45 | descriptors: MutableSet): Pair, MutableSet> { 46 | // base case 47 | if (dexFiles.isEmpty()) { 48 | return Pair(testNames, descriptors) 49 | } 50 | 51 | // look through each dex file and find the test names and classes that extend TestCase 52 | // pass the class list through to the next file in case we find something that extends TestCase 53 | // in one dex file, and there's something that extends THAT in a later dex file 54 | dexFiles.forEach { dexFile -> 55 | val newTestNames = dexFile.findJUnit3Tests(descriptors) 56 | testNames.addAll(newTestNames) 57 | } 58 | 59 | // chop off the last dex file, we've found everything in it 60 | // recursively look through all the other dex files again in case there 61 | // are more tests found using the data we found in the last dex file 62 | return findJUnit3Tests(dexFiles.subList(0, dexFiles.lastIndex), testNames, descriptors) 63 | } 64 | 65 | private fun DexFile.findJUnit3Tests(descriptors: MutableSet): List { 66 | val testClasses = findClassesWithSuperClass(descriptors) 67 | return createTestMethods(testClasses, { classDef, _ -> findMethodIds(classDef) }) 68 | .filter { it.testName.contains("#test") } 69 | } 70 | 71 | fun DexFile.findMethodIds(classDefItem: ClassDefItem): MutableList { 72 | return findMethodIdxs(classDefItem).map { this.methodIds[it] }.toMutableList() 73 | } 74 | 75 | // From the docs: 76 | // The classes must be ordered such that a given class's superclass and 77 | // implemented interfaces appear in the list earlier than the referring class 78 | private fun DexFile.findClassesWithSuperClass(targetDescriptors: MutableSet): List { 79 | val matchingClasses: MutableList = mutableListOf() 80 | 81 | classDefs.forEach { classDefItem -> 82 | if (hasDirectSuperClass(classDefItem, targetDescriptors)) { 83 | matchingClasses.add(classDefItem) 84 | targetDescriptors.add(ParseUtils.parseDescriptor(byteBuffer, typeIds[classDefItem.classIdx], stringIds)) 85 | } 86 | } 87 | return matchingClasses 88 | } 89 | 90 | private fun DexFile.hasDirectSuperClass(classDefItem: ClassDefItem, targetDescriptors: Set): Boolean { 91 | if (classDefItem.superclassIdx == DexFile.NO_INDEX) { 92 | return false 93 | } 94 | 95 | val superType = typeIds[classDefItem.superclassIdx] 96 | val superDescriptor = ParseUtils.parseDescriptor(byteBuffer, superType, stringIds) 97 | return targetDescriptors.contains(superDescriptor) 98 | } 99 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/JUnit4Extensions.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.parser 6 | 7 | import com.linkedin.dex.spec.ACC_ABSTRACT 8 | import com.linkedin.dex.spec.ACC_INTERFACE 9 | import com.linkedin.dex.spec.AnnotationsDirectoryItem 10 | import com.linkedin.dex.spec.ClassDefItem 11 | import com.linkedin.dex.spec.DexFile 12 | import com.linkedin.dex.spec.MethodIdItem 13 | 14 | /** 15 | * Find all methods that are annotated with JUnit4's @Test annotation, including any test methods that 16 | * may be inherited from superclasses or interfaces. 17 | */ 18 | fun findAllJUnit4Tests(dexFiles: List, customAnnotations: List): List { 19 | 20 | // Map to hold all the class information we've found as we go 21 | // From the docs: 22 | // The classes must be ordered such that a given class's superclass and 23 | // implemented interfaces appear in the list earlier than the referring class 24 | // BUT it's not true for multiple dex files: superclass can be located in a different dex file 25 | // since the order is not guaranteed in this case we need to traverse all superclasses for each class 26 | val classTestMethods: Map = dexFiles.parseClasses(customAnnotations) 27 | 28 | // Map for the second iteration to cache all found test methods including methods from superclass 29 | val classAllTestMethods: MutableMap> = hashMapOf() 30 | 31 | return classTestMethods 32 | .values 33 | .filter { it.isConcrete } 34 | .map { value -> createAllTestMethods(value, classTestMethods, classAllTestMethods) } 35 | .flatten() 36 | } 37 | 38 | private fun List.parseClasses(customAnnotations: List): Map = 39 | asSequence() 40 | .flatMap { dexFile -> 41 | // We include classes that do not have annotations because there may be an intermediary class without tests 42 | // For example, TestClass1 defines a test, EmptyClass2 extends TestClass1 and defines nothing, and then TestClass2 43 | // extends EmptyClass2, TestClass2 should also list the tests defined in TestClass1 44 | dexFile 45 | .classDefs 46 | .asSequence() 47 | .filterNot { classDefItem -> classDefItem.isInterface } 48 | .map { classDef -> 49 | val testMethods = dexFile 50 | .createTestMethods(classDef, dexFile.findMethodIds()) 51 | .filter { it.containsTestAnnotation(customAnnotations) } 52 | 53 | ClassParsingResult( 54 | dexFile = dexFile, 55 | classDef = classDef, 56 | className = dexFile.getClassName(classDef), 57 | superClassName = dexFile.getSuperclassName(classDef), 58 | testMethods = testMethods.toSet(), 59 | isConcrete = classDef.isConcrete 60 | ) 61 | } 62 | } 63 | .associateBy { it.className } 64 | 65 | private const val JUNIT_TEST_ANNOTATION_NAME = "org.junit.Test" 66 | 67 | private fun TestMethod.containsTestAnnotation(customAnnotations: List): Boolean { 68 | for (a in customAnnotations) { 69 | if (annotations.map { it.name }.contains(a)) { 70 | return true 71 | } 72 | } 73 | if (annotations.map { it.name }.contains(JUNIT_TEST_ANNOTATION_NAME)) { 74 | return true 75 | } 76 | return false 77 | } 78 | 79 | /** 80 | * Find methodIds we care about: any method in the class which is annotated 81 | */ 82 | private fun DexFile.findMethodIds(): (ClassDefItem, AnnotationsDirectoryItem?) -> List { 83 | return { classDefItem, directory -> 84 | val annotatedIds = directory?.methodAnnotations?.map { it.methodIdx } ?: emptyList() 85 | this.findMethodIdxs(classDefItem).filter { 86 | annotatedIds.contains(it) 87 | }.map { methodIds[it] } 88 | } 89 | } 90 | 91 | private fun DexFile.getClassName(classDefItem: ClassDefItem): String { 92 | return ParseUtils.parseClassName(byteBuffer, classDefItem, typeIds, stringIds) 93 | } 94 | 95 | private fun DexFile.getSuperclassName(classDefItem: ClassDefItem): String { 96 | val superClassIdx = classDefItem.superclassIdx 97 | val typeId = typeIds[superClassIdx] 98 | 99 | return ParseUtils.parseDescriptor(byteBuffer, typeId, stringIds) 100 | } 101 | 102 | /** 103 | * Creates new TestMethod objects with the class name changed from the super class to the subclass 104 | */ 105 | private fun createAllTestMethods( 106 | parsingResult: ClassParsingResult, 107 | classTestMethods: Map, 108 | classAllTestMethods: MutableMap> 109 | ): Set = 110 | classAllTestMethods.getOrPut(parsingResult.className) { 111 | val dexFile = parsingResult.dexFile 112 | 113 | val superTestMethods = classTestMethods[parsingResult.superClassName] 114 | ?.let { createAllTestMethods(it, classTestMethods, classAllTestMethods) } 115 | ?: emptySet() 116 | 117 | val className = dexFile.formatClassName(parsingResult.classDef) 118 | val directory = dexFile.getAnnotationsDirectory(parsingResult.classDef) 119 | val childClassAnnotations = dexFile.getClassAnnotationValues(directory) 120 | val childClassAnnotationNames = childClassAnnotations.map { it.name } 121 | 122 | // We need to differentiate cases where a method is overridden from the superclass 123 | // vs when they are not, as it will impact whether we should include all annotations 124 | // from the superclass method or just ones with the inherited property 125 | // So, we exclude any that have overridden versions 126 | val adaptedSuperMethods = superTestMethods 127 | .filterNot { superMethod -> parsingResult.testMethods.any { 128 | it.testNameWithoutClass == superMethod.testNameWithoutClass 129 | } } 130 | .map { method -> 131 | val onlyParentAnnotations = method 132 | .annotations 133 | .filterNot { childClassAnnotationNames.contains(it.name) } 134 | 135 | TestMethod( 136 | testName = className + method.testNameWithoutClass, 137 | annotations = (onlyParentAnnotations + childClassAnnotations).toMutableList() 138 | ) 139 | } 140 | .toSet() 141 | 142 | // alter the existing test methods to include super annotations if they're inherited 143 | val alteredMethods = parsingResult.testMethods.filter { method -> superTestMethods.any { 144 | it.testNameWithoutClass == method.testNameWithoutClass 145 | } } 146 | .map { method -> 147 | val superMethod = superTestMethods.find { it.testNameWithoutClass == method.testNameWithoutClass } 148 | val onlyParentAnnotations = superMethod?.annotations ?: emptySet() 149 | val inheritedAnnotations = onlyParentAnnotations 150 | .filterNot { childClassAnnotationNames.contains(it.name) } 151 | .filter { it.inherited } 152 | 153 | TestMethod(method.testName, inheritedAnnotations + method.annotations) 154 | } 155 | 156 | val originalTestMethods = parsingResult.testMethods.filterNot { method -> superTestMethods.any { 157 | it.testNameWithoutClass == method.testNameWithoutClass 158 | } } 159 | 160 | return adaptedSuperMethods union alteredMethods union originalTestMethods 161 | } 162 | 163 | private val TestMethod.testNameWithoutClass 164 | get() = testName.substring(testName.indexOf('#') + 1) 165 | 166 | private val ClassDefItem.isConcrete: Boolean 167 | get() = !isAbstract && !isInterface 168 | 169 | private val ClassDefItem.isInterface: Boolean 170 | get() = accessFlags and ACC_INTERFACE == ACC_INTERFACE 171 | 172 | private val ClassDefItem.isAbstract: Boolean 173 | get() = accessFlags and ACC_ABSTRACT == ACC_ABSTRACT 174 | 175 | // Class to hold the information we have parsed about the classes we have already seen 176 | // 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 177 | // We keep isConcrete as well to make filtering at the end easier 178 | private data class ClassParsingResult( 179 | val dexFile: DexFile, 180 | val classDef: ClassDefItem, 181 | val className: String, 182 | val superClassName: String, 183 | val testMethods: Set, 184 | val isConcrete: Boolean 185 | ) 186 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/ParseUtils.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.parser 6 | 7 | import com.linkedin.dex.spec.ClassDefItem 8 | import com.linkedin.dex.spec.Leb128 9 | import com.linkedin.dex.spec.MethodIdItem 10 | import com.linkedin.dex.spec.StringIdItem 11 | import com.linkedin.dex.spec.TypeIdItem 12 | import java.nio.ByteBuffer 13 | import java.util.ArrayList 14 | 15 | /** 16 | * Utility methods for parsing different types of data out of a dex file. 17 | */ 18 | object ParseUtils { 19 | fun parseClassName(byteBuffer: ByteBuffer, classDefItem: ClassDefItem, 20 | typeIds: Array, stringIds: Array): String { 21 | val typeIdItem = typeIds[classDefItem.classIdx] 22 | return parseDescriptor(byteBuffer, typeIdItem, stringIds) 23 | } 24 | 25 | fun parseDescriptor(byteBuffer: ByteBuffer, typeIdItem: TypeIdItem, stringIds: Array): String { 26 | val descriptorIdx = typeIdItem.descriptorIdx 27 | val descriptorId = stringIds[descriptorIdx] 28 | byteBuffer.position(descriptorId.stringDataOff) 29 | // read past unused descriptorSize item 30 | Leb128.readUnsignedLeb128(byteBuffer) 31 | val encodedDescriptorName = parseStringBytes(byteBuffer) 32 | return encodedDescriptorName 33 | } 34 | 35 | fun parseMethodName(byteBuffer: ByteBuffer, stringIds: Array, methodId: MethodIdItem): String { 36 | val methodNameStringId = stringIds[methodId.nameIdx] 37 | byteBuffer.position(methodNameStringId.stringDataOff) 38 | // read past unused size item 39 | Leb128.readUnsignedLeb128(byteBuffer) 40 | val encodedName = parseStringBytes(byteBuffer) 41 | return encodedName 42 | } 43 | 44 | fun parseValueName(byteBuffer: ByteBuffer, stringIds: Array, nameIdx: Int): String { 45 | val stringId = stringIds[nameIdx] 46 | byteBuffer.position(stringId.stringDataOff) 47 | // read past unused size item 48 | Leb128.readUnsignedLeb128(byteBuffer) 49 | val encodedName = parseStringBytes(byteBuffer) 50 | return encodedName 51 | } 52 | 53 | fun parseStringBytes(byteBuffer: ByteBuffer): String { 54 | val byteList = ArrayList() 55 | do { 56 | val nextByte = byteBuffer.get() 57 | byteList.add(nextByte) 58 | } while (nextByte != 0.toByte()) 59 | byteList.removeAt(byteList.size - 1) 60 | 61 | return String(byteList.toByteArray()) 62 | } 63 | 64 | fun parseByteList(byteBuffer: ByteBuffer, length: Int): List { 65 | val tmpArray = ByteArray(length) 66 | byteBuffer.get(tmpArray, 0, length) 67 | return tmpArray.toList() 68 | } 69 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/TestAnnotation.kt: -------------------------------------------------------------------------------- 1 | package com.linkedin.dex.parser 2 | 3 | /** 4 | * A class to represent an annotation on method. Includes both the name of the annotation itself, 5 | * and all of the values within it as a key-value map of name string to value 6 | */ 7 | data class TestAnnotation(val name: String, val values: Map, val inherited: Boolean) 8 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/parser/TestMethod.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.parser 6 | 7 | import com.linkedin.dex.spec.AnnotationsDirectoryItem 8 | import com.linkedin.dex.spec.ClassDefItem 9 | import com.linkedin.dex.spec.DexFile 10 | import com.linkedin.dex.spec.MethodIdItem 11 | 12 | data class TestMethod(val testName: String, val annotations: List) : Comparable { 13 | override fun compareTo(other: TestMethod): Int = testName.compareTo(other.testName) 14 | } 15 | 16 | /** 17 | * Create the list of [TestMethod] contained in the given [ClassDefItem]s 18 | * 19 | * @param [classes] the list of [ClassDefItem]s in which to search for tests 20 | * @param [methodIdFinder] a function to determine which methods to consider as potential tests (varies between 21 | * JUnit3 and JUnit 4) 22 | */ 23 | fun DexFile.createTestMethods( 24 | classes: List, 25 | methodIdFinder: (ClassDefItem, AnnotationsDirectoryItem?) -> List): List { 26 | return classes.flatMap { classDef -> 27 | createTestMethods(classDef, methodIdFinder) 28 | } 29 | } 30 | 31 | /** 32 | * Create the list of [TestMethod] contained in the given class 33 | * 34 | * @param [classDef] The class to search for tests 35 | * @param [methodIdFinder] a function to determine which methods to consider as potential tests (varies between 36 | * JUnit3 and JUnit 4) 37 | */ 38 | fun DexFile.createTestMethods(classDef: ClassDefItem, methodIdFinder: (ClassDefItem, AnnotationsDirectoryItem?) -> List): List { 39 | val directory = getAnnotationsDirectory(classDef) 40 | 41 | // compute these outside the method loop to avoid duplicate work 42 | val classAnnotations = getClassAnnotationValues(directory) 43 | 44 | val methodIds = methodIdFinder.invoke(classDef, directory) 45 | 46 | return methodIds.map { createTestMethod(it, directory, classDef, classAnnotations) } 47 | } 48 | 49 | private fun DexFile.createTestMethod(methodId: MethodIdItem, 50 | directory: AnnotationsDirectoryItem?, 51 | classDef: ClassDefItem, 52 | classAnnotations: List): TestMethod { 53 | val methodAnnotationDescriptors = getMethodAnnotationValues(methodId, directory) 54 | 55 | val annotations = classAnnotations.plus(methodAnnotationDescriptors).toMutableList() 56 | 57 | val className = formatClassName(classDef) 58 | val methodName = ParseUtils.parseMethodName(byteBuffer, stringIds, methodId) 59 | val testName = className + methodName 60 | 61 | return TestMethod(testName, annotations) 62 | } 63 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/AccessFlags.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | const val ACC_PUBLIC = 0x1 8 | const val ACC_PRIVATE = 0x2 9 | const val ACC_PROTECTED = 0x4 10 | const val ACC_STATIC = 0x8 11 | const val ACC_FINAL = 0x10 12 | const val ACC_SYNCHRONIZED = 0x20 13 | const val ACC_VOLATILE = 0x40 14 | const val ACC_BRIDGE = 0x40 15 | const val ACC_TRANSIENT = 0x80 16 | const val ACC_VARARGS = 0x80 17 | const val ACC_NATIVE = 0x100 18 | const val ACC_INTERFACE = 0x200 19 | const val ACC_ABSTRACT = 0x400 20 | const val ACC_STRICT = 0x800 21 | const val ACC_SYNTHETIC = 0x1000 22 | const val ACC_ANNOTATION = 0x2000 23 | const val ACC_ENUM = 0x4000 24 | const val ACC_CONSTRUCTOR = 0x10000 25 | const val ACC_DECLARED_SYNCHRONIZED = 0x20000 26 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/AnnotationElement.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class AnnotationElement( 10 | val nameIdx: Int, 11 | val value: EncodedValue 12 | ) { 13 | constructor(byteBuffer: ByteBuffer) : this( 14 | nameIdx = Leb128.readUnsignedLeb128(byteBuffer), 15 | value = EncodedValue.create(byteBuffer) 16 | ) 17 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/AnnotationItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class AnnotationItem( 10 | val visibility: Byte, 11 | val encodedAnnotation: EncodedAnnotation 12 | ) { 13 | companion object { 14 | fun create(byteBuffer: ByteBuffer, offset: Int): AnnotationItem { 15 | byteBuffer.position(offset) 16 | 17 | return AnnotationItem( 18 | visibility = byteBuffer.get(), 19 | encodedAnnotation = EncodedAnnotation.create(byteBuffer) 20 | ) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/AnnotationOffItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class AnnotationOffItem( 10 | val annotationOff: Int 11 | ) { 12 | constructor(byteBuffer: ByteBuffer) : this( 13 | annotationOff = byteBuffer.int 14 | ) 15 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/AnnotationSetItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class AnnotationSetItem( 10 | val size: Int, 11 | val entries: Array 12 | ) { 13 | companion object { 14 | fun create(byteBuffer: ByteBuffer, offset: Int): AnnotationSetItem { 15 | byteBuffer.position(offset) 16 | 17 | val size = byteBuffer.int 18 | val entries = Array(size, { AnnotationOffItem(byteBuffer) }) 19 | return AnnotationSetItem(size, entries) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/AnnotationsDirectoryItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class AnnotationsDirectoryItem( 10 | val classAnnotationsOff: Int, 11 | val fieldsSize: Int, 12 | val annotatedMethodsSize: Int, 13 | val annotatedParametersSize: Int, 14 | val fieldAnnotations: Array, 15 | val methodAnnotations: Array, 16 | val parameterAnnotations: Array 17 | ) { 18 | companion object { 19 | fun create(byteBuffer: ByteBuffer, offset: Int): AnnotationsDirectoryItem { 20 | byteBuffer.position(offset) 21 | 22 | val classAnnotationsOff = byteBuffer.int 23 | val fieldsSize = byteBuffer.int 24 | val annotatedMethodsSize = byteBuffer.int 25 | val annotatedParametersSize = byteBuffer.int 26 | val fieldAnnotations = Array(fieldsSize, { FieldAnnotation(byteBuffer) }) 27 | val methodAnnotations = Array(annotatedMethodsSize, { MethodAnnotation(byteBuffer) }) 28 | val parameterAnnotations = Array(annotatedParametersSize, { ParameterAnnotation(byteBuffer) }) 29 | return AnnotationsDirectoryItem( 30 | classAnnotationsOff, 31 | fieldsSize, 32 | annotatedMethodsSize, 33 | annotatedParametersSize, 34 | fieldAnnotations, 35 | methodAnnotations, 36 | parameterAnnotations 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/ClassDataItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class ClassDataItem( 10 | val staticFieldsSize: Int, 11 | val instanceFieldsSize: Int, 12 | val directMethodsSize: Int, 13 | val virtualMethodsSize: Int, 14 | val staticFields: Array, 15 | val instanceFields: Array, 16 | val directMethods: Array, 17 | val virtualMethods: Array 18 | ) { 19 | companion object { 20 | fun create(byteBuffer: ByteBuffer, offset: Int): ClassDataItem { 21 | byteBuffer.position(offset) 22 | 23 | val staticFieldsSize = Leb128.readUnsignedLeb128(byteBuffer) 24 | val instanceFieldsSize = Leb128.readUnsignedLeb128(byteBuffer) 25 | val directMethodsSize = Leb128.readUnsignedLeb128(byteBuffer) 26 | val virtualMethodsSize = Leb128.readUnsignedLeb128(byteBuffer) 27 | val staticFields = Array(staticFieldsSize, { EncodedField(byteBuffer) }) 28 | val instanceFields = Array(instanceFieldsSize, { EncodedField(byteBuffer) }) 29 | val directMethods = Array(directMethodsSize, { EncodedMethod(byteBuffer) }) 30 | val virtualMethods = Array(virtualMethodsSize, { EncodedMethod(byteBuffer) }) 31 | 32 | return ClassDataItem( 33 | staticFieldsSize, 34 | instanceFieldsSize, 35 | directMethodsSize, 36 | virtualMethodsSize, 37 | staticFields, 38 | instanceFields, 39 | directMethods, 40 | virtualMethods 41 | ) 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/ClassDefItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class ClassDefItem( 10 | val classIdx: Int, 11 | val accessFlags: Int, 12 | val superclassIdx: Int, 13 | val interfacesOff: Int, 14 | val sourceFileIdx: Int, 15 | val annotationsOff: Int, 16 | val classDataOff: Int, 17 | val staticValuesOff: Int 18 | ) { 19 | companion object { 20 | val size: Int = 0x20 21 | } 22 | 23 | constructor(byteBuffer: ByteBuffer) : this( 24 | classIdx = byteBuffer.int, 25 | accessFlags = byteBuffer.int, 26 | superclassIdx = byteBuffer.int, 27 | interfacesOff = byteBuffer.int, 28 | sourceFileIdx = byteBuffer.int, 29 | annotationsOff = byteBuffer.int, 30 | classDataOff = byteBuffer.int, 31 | staticValuesOff = byteBuffer.int 32 | ) 33 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/DexException.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | class DexException(override val message: String?) : RuntimeException(message) -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/DexFile.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import com.linkedin.dex.parser.ParseUtils 8 | import java.nio.ByteBuffer 9 | import java.nio.ByteOrder 10 | 11 | class DexFile(byteBuffer: ByteBuffer) { 12 | val byteBuffer: ByteBuffer 13 | val headerItem: HeaderItem 14 | val stringIds: Array 15 | val typeIds: Array 16 | val protoIds: Array 17 | val fieldIds: Array 18 | val methodIds: Array 19 | val classDefs: Array 20 | 21 | companion object { 22 | val NO_INDEX = -1 23 | } 24 | 25 | inline fun parse(count: Int, offset: Int, size: Int, init: (ByteBuffer) -> T): Array { 26 | return Array(count, { index -> 27 | byteBuffer.position(offset + (index * size)) 28 | init(byteBuffer) 29 | }) 30 | } 31 | 32 | init { 33 | this.byteBuffer = byteBuffer.asReadOnlyBuffer().order(ByteOrder.LITTLE_ENDIAN) 34 | this.byteBuffer.position(0) 35 | headerItem = HeaderItem(this.byteBuffer) 36 | headerItem.validate() 37 | stringIds = parse(headerItem.stringIdsSize, headerItem.stringIdsOff, StringIdItem.size) { StringIdItem(it) } 38 | typeIds = parse(headerItem.typeIdsSize, headerItem.typeIdsOff, TypeIdItem.size) { TypeIdItem(it) } 39 | protoIds = parse(headerItem.protoIdsSize, headerItem.protoIdsOff, ProtoIdItem.size) { ProtoIdItem(it) } 40 | fieldIds = parse(headerItem.fieldIdsSize, headerItem.fieldIdsOff, FieldIdItem.size) { FieldIdItem(it) } 41 | methodIds = parse(headerItem.methodIdsSize, headerItem.methodIdsOff, MethodIdItem.size) { MethodIdItem(it) } 42 | classDefs = parse(headerItem.classDefsSize, headerItem.classDefsOff, ClassDefItem.size) { ClassDefItem(it) } 43 | } 44 | 45 | val inheritedAnnotationTypeIdIndex: Int? by lazy { 46 | var result: Int? = null 47 | typeIds.forEachIndexed { index, typeIdItem -> 48 | if (ParseUtils.parseDescriptor(byteBuffer, typeIdItem, stringIds) == "Ljava/lang/annotation/Inherited;") { 49 | result = index 50 | } 51 | } 52 | 53 | result 54 | } 55 | 56 | val typeIdToClassDefMap: Map by lazy { 57 | val map = mutableMapOf() 58 | 59 | for (classDef in classDefs) { 60 | map[classDef.classIdx] = classDef 61 | } 62 | map.toMap() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/DexMagic.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import com.linkedin.dex.parser.ParseUtils 8 | import java.nio.ByteBuffer 9 | 10 | data class DexMagic( 11 | val dex: List, 12 | val newline: Byte, 13 | val version: List, 14 | val zero: Byte 15 | ) { 16 | constructor(byteBuffer: ByteBuffer) : this( 17 | dex = ParseUtils.parseByteList(byteBuffer, 3), 18 | newline = byteBuffer.get(), 19 | version = ParseUtils.parseByteList(byteBuffer, 3), 20 | zero = byteBuffer.get() 21 | ) 22 | 23 | fun validate() { 24 | if ((dex[0].toInt() == 0x64) and 25 | (dex[1].toInt() == 0x65) and 26 | (dex[2].toInt() == 0x78) and 27 | (newline.toInt() == 0x0A) and 28 | (version[0].toInt() == 0x30) and 29 | (version[1].toInt() == 0x33) and 30 | (version[2].toInt() >= 0x35) and 31 | (zero.toInt() == 0x00)) { 32 | return 33 | } 34 | throw DexException("Invalid dexMagic:\n" + this + "\n") 35 | } 36 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/EncodedAnnotation.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class EncodedAnnotation( 10 | val typeIdx: Int, 11 | val size: Int, 12 | val elements: Array 13 | ) { 14 | companion object { 15 | fun create(byteBuffer: ByteBuffer): EncodedAnnotation { 16 | val typeIdx = Leb128.readUnsignedLeb128(byteBuffer) 17 | val size = Leb128.readUnsignedLeb128(byteBuffer) 18 | val elements = Array(size, { AnnotationElement(byteBuffer) }) 19 | return EncodedAnnotation(typeIdx, size, elements) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/EncodedArray.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class EncodedArray( 10 | val size: Int, 11 | val values: Array 12 | ) { 13 | companion object { 14 | fun create(byteBuffer: ByteBuffer): EncodedArray { 15 | val size = Leb128.readUnsignedLeb128(byteBuffer) 16 | val values = Array(size, { EncodedValue.create(byteBuffer) }) 17 | return EncodedArray(size, values) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/EncodedField.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class EncodedField( 10 | val fieldIdxDiff: Int, 11 | val accessFlags: Int 12 | ) { 13 | constructor(byteBuffer: ByteBuffer) : this( 14 | fieldIdxDiff = Leb128.readUnsignedLeb128(byteBuffer), 15 | accessFlags = Leb128.readUnsignedLeb128(byteBuffer) 16 | ) 17 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/EncodedMethod.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class EncodedMethod( 10 | val methodIdxDiff: Int, 11 | val accessFlags: Int, 12 | val codeOff: Int 13 | ) { 14 | constructor(byteBuffer: ByteBuffer) : this( 15 | methodIdxDiff = Leb128.readUnsignedLeb128(byteBuffer), 16 | accessFlags = Leb128.readUnsignedLeb128(byteBuffer), 17 | codeOff = Leb128.readUnsignedLeb128(byteBuffer) 18 | ) 19 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/EncodedValue.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | import java.nio.ByteOrder 9 | 10 | sealed class EncodedValue { 11 | data class EncodedByte(val value: Byte): EncodedValue() 12 | data class EncodedShort(val value: Short): EncodedValue() 13 | data class EncodedChar(val value: Char): EncodedValue() 14 | data class EncodedInt(val value: Int): EncodedValue() 15 | data class EncodedLong(val value: Long): EncodedValue() 16 | data class EncodedFloat(val value: Float): EncodedValue() 17 | data class EncodedDouble(val value: Double): EncodedValue() 18 | // Value is an index into the string table 19 | data class EncodedString(val value: Int): EncodedValue() 20 | data class EncodedType(val value: Int): EncodedValue() 21 | data class EncodedField(val value: Int): EncodedValue() 22 | data class EncodedMethod(val value: Int): EncodedValue() 23 | data class EncodedEnum(val value: Int): EncodedValue() 24 | data class EncodedArrayValue(val value: EncodedArray): EncodedValue() 25 | data class EncodedAnnotationValue(val value: EncodedAnnotation): EncodedValue() 26 | object EncodedNull: EncodedValue() 27 | data class EncodedBoolean(val value: Boolean): EncodedValue() 28 | companion object { 29 | val VALUE_BYTE: Byte = 0x00 30 | val VALUE_SHORT: Byte = 0x02 31 | val VALUE_CHAR: Byte = 0x03 32 | val VALUE_INT: Byte = 0x04 33 | val VALUE_LONG: Byte = 0x06 34 | val VALUE_FLOAT: Byte = 0x10 35 | val VALUE_DOUBLE: Byte = 0x11 36 | val VALUE_STRING: Byte = 0x17 37 | val VALUE_TYPE: Byte = 0x18 38 | val VALUE_FIELD: Byte = 0x19 39 | val VALUE_METHOD: Byte = 0x1a 40 | val VALUE_ENUM: Byte = 0x1b 41 | val VALUE_ARRAY: Byte = 0x1c 42 | val VALUE_ANNOTATION: Byte = 0x1d 43 | val VALUE_NULL: Byte = 0x1e 44 | val VALUE_BOOLEAN: Byte = 0x1f 45 | 46 | fun create(byteBuffer: ByteBuffer): EncodedValue { 47 | val argAndType = byteBuffer.get().toUnsigned8BitInt() 48 | 49 | // first three bits are the optional valueArg 50 | val valueArg = (argAndType ushr 5).toByte() 51 | // last five bits are the valueType 52 | val valueType = (argAndType and 0x1F).toByte() 53 | 54 | when (valueType) { 55 | VALUE_BYTE -> return EncodedByte(byteBuffer.get()) 56 | VALUE_SHORT -> return EncodedShort(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 2).short) 57 | VALUE_CHAR -> return EncodedChar(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 2).char) 58 | VALUE_INT -> return EncodedInt(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 4).int) 59 | VALUE_LONG -> return EncodedLong(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 8).long) 60 | VALUE_FLOAT -> return EncodedFloat(getPaddedBufferToTheRight(byteBuffer, sizeOf(valueArg), 4).float) 61 | VALUE_DOUBLE -> return EncodedDouble(getPaddedBufferToTheRight(byteBuffer, sizeOf(valueArg), 8).double) 62 | VALUE_STRING -> return EncodedString(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 4).int) 63 | VALUE_TYPE -> return EncodedType(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 4).int) 64 | VALUE_FIELD -> return EncodedField(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 4).int) 65 | VALUE_METHOD -> return EncodedMethod(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 4).int) 66 | VALUE_ENUM -> return EncodedEnum(getPaddedBuffer(byteBuffer, sizeOf(valueArg), 4).int) 67 | VALUE_ARRAY -> return EncodedArrayValue(EncodedArray.create(byteBuffer)) 68 | VALUE_ANNOTATION -> return EncodedAnnotationValue(EncodedAnnotation.create(byteBuffer)) 69 | VALUE_NULL -> return EncodedNull 70 | VALUE_BOOLEAN -> return EncodedBoolean(valueArg.toInt() == 1) 71 | else -> { 72 | throw DexException("Bad value type: " + valueType) 73 | } 74 | } 75 | } 76 | 77 | // The size of the field is generaly represented as 1 more than the value of the first byte 78 | // See https://source.android.com/devices/tech/dalvik/dex-format#encoding 79 | private fun sizeOf(valueArg: Byte): Int { 80 | return valueArg + 1 81 | } 82 | 83 | // In the dex format, when a value can be represented with less than the bytes defined by its type (ex, an Int 84 | // that can be represented in only 1 byte), then it does not pad the extra bytes 85 | // ByteBuffer makes parsing bytes nice since it handles endianness and other small issues, so we can just create 86 | // a buffer and fill in the extra bits not specified in the file to fill the appropriate size for the type 87 | private fun getPaddedBuffer(byteBuffer: ByteBuffer, size: Int, fullSize: Int): ByteBuffer { 88 | val buffer = ByteBuffer.allocate(fullSize) 89 | buffer.order(ByteOrder.LITTLE_ENDIAN) 90 | var i = 0 91 | while (i < size) { 92 | i++ 93 | buffer.put(byteBuffer.get()) 94 | } 95 | for (x in size+1..fullSize) { 96 | buffer.put(0) 97 | } 98 | 99 | // Move to the start of the buffer so we can read values 100 | buffer.position(0) 101 | 102 | return buffer 103 | } 104 | 105 | // For float and double values, the value is padded to the right, so we need to build the buffer in the 106 | // opposite order 107 | private fun getPaddedBufferToTheRight(byteBuffer: ByteBuffer, size: Int, fullSize: Int): ByteBuffer { 108 | val buffer = ByteBuffer.allocate(fullSize) 109 | buffer.order(ByteOrder.LITTLE_ENDIAN) 110 | 111 | for (x in size+1..fullSize) { 112 | buffer.put(0) 113 | } 114 | 115 | var i = 0 116 | while (i < size) { 117 | i++ 118 | buffer.put(byteBuffer.get()) 119 | } 120 | 121 | 122 | // Move to the start of the buffer so we can read values 123 | buffer.position(0) 124 | 125 | return buffer 126 | } 127 | } 128 | } 129 | 130 | private fun Byte.toUnsigned8BitInt(): Int = (this.toInt() and 0xFF) 131 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/FieldAnnotation.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class FieldAnnotation( 10 | val fieldIdx: Int, 11 | val annotationsOff: Int 12 | ) { 13 | companion object { 14 | val size: Int = 4 15 | } 16 | 17 | constructor(byteBuffer: ByteBuffer) : this( 18 | fieldIdx = byteBuffer.int, 19 | annotationsOff = byteBuffer.int 20 | ) 21 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/FieldIdItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class FieldIdItem( 10 | val classIdx: Short, 11 | val typeIdx: Short, 12 | val nameIdx: Int 13 | ) { 14 | companion object { 15 | val size: Int = 8 16 | } 17 | 18 | constructor(byteBuffer: ByteBuffer) : this( 19 | classIdx = byteBuffer.short, 20 | typeIdx = byteBuffer.short, 21 | nameIdx = byteBuffer.int 22 | ) 23 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/HeaderItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import com.linkedin.dex.parser.ParseUtils 8 | import java.nio.ByteBuffer 9 | 10 | data class HeaderItem( 11 | val magic: DexMagic, 12 | val checksum: Int, 13 | val signature: List, 14 | val fileSize: Int, 15 | val headerSize: Int, 16 | val endianTag: Int, 17 | val linkSize: Int, 18 | val linkOff: Int, 19 | val mapOff: Int, 20 | val stringIdsSize: Int, 21 | val stringIdsOff: Int, 22 | val typeIdsSize: Int, 23 | val typeIdsOff: Int, 24 | val protoIdsSize: Int, 25 | val protoIdsOff: Int, 26 | val fieldIdsSize: Int, 27 | val fieldIdsOff: Int, 28 | val methodIdsSize: Int, 29 | val methodIdsOff: Int, 30 | val classDefsSize: Int, 31 | val classDefsOff: Int, 32 | val dataSize: Int, 33 | val dataOff: Int 34 | ) { 35 | constructor(byteBuffer: ByteBuffer) : this( 36 | magic = DexMagic(byteBuffer), 37 | checksum = byteBuffer.int, 38 | signature = ParseUtils.parseByteList(byteBuffer, 20), 39 | fileSize = byteBuffer.int, 40 | headerSize = byteBuffer.int, 41 | endianTag = byteBuffer.int, 42 | linkSize = byteBuffer.int, 43 | linkOff = byteBuffer.int, 44 | mapOff = byteBuffer.int, 45 | stringIdsSize = byteBuffer.int, 46 | stringIdsOff = byteBuffer.int, 47 | typeIdsSize = byteBuffer.int, 48 | typeIdsOff = byteBuffer.int, 49 | protoIdsSize = byteBuffer.int, 50 | protoIdsOff = byteBuffer.int, 51 | fieldIdsSize = byteBuffer.int, 52 | fieldIdsOff = byteBuffer.int, 53 | methodIdsSize = byteBuffer.int, 54 | methodIdsOff = byteBuffer.int, 55 | classDefsSize = byteBuffer.int, 56 | classDefsOff = byteBuffer.int, 57 | dataSize = byteBuffer.int, 58 | dataOff = byteBuffer.int 59 | ) 60 | 61 | fun validate() { 62 | magic.validate() 63 | 64 | val expectedEndianTag = 0x12345678; 65 | if (endianTag != expectedEndianTag) { 66 | throw DexException("Invalid endian tag:" + endianTag) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/Leb128.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | class Leb128 private constructor() { 10 | companion object { 11 | /** 12 | * Reads an unsigned integer from byteBuffer. 13 | */ 14 | fun readUnsignedLeb128(byteBuffer: ByteBuffer): Int { 15 | var result = 0 16 | var current: Int 17 | var count = 0 18 | 19 | do { 20 | current = byteBuffer.get().toInt() and 0xff 21 | result = result or (current and 0x7f shl count * 7) 22 | count++ 23 | } while (current and 0x80 == 0x80 && count < 5) 24 | 25 | if (current and 0x80 == 0x80) { 26 | throw DexException("invalid LEB128 sequence") 27 | } 28 | 29 | return result 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/MethodAnnotation.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class MethodAnnotation( 10 | val methodIdx: Int, 11 | val annotationsOff: Int 12 | ) { 13 | companion object { 14 | val size: Int = 4 15 | } 16 | 17 | constructor(byteBuffer: ByteBuffer) : this( 18 | methodIdx = byteBuffer.int, 19 | annotationsOff = byteBuffer.int 20 | ) 21 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/MethodIdItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class MethodIdItem( 10 | val classIdx: Short, 11 | val protoIdx: Short, 12 | val nameIdx: Int 13 | ) { 14 | companion object { 15 | val size: Int = 8 16 | } 17 | 18 | constructor(byteBuffer: ByteBuffer) : this( 19 | classIdx = byteBuffer.short, 20 | protoIdx = byteBuffer.short, 21 | nameIdx = byteBuffer.int 22 | ) 23 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/ParameterAnnotation.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class ParameterAnnotation( 10 | val parameterIdx: Int, 11 | val annotationsOff: Int 12 | ) { 13 | companion object { 14 | val size: Int = 4 15 | } 16 | 17 | constructor(byteBuffer: ByteBuffer) : this( 18 | parameterIdx = byteBuffer.int, 19 | annotationsOff = byteBuffer.int 20 | ) 21 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/ProtoIdItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class ProtoIdItem( 10 | val shortyIdx: Int, 11 | val returnTypeIdx: Int, 12 | val parametersOff: Int 13 | ) { 14 | companion object { 15 | val size: Int = 12 16 | } 17 | 18 | constructor(byteBuffer: ByteBuffer) : this( 19 | shortyIdx = byteBuffer.int, 20 | returnTypeIdx = byteBuffer.int, 21 | parametersOff = byteBuffer.int 22 | ) 23 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/StringIdItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class StringIdItem( 10 | val stringDataOff: Int 11 | ) { 12 | companion object { 13 | val size: Int = 4 14 | } 15 | 16 | constructor(byteBuffer: ByteBuffer) : this( 17 | stringDataOff = byteBuffer.int 18 | ) 19 | } -------------------------------------------------------------------------------- /parser/src/main/kotlin/com/linkedin/dex/spec/TypeIdItem.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.dex.spec 6 | 7 | import java.nio.ByteBuffer 8 | 9 | data class TypeIdItem( 10 | val descriptorIdx: Int 11 | ) { 12 | companion object { 13 | val size: Int = 4 14 | } 15 | 16 | constructor(byteBuffer: ByteBuffer) : this( 17 | descriptorIdx = byteBuffer.int 18 | ) 19 | } -------------------------------------------------------------------------------- /parser/src/test/fixtures/abstract-class-in-second-dex.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/dex-test-parser/589f2520bfe63e2e36c9d550d1fd12d7c59c2028/parser/src/test/fixtures/abstract-class-in-second-dex.apk -------------------------------------------------------------------------------- /parser/src/test/kotlin/com/linkedin/dex/AbstractClassInSecondDex.kt: -------------------------------------------------------------------------------- 1 | package com.linkedin.dex 2 | 3 | import com.linkedin.dex.parser.DexParser 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Test 6 | 7 | class AbstractClassInSecondDex { 8 | 9 | companion object { 10 | /** 11 | * This apk contains two dex files: 12 | * 13 | * classes.dex: 14 | * 15 | * class ConcreteTest : AbstractTest { 16 | * @Test fun concreteTest() { ... } 17 | * } 18 | * 19 | * classes2.dex: 20 | * 21 | * abstract class AbstractTest { 22 | * @Test fun abstractTest() { ... } 23 | * } 24 | * 25 | * The archive was created in a way that "classes.dex" goes before "classes2.dex" 26 | */ 27 | const val APK_PATH = "parser/src/test/fixtures/abstract-class-in-second-dex.apk" 28 | } 29 | 30 | @Test 31 | fun parseMethodFromBaseAbstractClass_whenAbstractClassInTheSecondDex() { 32 | val testMethods = DexParser 33 | .findTestMethods(APK_PATH, listOf("")) 34 | .filter { it.testName == "com.linkedin.parser.test.junit4.java.BasicJUnit4#abstractTest" } 35 | 36 | assertEquals(1, testMethods.size) 37 | } 38 | 39 | @Test 40 | fun parseMethodFromConcreteClassThatExtendsFromAbstract_whenAbstractClassInTheSecondDex() { 41 | val testMethods = DexParser 42 | .findTestMethods(APK_PATH, listOf("")) 43 | .filter { it.testName == "com.linkedin.parser.test.junit4.java.BasicJUnit4#concreteTest" } 44 | 45 | assertEquals(1, testMethods.size) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /parser/src/test/kotlin/com/linkedin/dex/DexParserShould.kt: -------------------------------------------------------------------------------- 1 | package com.linkedin.dex 2 | 3 | import com.linkedin.dex.parser.DecodedValue 4 | import com.linkedin.dex.parser.DexParser 5 | import com.linkedin.dex.parser.TestMethod 6 | import org.junit.Assert.assertArrayEquals 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Assert.assertFalse 9 | import org.junit.Assert.assertNotNull 10 | import org.junit.Assert.assertTrue 11 | import org.junit.Test 12 | 13 | class DexParserShould { 14 | companion object { 15 | val APK_PATH = "test-app/build/outputs/apk/androidTest/debug/test-app-debug-androidTest.apk" 16 | } 17 | 18 | @Test 19 | fun parseCorrectNumberOfTestMethods() { 20 | val testMethods = DexParser.findTestNames(APK_PATH, listOf("")) 21 | 22 | assertEquals(23, testMethods.size) 23 | } 24 | 25 | @Test 26 | fun parseMethodWithMultipleMethodAnnotations() { 27 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.annotations.filter { it.name.contains("TestValueAnnotation") }.isNotEmpty() } 28 | 29 | assertEquals(5, testMethods.size) 30 | 31 | val method = testMethods[1] 32 | assertEquals(method.testName, "com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4") 33 | // TestValueAnnotation at the class level, Test annotation at the method level, and TestValueAnnotation at the method level 34 | assertEquals(method.annotations.size, 3) 35 | } 36 | 37 | @Test 38 | fun parseMethodWithChildclassAnnotation() { 39 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.annotations.filter { it.name.contains("TestValueAnnotation") }.isNotEmpty() } 40 | 41 | val method = testMethods[0] 42 | assertEquals("com.linkedin.parser.test.junit4.java.BasicJUnit4#abstractTest", method.testName) 43 | assertEquals(method.annotations[2].values["stringValue"], DecodedValue.DecodedString("Hello world!")) 44 | } 45 | 46 | @Test 47 | fun parseInheritedMethodAnnotation() { 48 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.annotations.filter { it.name.contains("InheritedAnnotation") }.isNotEmpty() } 49 | 50 | val method = testMethods[0] 51 | assertEquals("com.linkedin.parser.test.junit4.java.BasicJUnit4#concreteTest", method.testName) 52 | assertEquals(method.annotations[2].values["stringValue"], DecodedValue.DecodedString("Hello world!")) 53 | } 54 | 55 | @Test 56 | fun parsNonInheritedMethodAnnotation() { 57 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.annotations.filter { it.name.contains("InheritedAnnotation") }.isNotEmpty() } 58 | 59 | val method = testMethods.find { it.testName.contains("BasicJUnit4#concreteTest") } 60 | requireNotNull(method) 61 | assertEquals("com.linkedin.parser.test.junit4.java.BasicJUnit4#concreteTest", method.testName) 62 | assertFalse(method.annotations.any { it.name.contains("NonInheritedAnnotation") }) 63 | } 64 | 65 | @Test 66 | fun parseInheritedClassAnnotation() { 67 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.annotations.filter { it.name.contains("InheritedAnnotation") }.isNotEmpty() } 68 | 69 | val method = testMethods[0] 70 | assertEquals("com.linkedin.parser.test.junit4.java.BasicJUnit4#concreteTest", method.testName) 71 | assertTrue(method.annotations.any { it.name == "com.linkedin.parser.test.junit4.java.InheritedClassAnnotation" }) 72 | } 73 | 74 | @Test 75 | fun parseStringAnnotationValues() { 76 | val method = getBasicJunit4TestMethod() 77 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 78 | 79 | val classAnnotation = valueAnnotations.first() 80 | val stringValue = classAnnotation.values["stringValue"] 81 | assertNotNull(stringValue) 82 | assertMatches(stringValue, "Hello world!") 83 | 84 | val methodAnnotation = valueAnnotations[1] 85 | val methodStringValue = methodAnnotation.values["stringValue"] 86 | assertMatches(methodStringValue, "On a method") 87 | } 88 | 89 | @Test 90 | fun parseIntAnnotationValues() { 91 | val method = getBasicJunit4TestMethod() 92 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 93 | 94 | val methodAnnotation = valueAnnotations[1] 95 | val value = methodAnnotation.values["intValue"] 96 | assertMatches(value, 12345) 97 | } 98 | 99 | @Test 100 | fun parseBoolAnnotationValues() { 101 | val method = getBasicJunit4TestMethod() 102 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 103 | 104 | val methodAnnotation = valueAnnotations[1] 105 | val value = methodAnnotation.values["boolValue"] 106 | assertMatches(value, true) 107 | } 108 | 109 | @Test 110 | fun parseLongAnnotationValues() { 111 | val method = getBasicJunit4TestMethod() 112 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 113 | 114 | val methodAnnotation = valueAnnotations[1] 115 | val value = methodAnnotation.values["longValue"] 116 | assertMatches(value, 56789L) 117 | } 118 | 119 | @Test 120 | fun parseFloatAnnotationValues() { 121 | val method = getSecondBasicJunit4TestMethod() 122 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 123 | 124 | val methodAnnotation = valueAnnotations[1] 125 | val value = methodAnnotation.values["floatValue"] 126 | assertMatches(value, 0.25f) 127 | } 128 | 129 | @Test 130 | fun parseDoubleAnnotationValues() { 131 | val method = getSecondBasicJunit4TestMethod() 132 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 133 | 134 | val methodAnnotation = valueAnnotations[1] 135 | val value = methodAnnotation.values["doubleValue"] 136 | assertMatches(value, 0.5) 137 | } 138 | 139 | @Test 140 | fun parseByteAnnotationValues() { 141 | val method = getSecondBasicJunit4TestMethod() 142 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 143 | 144 | val methodAnnotation = valueAnnotations[1] 145 | val value = methodAnnotation.values["byteValue"] 146 | assertMatches(value, 0x0f.toByte()) 147 | } 148 | 149 | @Test 150 | fun parseFloatMaxValuesInDoubleFields() { 151 | val method = getSecondBasicJunit4TestMethod() 152 | val valueAnnotations = method.annotations.filter { it.name.contains("FloatRange") } 153 | 154 | val methodAnnotation = valueAnnotations[0] 155 | val from = methodAnnotation.values["from"] 156 | assertMatches(from, 0f.toDouble()) 157 | val to = methodAnnotation.values["to"] 158 | assertMatches(to, Float.MAX_VALUE.toDouble()) 159 | } 160 | 161 | @Test 162 | fun parseCharAnnotationValues() { 163 | val method = getSecondBasicJunit4TestMethod() 164 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 165 | 166 | val methodAnnotation = valueAnnotations[1] 167 | val value = methodAnnotation.values["charValue"] 168 | assertMatches(value, '?') 169 | } 170 | 171 | @Test 172 | fun parseShortAnnotationValues() { 173 | val method = getSecondBasicJunit4TestMethod() 174 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 175 | 176 | val methodAnnotation = valueAnnotations[1] 177 | val value = methodAnnotation.values["shortValue"] 178 | assertMatches(value, 3.toShort()) 179 | } 180 | 181 | @Test 182 | fun parseClassArrayAnnotationnValues() { 183 | val method = getSecondBasicJunit4TestMethod() 184 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 185 | 186 | val methodAnnotation = valueAnnotations[1] 187 | val value = methodAnnotation.values["arrayTypeValue"] 188 | 189 | // We have to use a string as opposed to a class reference in this assertion, since the way 190 | // that its actually stored on disk is their special class format and not what class.name 191 | // will give 192 | assertMatches(value, arrayOf("Ljava/util/function/Function;", "Ljava/lang/Integer;")) 193 | } 194 | 195 | @Test 196 | fun parseMultipleValuesInASingleAnnotation() { 197 | val method = getBasicJunit4TestMethod() 198 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 199 | 200 | val methodAnnotation = valueAnnotations[1] 201 | assertMatches(methodAnnotation.values["stringValue"], "On a method") 202 | assertMatches(methodAnnotation.values["intValue"], 12345) 203 | assertMatches(methodAnnotation.values["boolValue"], true) 204 | assertMatches(methodAnnotation.values["longValue"], 56789L) 205 | } 206 | 207 | @Test 208 | fun parseEnumAnnotation() { 209 | val method = getSecondBasicJunit4TestMethod() 210 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 211 | 212 | val methodAnnotation = valueAnnotations[1] 213 | assertMatches(methodAnnotation.values["enumValue"], "FAIL") 214 | } 215 | 216 | @Test 217 | fun parseTypeAnnotation() { 218 | val method = getSecondBasicJunit4TestMethod() 219 | val valueAnnotations = method.annotations.filter { it.name.contains("TestValueAnnotation") } 220 | 221 | val methodAnnotation = valueAnnotations[1] 222 | assertMatches(methodAnnotation.values["typeValue"], "Lorg/junit/Test;") 223 | } 224 | 225 | @Test 226 | fun parseAbstractClassTestMethodCorrectly() { 227 | val method = getTestMethodFromAbstractClass() 228 | val annotations = method.annotations 229 | 230 | assertEquals(annotations.size, 2) 231 | assertTrue(annotations.any { it.name == "org.junit.Test" }) 232 | } 233 | 234 | private fun getBasicJunit4TestMethod(): TestMethod { 235 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.annotations.filter { it.name.contains("TestValueAnnotation") }.isNotEmpty() }.filter { it.testName.equals("com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4") } 236 | 237 | assertEquals(1, testMethods.size) 238 | 239 | val method = testMethods.first() 240 | assertEquals(method.testName, "com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4") 241 | 242 | return method 243 | } 244 | 245 | private fun getSecondBasicJunit4TestMethod(): TestMethod { 246 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.annotations.filter { it.name.contains("TestValueAnnotation") }.isNotEmpty() }.filter { it.testName.equals("com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4Second") } 247 | 248 | assertEquals(1, testMethods.size) 249 | 250 | val method = testMethods.first() 251 | assertEquals(method.testName, "com.linkedin.parser.test.junit4.java.BasicJUnit4#basicJUnit4Second") 252 | 253 | return method 254 | } 255 | 256 | private fun getTestMethodFromAbstractClass(): TestMethod { 257 | val testMethods = DexParser.findTestMethods(APK_PATH, listOf("")).filter { it.testName.equals("com.linkedin.parser.test.junit4.java.ConcreteTest#abstractTest") } 258 | 259 | assertEquals(1, testMethods.size) 260 | 261 | val method = testMethods.first() 262 | assertEquals(method.testName, "com.linkedin.parser.test.junit4.java.ConcreteTest#abstractTest") 263 | 264 | return method 265 | } 266 | 267 | // region value type matchers 268 | private fun assertMatches(value: DecodedValue?, string: String) { 269 | if (value is DecodedValue.DecodedString) { 270 | assertEquals(string, value.value) 271 | } else if (value is DecodedValue.DecodedEnum) { 272 | assertEquals(string, value.value) 273 | } else if (value is DecodedValue.DecodedType) { 274 | assertEquals(string, value.value) 275 | } else { 276 | throw Exception("Value was not a string type") 277 | } 278 | } 279 | 280 | private fun assertMatches(value: DecodedValue?, number: Int) { 281 | if (value is DecodedValue.DecodedInt) { 282 | assertEquals(number, value.value) 283 | } else { 284 | throw Exception("Value was not an int type") 285 | } 286 | } 287 | 288 | private fun assertMatches(value: DecodedValue?, bool: Boolean) { 289 | if (value is DecodedValue.DecodedBoolean) { 290 | assertEquals(bool, value.value) 291 | } else { 292 | throw Exception("Value was not a boolean type") 293 | } 294 | } 295 | 296 | private fun assertMatches(value: DecodedValue?, long: Long) { 297 | if (value is DecodedValue.DecodedLong) { 298 | assertEquals(long, value.value) 299 | } else { 300 | throw Exception("Value was not a long type") 301 | } 302 | } 303 | 304 | private fun assertMatches(value: DecodedValue?, float: Float) { 305 | if (value is DecodedValue.DecodedFloat) { 306 | assertEquals(float, value.value) 307 | } else { 308 | throw Exception("Value was not a float type") 309 | } 310 | } 311 | 312 | private fun assertMatches(value: DecodedValue?, double: Double) { 313 | if (value is DecodedValue.DecodedDouble) { 314 | assertEquals(double, value.value, 0.0) 315 | } else { 316 | throw Exception("Value was not a double type") 317 | } 318 | } 319 | 320 | private fun assertMatches(value: DecodedValue?, byte: Byte) { 321 | if (value is DecodedValue.DecodedByte) { 322 | assertEquals(byte, value.value) 323 | } else { 324 | throw Exception("Value was not a byte type") 325 | } 326 | } 327 | 328 | private fun assertMatches(value: DecodedValue?, char: Char) { 329 | if (value is DecodedValue.DecodedChar) { 330 | assertEquals(char, value.value) 331 | } else { 332 | throw Exception("Value was not a char type") 333 | } 334 | } 335 | 336 | private fun assertMatches(value: DecodedValue?, short: Short) { 337 | if (value is DecodedValue.DecodedShort) { 338 | assertEquals(short, value.value) 339 | } else { 340 | throw Exception("Value was not a short type") 341 | } 342 | } 343 | 344 | private fun assertMatches(value: DecodedValue?, values: Array) { 345 | if (value is DecodedValue.DecodedArrayValue) { 346 | val stringValues = value.values.map { (it as? DecodedValue.DecodedType)?.value }.toTypedArray() 347 | assertArrayEquals(stringValues, values) 348 | } else { 349 | throw Exception("Value was not an array value") 350 | } 351 | } 352 | 353 | // endregion 354 | } 355 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':parser', ':test-app' 2 | -------------------------------------------------------------------------------- /test-app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 26 6 | buildToolsVersion "29.0.2" 7 | defaultConfig { 8 | applicationId "com.linkedin.parser.test" 9 | minSdkVersion 15 10 | targetSdkVersion 26 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | lintOptions { 15 | abortOnError false 16 | } 17 | 18 | } 19 | 20 | repositories { 21 | google() 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 27 | } 28 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/java/JUnit3ActivityInstrumentationTestCase2.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit3.java; 6 | 7 | import android.test.ActivityInstrumentationTestCase2; 8 | 9 | public class JUnit3ActivityInstrumentationTestCase2 extends ActivityInstrumentationTestCase2 { 10 | 11 | public JUnit3ActivityInstrumentationTestCase2() { 12 | super(null); 13 | } 14 | 15 | public void testJUnit3ActivityInstrumentationTestCase2() throws Exception { 16 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/java/JUnit3Basic.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit3.java; 6 | 7 | import junit.framework.TestCase; 8 | 9 | public class JUnit3Basic extends TestCase { 10 | 11 | public void testJUnit3Basic() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/java/JUnit3InstrumentationTestCase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit3.java; 6 | 7 | import android.test.InstrumentationTestCase; 8 | 9 | public class JUnit3InstrumentationTestCase extends InstrumentationTestCase { 10 | 11 | public void testJUnit3InstrumentationTestCase() throws Exception { 12 | assertTrue(true); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/java/JUnit3TestInsideStaticInnerClass.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit3.java; 6 | 7 | import android.test.SingleLaunchActivityTestCase; 8 | 9 | public class JUnit3TestInsideStaticInnerClass extends SingleLaunchActivityTestCase { 10 | 11 | public JUnit3TestInsideStaticInnerClass() { 12 | super(null, null); 13 | } 14 | 15 | public void testJUnit3TestInsideStaticInnerClass() throws Exception { 16 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/java/JUnit3WithAnnotations.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit3.java; 6 | 7 | import android.support.test.filters.SmallTest; 8 | import android.test.ActivityUnitTestCase; 9 | 10 | public class JUnit3WithAnnotations extends ActivityUnitTestCase { 11 | 12 | public JUnit3WithAnnotations() { 13 | super(null); 14 | } 15 | 16 | @SmallTest 17 | public void testJUnit3WithAnnotations() throws Exception { 18 | assertTrue(true); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/kotlin/KotlinJUnit3AndroidTestCase.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | @file:Suppress("DEPRECATION") 6 | 7 | package com.linkedin.parser.test.junit3.kotlin 8 | 9 | import android.test.AndroidTestCase 10 | 11 | class KotlinJUnit3AndroidTestCase : AndroidTestCase() { 12 | 13 | fun testKotlinJUnit3AndroidTestCase() { 14 | assertTrue(true) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/kotlin/KotlinJUnit3TestInsideStaticInnerClass.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | @file:Suppress("DEPRECATION") 6 | 7 | package com.linkedin.parser.test.junit3.kotlin 8 | 9 | import android.app.Service 10 | import android.test.ServiceTestCase 11 | 12 | class KotlinJUnit3TestInsideStaticInnerClass { 13 | 14 | class InnerClass : ServiceTestCase(null) { 15 | 16 | fun testKotlinJUnit3TestInsideStaticInnerClass() { 17 | assertTrue(true) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit3/kotlin/KotlinJUnit3WithAnnotations.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | @file:Suppress("DEPRECATION") 6 | 7 | package com.linkedin.parser.test.junit3.kotlin 8 | 9 | import android.app.Application 10 | import android.support.test.filters.MediumTest 11 | import android.test.ApplicationTestCase 12 | 13 | class KotlinJUnit3WithAnnotations : ApplicationTestCase(null) { 14 | 15 | @MediumTest 16 | fun testKotlinJUnit3WithAnnotations() { 17 | assertTrue(true) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/AbstractTest.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.parser.test.junit4.java; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public abstract class AbstractTest { 8 | 9 | @Test 10 | public void abstractTest() { 11 | assertTrue(true); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/BasicJUnit4.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import org.junit.Test; 8 | 9 | import java.util.function.Function; 10 | 11 | import static org.junit.Assert.assertTrue; 12 | 13 | @TestValueAnnotation(stringValue = "Hello world!") 14 | public class BasicJUnit4 extends ConcreteTest { 15 | 16 | @Test 17 | @TestValueAnnotation(stringValue = "On a method", intValue = 12345, boolValue = true, longValue = 56789L, enumValue = TestEnum.SUCCESS) 18 | public void basicJUnit4() { 19 | assertTrue(true); 20 | } 21 | 22 | @Test 23 | @TestValueAnnotation(floatValue = 0.25f, doubleValue = 0.5, byteValue = 0x0f, charValue = '?', shortValue = 3, enumValue = TestEnum.FAIL, typeValue = Test.class, arrayTypeValue = { Function.class, Integer.class}) 24 | @FloatRange(from = 0f, to = Float.MAX_VALUE) 25 | public void basicJUnit4Second() { 26 | assertTrue(true); 27 | } 28 | 29 | @Test 30 | private void privateTestShouldNotBeReported() { 31 | assertTrue(true); 32 | } 33 | 34 | @Override 35 | @Test 36 | public void concreteTest() { 37 | super.concreteTest(); 38 | } 39 | 40 | @NonInheritedAnnotation 41 | public void customAnnotationTest() { 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/ConcreteTest.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.parser.test.junit4.java; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | @InheritedClassAnnotation 8 | public class ConcreteTest extends AbstractTest { 9 | 10 | @NonInheritedAnnotation 11 | @InheritedAnnotation 12 | @Test 13 | public void concreteTest() { 14 | assertTrue(true); 15 | } 16 | 17 | @NonInheritedAnnotation 18 | @InheritedAnnotation 19 | @Test 20 | public void nonOverriddenConcreteTest() { 21 | assertTrue(true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/FloatRange.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.parser.test.junit4.java; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.FIELD; 7 | import static java.lang.annotation.ElementType.LOCAL_VARIABLE; 8 | import static java.lang.annotation.ElementType.METHOD; 9 | import static java.lang.annotation.ElementType.PARAMETER; 10 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 11 | 12 | @Retention(RUNTIME) 13 | @Target({ METHOD, PARAMETER, FIELD, LOCAL_VARIABLE}) 14 | public @interface FloatRange { 15 | /** Smallest value. Whether it is inclusive or not is determined 16 | * by {@link #fromInclusive} */ 17 | double from() default Double.NEGATIVE_INFINITY; 18 | /** Largest value. Whether it is inclusive or not is determined 19 | * by {@link #toInclusive} */ 20 | double to() default Double.POSITIVE_INFINITY; 21 | /** Whether the from value is included in the range */ 22 | boolean fromInclusive() default true; 23 | /** Whether the to value is included in the range */ 24 | boolean toInclusive() default true; 25 | } -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/IgnoreJUnit4TestInterface.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import org.junit.Test; 8 | 9 | public interface IgnoreJUnit4TestInterface { 10 | 11 | @Test 12 | void thisTestShouldNotBeReported(); 13 | } 14 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/InheritedAnnotation.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import java.lang.annotation.*; 8 | 9 | @Inherited 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target({ ElementType.METHOD, ElementType.TYPE }) 12 | public @interface InheritedAnnotation { 13 | } 14 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/InheritedClassAnnotation.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import java.lang.annotation.*; 8 | 9 | @Inherited 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target({ ElementType.METHOD, ElementType.TYPE }) 12 | public @interface InheritedClassAnnotation { 13 | } 14 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/JUnit4ClassInsideInterface.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import org.junit.Test; 8 | 9 | import static org.junit.Assert.assertTrue; 10 | 11 | public interface JUnit4ClassInsideInterface { 12 | 13 | class InnerClass { 14 | @Test 15 | public void innerClassTest() { 16 | assertTrue(true); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/JUnit4TestInsideStaticInnerClass.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import org.junit.Test; 8 | 9 | import static org.junit.Assert.assertTrue; 10 | 11 | public class JUnit4TestInsideStaticInnerClass { 12 | 13 | public static class InnerClass { 14 | 15 | @Test 16 | public void innerClassTest() { 17 | assertTrue(true); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/NonInheritedAnnotation.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import java.lang.annotation.*; 8 | 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Target({ ElementType.METHOD, ElementType.TYPE }) 11 | public @interface NonInheritedAnnotation { 12 | } 13 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/TestEnum.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.parser.test.junit4.java; 2 | 3 | public enum TestEnum { 4 | SUCCESS, 5 | FAIL 6 | } 7 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/java/TestValueAnnotation.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.java; 6 | 7 | import android.support.annotation.NonNull; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target({ java.lang.annotation.ElementType.METHOD, ElementType.TYPE }) 16 | public @interface TestValueAnnotation { 17 | 18 | @NonNull 19 | String stringValue() default ""; 20 | 21 | int intValue() default 0; 22 | 23 | boolean boolValue() default false; 24 | 25 | long longValue() default 0L; 26 | 27 | float floatValue() default 0f; 28 | 29 | double doubleValue() default 0d; 30 | 31 | byte byteValue() default 0; 32 | 33 | char charValue() default 0; 34 | 35 | short shortValue() default 0; 36 | 37 | TestEnum enumValue() default TestEnum.SUCCESS; 38 | 39 | Class typeValue() default Object.class; 40 | 41 | Class[] arrayTypeValue() default {}; 42 | } 43 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/kotlin/DefaultInterfaceImplementation.kt: -------------------------------------------------------------------------------- 1 | package com.linkedin.parser.test.junit4.kotlin 2 | 3 | import org.junit.Test 4 | 5 | class DefaultInterfaceImplementation : InterfaceWithDefaultMethods { 6 | @Test 7 | override fun testToBeOverrideShouldNotBeReportedInInterface() { 8 | super.testToBeOverrideShouldNotBeReportedInInterface() 9 | } 10 | } -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/kotlin/InterfaceWithDefaultMethods.kt: -------------------------------------------------------------------------------- 1 | package com.linkedin.parser.test.junit4.kotlin 2 | 3 | import org.junit.Test 4 | 5 | interface InterfaceWithDefaultMethods { 6 | @Test 7 | fun testMethodShouldNotBeReported() { 8 | } 9 | 10 | @Test 11 | fun testToBeOverrideShouldNotBeReportedInInterface() { 12 | } 13 | } -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/kotlin/KotlinJUnit4Basic.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.kotlin 6 | 7 | import org.junit.Assert.assertTrue 8 | import org.junit.Test 9 | 10 | class KotlinJUnit4Basic { 11 | 12 | @Test 13 | fun testKotlinJUnit4Basic() { 14 | assertTrue(true) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/kotlin/KotlinJUnit4TestInsideStaticInnerClass.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.kotlin 6 | 7 | import org.junit.Assert.assertTrue 8 | import org.junit.Test 9 | 10 | class KotlinJUnit4TestInsideStaticInnerClass { 11 | 12 | class InnerClass { 13 | 14 | @Test 15 | fun testKotlinJUnit4TestInsideStaticInnerClass() { 16 | assertTrue(true) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test-app/src/androidTest/java/com/linkedin/parser/test/junit4/kotlin/KotlinJUnit4WithAnnotations.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | package com.linkedin.parser.test.junit4.kotlin 6 | 7 | import android.support.test.filters.LargeTest 8 | import org.junit.Assert.assertTrue 9 | import org.junit.Test 10 | 11 | class KotlinJUnit4WithAnnotations { 12 | 13 | @Test 14 | @LargeTest 15 | fun testKotlinJUnit4WithAnnotations() { 16 | assertTrue(true) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------