├── .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 | [](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 |
--------------------------------------------------------------------------------