diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0959132 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## Proposed Changes + +## Testing + + + +## Issues Fixed \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cc45e5d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: App Sizer CI + +on: + push: + branches: [ main, master ] + pull_request: + workflow_dispatch: + inputs: + release: + description: 'Trigger a release build' + required: false + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + TERM: dumb + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install JDK 17 + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "17" + - name: Build + run: ./gradlew assemble + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: | + gradle-plugin/build/libs/*.jar + clt/build/libs/*.jar + retention-days: 7 + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install JDK 17 + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "17" + - name: Run tests + run: ./gradlew test + + build-sample-app: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install JDK 17 + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "17" + - name: Build sample app + run: | + cd ./sample + ./gradlew app:appSizeAnalysisProRelease --stacktrace -Dorg.gradle.debug=false --no-daemon + + publish-artifact: + runs-on: ubuntu-latest + if: github.event.inputs.release == 'publish' + steps: + - uses: actions/checkout@v4 + - name: Install JDK 17 + uses: actions/setup-java@v4 + with: + distribution: "zulu" + java-version: "17" + - name: Publish to Artifactory + run: ./gradlew artifactoryPublish \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ef9d7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle/ +local.properties +.idea/* +!.idea/copyright +sample/.idea/* +.DS_Store +/build/ +build/ +/captures + + +# Windows thumbnail db +# Idea non-crucial project fileS +*.iws +.java-version diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..829b267 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,39 @@ +default: + image: gitlab.myteksi.net:4567/mobile/platform/mobile-tooling/base-android:34.0.0 + tags: + - 2xlarge +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +stages: + - build + - test + - publish + +build: + stage: build + script: + - ./gradlew assemble + artifacts: + paths: + - gradle-plugin/build/libs/*.jar + - clt/build/libs/*.jar + expire_in: 1 week + +test: + stage: test + script: + - ./gradlew test + +build_sample_app: + stage: test + script: + - cd ./sample + - ./gradlew app:appSizeAnalysisProRelease --stacktrace -Dorg.gradle.debug=false --no-daemon + +publish_artifact: + stage: build + rules: + - if: $RELEASE == "publish" + script: + - ./gradlew artifactoryPublish \ No newline at end of file diff --git a/.idea/copyright/MIT_License.xml.xml b/.idea/copyright/MIT_License.xml.xml new file mode 100644 index 0000000..65ffc55 --- /dev/null +++ b/.idea/copyright/MIT_License.xml.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..2c65983 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e0dd016 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +MIT License + + +Copyright 2024 Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..36b3f12 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# App Sizer +See the [project website][app-sizer-website] for documentation and APIs. + +## Overview +App Sizer is a tool designed to analyze the download size of Android applications. By providing detailed insights into the composition of your app's binary, App Sizer helps developers identify areas for size reduction, ultimately improving user acquisition and retention rates. + +*The app download size in Android refers to the amount of data a user needs to download from an app store (typically Google Play Store) to install an application on their Android device* + +

+ +

+ +## Key Features +App Sizer offers comprehensive analysis including: +1. Total app download size +2. Detailed size breakdown +3. Size contribution by teams +4. Module-wise size contribution +5. Size contribution by libraries +6. List of large files + +Reports are generated based on the provided Android device specifications. Our [blogpost][blog-post] introduce the tool features + +## Quick Start + +App Sizer provides two flexible integration methods: + +* A Gradle plugin that seamlessly integrates with your Android Gradle project. +* A command-line tool to cater to non-Gradle build systems, offering the same comprehensive features. + + *Note: The command-line option was the original implementation and remains supported for broader compatibility.* + +### Gradle Plugin Integration +In root `build.gradle`: + +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "com.grab:app-sizer:SNAPSHOT" + } +} +``` +In the app module 's `build.gradle` +```groovy +apply plugin: "com.grab.app-sizer" + +// AppSizer configuration +appSizer { + // DSL +} +``` + +To run analysis, execute + +``` +./gradlew app:appSizeAnalysisRelease --no-configure-on-demand +``` + +For plugin configuration options, see [Plugin Configuration][plugin_doc]. + +### Cli Tool Integration +To generate the command line binary file, execute +```text +./gradlew clt:shadowJar +``` + +To run analysis using the command line tool, execute +```text +java -jar clt-all.jar --config-file ./path/to/config/app-size-settings.yml +``` + +For command line configuration options, see [Commandline Configuration][cli_doc]. + +## Report Types + +App Sizer currently supports three types of reports: + +* InfluxDB database (1.x) - suitable for CI tracking and enabling the creation of customized dashboards. For InfluxDB and Grafana setup, see our [Docker Setup Guide][grafana-docker]. +* Markdown table for convenient local analysis. +* JSON data for compatibility with other platforms. + +For more detail on reports, see [Report Detail][report_doc] + +## How it works +App Sizer functions as a mapping tool to generate the report. It takes APK, AAR, and JAR files as inputs. +1. **Input parsing**: +- The tool parses the APK down to file and class levels. It calculates the contribution of each component to the total app download size. +- Similarly, App Sizer parses AAR and JAR files. +2. **Mapping and Report Generation**: +- The tool then maps the APK components to their corresponding elements in the AAR and JAR files. +- Based on this analysis and other metadata, App Sizer generates comprehensive reports detailing size contributions. + +## Limitations + +App Sizer approximates class download sizes due to Dex structure complexity, and may not accurately attribute sizes for inline functions or uncategorized files. Results should be interpreted as close estimates, best used for identifying trends and relative size comparisons rather than exact measurements. + +For more details on limitations, see the [Limitation][limitation_doc]. + +## Components +* [Gradle Plugin][gradle-plugin] +* [Command line tool][commandline-tool] +* [InfluxDb & Grafana Docker][grafana-docker] + +## License + +``` +MIT License + + +Copyright 2024 Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +``` +[app-sizer-website]: TBA +[report_doc]: ./docs/report.md +[plugin_doc]: ./docs/plugin.md +[cli_doc]: ./docs/cli.md +[limitation_doc]:./docs/limitation.md +[gradle-plugin]: ./gradle-plugin +[commandline-tool]: ./clt +[grafana-docker]: ./docker +[blog-post]: https://engineering.grab.com/project-bonsai + + + + + + + diff --git a/app-sizer/build.gradle b/app-sizer/build.gradle new file mode 100644 index 0000000..b2f26e3 --- /dev/null +++ b/app-sizer/build.gradle @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +plugins { + id 'com.grab.sizer.kotlin' +} + +dependencies { + /** + * These two libs are already in the AGP + */ + compileOnly libs.android.tools.bundletool + compileOnly libs.android.tools.common + + implementation libs.org.influxdb.client + implementation libs.org.smali.dexlib2 + implementation libs.android.tools.apkanalyzer + implementation libs.guava + implementation libs.google.dagger + implementation libs.gson + + kapt libs.dagger.compiler + + testImplementation libs.kotlin.test + testImplementation libs.android.tools.bundletool + testImplementation libs.android.tools.common +} + +ext.artifactId = "app-sizer" \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/AnalyticsOptions.kt b/app-sizer/src/main/kotlin/com/grab/sizer/AnalyticsOptions.kt new file mode 100644 index 0000000..71a7649 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/AnalyticsOptions.kt @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import java.io.Serializable + +/** + * The AnalyticsOption enum represents reporting options for the app-sizer tool: + * - LIBRARIES: Generates a report on library metrics, providing insights into how each library contributes to the total app download size. + * - DEFAULT: Generates a comprehensive reports covering all options except LIB_CONTENT. + * - APK: Generates a report breaking down the App Download Size by Components. Sections include android-java-libraries, codebase-kotlin-java, codebase-resources, codebase-assets, codebase-native, and native libraries. + * - BASIC: Provides a fundamental breakdown report of the App Download size, similar to opening the APK in Android Studio. + * - MODULES: Generates a report showing the contribution of each module to the total App Download Size. Grouping by team should be feasible. + * - CODEBASE: Produces a report showing the contribution of each team to the total App Download Size. + * - LARGE_FILE: Generates a list of files whose sizes exceed a certain threshold. + * - LIB_CONTENT: Provides a breakdown of a single library's size contribution (Resources, Assets, Native libraries, Classes, others). + */ +enum class AnalyticsOption : Serializable { + LIBRARIES, + DEFAULT, + APK, + BASIC, + MODULES, + CODEBASE, + LARGE_FILE, + LIB_CONTENT; + + companion object { + /** + * Converts a string value to the corresponding AnalyticsOption. + * If the value does not match any predefined options, the DEFAULT option is chosen. + */ + fun fromString(value: String?): AnalyticsOption = when (value) { + "libraries" -> LIBRARIES + "modules" -> MODULES + "apk" -> APK + "basic" -> BASIC + "codebase" -> CODEBASE + "large-files" -> LARGE_FILE + "lib-content" -> LIB_CONTENT + else -> DEFAULT + } + } +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/AppSizer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/AppSizer.kt new file mode 100644 index 0000000..16811cc --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/AppSizer.kt @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import com.grab.sizer.di.DaggerAnalyzerComponent +import com.grab.sizer.utils.InputProvider +import com.grab.sizer.utils.Logger +import com.grab.sizer.utils.OutputProvider + +class AppSizer( + private val inputProvider: InputProvider, + private val outputProvider: OutputProvider, + private val libName: String?, + private val logger: Logger +) { + fun process(option: AnalyticsOption) { + val analyzerComponent = DaggerAnalyzerComponent.factory() + .create( + inputProvider = inputProvider, + outputProvider = outputProvider, + libName = libName, + logger = logger + ) + val analyzerMap = analyzerComponent.analyzerMap() + val reportWriters = analyzerComponent.reportWriters() + if (option == AnalyticsOption.DEFAULT) { + analyzerMap + .filterKeys { it != AnalyticsOption.LIB_CONTENT } + .map { (key, analyzer) -> key to analyzer.process() } + .onEach { (_, report) -> + reportWriters.forEach { reportWriter -> + reportWriter.write( + report + ) + } + } + } else { + analyzerMap[option]?.run { + reportWriters.forEach { reportWriter -> + reportWriter.write( + process() + ) + } + } + } + + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/Analyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/Analyzer.kt new file mode 100644 index 0000000..5d06ee7 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/Analyzer.kt @@ -0,0 +1,40 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.report.Report + + +/** + * An Analyzer interface represents the logic that needs to be executed based on the [com.grab.sizer.AnalyticsOption] + * Each [com.grab.sizer.AnalyticsOption] should have an implement of Analyzer + * It produces its own [Report] and should be designed to operate independently of other analyzers. + */ +interface Analyzer { + fun process() : Report +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ApkAnalyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ApkAnalyzer.kt new file mode 100644 index 0000000..0293f89 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ApkAnalyzer.kt @@ -0,0 +1,170 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.ApkComponentProcessor +import com.grab.sizer.analyzer.model.Contributor +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.DataParser +import com.grab.sizer.report.Report +import com.grab.sizer.report.Row +import com.grab.sizer.report.apksSizeReport +import com.grab.sizer.report.toReportField +import java.io.File +import javax.inject.Inject + +internal const val CODE_BASE_ID = "Codebase" + +/** + * A specific implementation of the Analyzer, focusing on APK analysis. + * This class take responsibility to handle [com.grab.sizer.AnalyticsOption.APK] + * It processes APK, AAR, and JAR components and generates detailed reports that break down the binary download size into: + * - android-java-libraries: Represents the size contributions from jar & aar libraries (excluding native .so files). + * - codebase-kotlin-java: Denotes the size contributions from the codebase's Java and Kotlin classes. + * - codebase-resources: Specifies the size contributions from the codebase's resources (eg: images, layouts). + * - codebase-assets: Represents the size contributions from the codebase's asset files. + * - codebase-native: Symbolizes the size contributions from the codebase's native C/C++ libraries. + * - native-libraries: Indicates the size contributions from native libraries (C/C++). + * + * @property apkComponentProcessor Responsible for processing APK, AAR or JAR files to generate the contributors + * @property dataParser to parse APK, AAR or JAR files. + */ +internal class ApkAnalyzer @Inject constructor( + private val apkComponentProcessor: ApkComponentProcessor, + private val dataParser: DataParser +) : Analyzer { + override fun process(): Report { + val processedData = apkComponentProcessor.process( + dataParser.apks, + dataParser.libAars, + dataParser.libJars + ) + return generateReport(dataParser.apks, processedData.contributors) + } + + private fun generateReport(apks: Set, contributors: Set): Report { + val contributorList = contributors.sortedBy { it.getDownloadSize() } + val apkReportRow = createApkReportRow(apks) + val totalLibsReport = totalLibrariesReport(contributorList) + val libComponentReport = libComponentReport(totalLibsReport) + val apkReport = apks.apksSizeReport() + val codeBaseReports = codeBaseComponentReport(codeBaseReport(totalLibsReport, apkReport)) + val listOfReport = listOf(apkReportRow) + codeBaseReports + libComponentReport + + return Report( + rows = listOfReport, + id = METRICS_ID_APK, + name = METRICS_ID_APK, + ) + } + + private fun createApkReportRow( + apks: Set + ) = Row( + fields = apks.toReportField(), + name = "Apk" + ) + + private fun codeBaseReport( + totalLibsReport: ReportItem, + apkReport: ReportItem + ): ReportItem = ReportItem( + id = CODE_BASE_ID, + name = CODE_BASE_ID, + totalDownloadSize = apkReport.totalDownloadSize - totalLibsReport.totalDownloadSize, + otherDownloadSize = apkReport.otherDownloadSize - totalLibsReport.otherDownloadSize, + resourceDownloadSize = apkReport.resourceDownloadSize - totalLibsReport.resourceDownloadSize, + nativeLibDownloadSize = apkReport.nativeLibDownloadSize - totalLibsReport.nativeLibDownloadSize, + assetDownloadSize = apkReport.assetDownloadSize - totalLibsReport.assetDownloadSize, + classesDownloadSize = apkReport.classesDownloadSize - totalLibsReport.classesDownloadSize, + ) + + private fun Contributor.toReportItem(): ReportItem = ReportItem( + name = File(path).nameWithoutExtension, + extraInfo = path.substring(path.indexOf("files-2.1/") + 9), + id = File(path).nameWithoutExtension, + totalDownloadSize = getDownloadSize(), + classesDownloadSize = classDownloadSize, + nativeLibDownloadSize = nativeLibDownloadSize, + resourceDownloadSize = resourcesDownloadSize, + assetDownloadSize = assetsDownloadSize, + otherDownloadSize = othersDownloadSize, + ) + + + private fun libComponentReport(allLibReport: ReportItem): List = listOf( + createRow( + name = "android-java-libraries", + value = allLibReport.totalDownloadSize - allLibReport.nativeLibDownloadSize + ), + createRow( + name = "native-libraries", + value = allLibReport.nativeLibDownloadSize + ) + ) + + private fun codeBaseComponentReport(codeBaseReport: ReportItem): List = listOf( + createRow( + name = "codebase-kotlin-java", + value = codeBaseReport.classesDownloadSize, + ), + createRow( + name = "codebase-resources", + value = codeBaseReport.resourceDownloadSize, + ), + createRow( + name = "codebase-assets", + value = codeBaseReport.assetDownloadSize, + ), + createRow( + name = "codebase-native", + value = codeBaseReport.nativeLibDownloadSize, + ), + createRow( + name = "others", + value = codeBaseReport.otherDownloadSize, + ), + ) + + private fun totalLibrariesReport(data: List): ReportItem { + return data.reduce { pre, cur -> + pre.copy( + resources = pre.resources + cur.resources, + assets = pre.assets + cur.assets, + nativeLibs = pre.nativeLibs + cur.nativeLibs, + classes = pre.classes + cur.classes, + others = pre.others + cur.others + ) + }.toReportItem() + .copy( + name = "All libraries", + extraInfo = "Sum up all libraries values", + id = "all_libraries", + ) + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/BasicAnalyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/BasicAnalyzer.kt new file mode 100644 index 0000000..6f45e14 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/BasicAnalyzer.kt @@ -0,0 +1,98 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.DataParser +import com.grab.sizer.report.Report +import com.grab.sizer.report.Row +import javax.inject.Inject + +/** + * A specialized implementation of the Analyzer interface that focuses on basic APK analysis. + * This class specifically handles [com.grab.sizer.AnalyticsOption.BASIC] and provides metrics similar to those + * obtained by opening the APK file in Android Studio, including: + * - apk : The total download size of the app. + * - resource : The cumulative size contribution from resources such as images and layouts. + * - native_lib : The cumulative size contribution from native libraries. + * - asset : The cumulative size contribution from assets. + * - code : The cumulative size contribution from Java/Kotlin code. + * + * @property dataParser Parses APK, AAR and JAR for analysis. + **/ +internal class BasicApkAnalyzer @Inject constructor( + private val dataParser: DataParser +) : Analyzer { + override fun process(): Report { + val androidBinaryInfo = dataParser.apks + return Report( + rows = androidBinaryInfo.createApkReportRows(), + id = METRICS_ID_BASIC, + name = METRICS_ID_BASIC, + ) + } + + private fun Set.createApkReportRows(): List { + val resourceDownloadSize = flatMap { it.resources }.sumOf { it.downloadSize } + val nativeLibDownloadSize = flatMap { it.nativeLibs }.sumOf { it.downloadSize } + val assetDownloadSize = flatMap { it.assets }.sumOf { it.downloadSize } + val otherDownloadSize = flatMap { it.others }.sumOf { it.downloadSize } + val dexDownloadFile = flatMap { it.dexes }.sumOf { it.downloadSize } + val classDownloadSize = flatMap { it.dexes }.flatMap { it.classes }.sumOf { it.downloadSize } + val total = + resourceDownloadSize + nativeLibDownloadSize + assetDownloadSize + otherDownloadSize + classDownloadSize + + return listOf( + createRow( + name = "apk", + value = total + ), + createRow( + name = "resource", + value = resourceDownloadSize + ), + createRow( + name = "native_lib", + value = nativeLibDownloadSize + ), + createRow( + name = "asset", + value = assetDownloadSize + ), + createRow( + name = "other", + value = otherDownloadSize + ), + createRow( + name = "code", + value = dexDownloadFile + ) + ) + } +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/CodebaseAnalyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/CodebaseAnalyzer.kt new file mode 100644 index 0000000..2aeee68 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/CodebaseAnalyzer.kt @@ -0,0 +1,105 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.ApkComponentProcessor +import com.grab.sizer.analyzer.model.* +import com.grab.sizer.parser.BinaryFileInfo +import com.grab.sizer.parser.DataParser +import com.grab.sizer.parser.getAars +import com.grab.sizer.parser.getJars +import com.grab.sizer.report.Report +import com.grab.sizer.report.Row +import javax.inject.Inject + + +/** + * A specific implementation of the Analyzer interface with a focus on project codebase analysis. + * Assigned to handle [com.grab.sizer.AnalyticsOption.CODEBASE], this class provides a detailed report on the + * size contributions of individual team to the total app download size. + * + * @property dataParser Handles the parsing of APK, AAR, or JAR files. + * @property apkComponentProcessor Processes APK, AAR, or JAR files to produce a list of contributors. + * @property teamMapping Maps module to their corresponding team and vise versa + */ +internal class CodebaseAnalyzer @Inject constructor( + private val dataParser: DataParser, + private val apkComponentProcessor: ApkComponentProcessor, + private val teamMapping: TeamMapping, +) : Analyzer { + override fun process(): Report { + /** + * Process the whole project to get the app module information + */ + val wholeProject = apkComponentProcessor + .process( + dataParser.apks, + dataParser.getAars(), + dataParser.getJars() + ) + + val appContributor = Contributor( + originalOwner = createAppInfo(), + assets = wholeProject.noOwnerAssets.castToRawFile(), + resources = wholeProject.noOwnerResources.castToRawFile(), + nativeLibs = wholeProject.noOwnerNativeLibs.castToRawFile(), + classes = wholeProject.noOwnerClasses.castToClass(), + //others = wholeProject.noOwnerOthers.castToRawFile() + ) + + val modulesData = apkComponentProcessor + .process( + dataParser.apks, + dataParser.moduleAars, + dataParser.moduleJars + ) + return generateReport(modulesData.contributors + appContributor) + } + + private fun generateReport(contributors: Set): Report { + val teams: List = contributors.toTeams(teamMapping) + val sortedTeamsReport = teams.sort() + .map { it.toReportRow() } + return Report( + id = METRICS_ID_CODEBASE, + name = METRICS_ID_CODEBASE, + rows = sortedTeamsReport + ) + } + + private fun Team.toReportRow(): Row = createRow( + name, + getDownloadSize(), + ) +} + +internal fun createAppInfo() = object : BinaryFileInfo { + override val name: String = "app" + override val path: String = "root/app/build/" + override val tag: String = "app" +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LargeFileAnalyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LargeFileAnalyzer.kt new file mode 100644 index 0000000..c3d51f9 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LargeFileAnalyzer.kt @@ -0,0 +1,133 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.ApkComponentProcessor +import com.grab.sizer.analyzer.model.* +import com.grab.sizer.parser.DataParser +import com.grab.sizer.parser.getAars +import com.grab.sizer.parser.getJars +import com.grab.sizer.report.Report +import com.grab.sizer.report.Row +import javax.inject.Inject +import javax.inject.Named + +/** + * A specific implementation of the Analyzer interface with a focus on identifying large files in the project. + * This class handles [com.grab.sizer.AnalyticsOption.LARGE_FILE] and generates a report listing large files + * along with their corresponding modules and owners. (Haven't supported library) + * Files are considered 'large' if their download size exceeds a user-configurable threshold. + * + * @property apkComponentProcessor Responsible for processing APK, AAR, or JAR files to compile a list of contributors. + * @property dataParser Parse APK, AAR, or JAR files. + * @property teamMapping Handles the bi-directional mapping between modules and teams. + * @property largeFileThreshold threshold value for large file identification. + */ +internal class LargeFileAnalyzer @Inject constructor( + private val apkComponentProcessor: ApkComponentProcessor, + private val dataParser: DataParser, + private val teamMapping: TeamMapping, + @Named("largeFileThreshold") + private val largeFileThreshold: Long +) : Analyzer { + override fun process(): Report { + /** + * Process the whole project to get the app module information + */ + val wholeProject = + apkComponentProcessor.process( + dataParser.apks, + dataParser.getAars(), + dataParser.getJars() + ) + + val appModule = Contributor( + originalOwner = createAppInfo(), + assets = wholeProject.noOwnerAssets.castToRawFile(), + resources = wholeProject.noOwnerResources.castToRawFile(), + nativeLibs = wholeProject.noOwnerNativeLibs.castToRawFile(), + classes = wholeProject.noOwnerClasses.castToClass(), + ) + + val processedData = apkComponentProcessor.process( + dataParser.apks, + dataParser.moduleAars, + dataParser.moduleJars + ) + + return generateReport(processedData.contributors + appModule) + } + + private fun generateReport(contributors: Set): Report { + return contributors.filterLargeFileContributors() + .toTeams(teamMapping) + .run { + reportLargeFiles(this) + } + + } + + private fun Set.filterLargeFileContributors(): Set = map { + val resources = it.resources.filter { file -> file.downloadSize >= largeFileThreshold }.toSet() + val assets = it.assets.filter { file -> file.downloadSize >= largeFileThreshold }.toSet() + return@map it.copy(resources = resources, assets = assets) + }.filter { it.resources.isNotEmpty() || it.assets.isNotEmpty() } + .toSet() + + private fun reportLargeFiles(teams: List): Report { + val sortedTeamsReport = teams.sorByResources() + val reportRows = sortedTeamsReport.toReportRows() + return Report( + id = METRICS_ID_LARGE_FILES, + name = METRICS_ID_LARGE_FILES, + rows = reportRows, + ) + } + + private fun List.sorByResources(): List = this.sortedBy { + it.resourcesDownloadSize + it.assetsDownloadSize + } + + private fun List.toReportRows(): List = map { it to it.modules } + .flatMap { pair -> + pair.second.flatMap { module -> + module.contributors.flatMap { contributor -> contributor.resources + contributor.assets } + .map { res -> + val segmentPaths = res.path.split("/") + val fileName = segmentPaths.last() + createRow( + name = fileName, + value = res.downloadSize, + owner = pair.first.name, + tag = module.tag, + rowName = fileName + ) + } + } + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LibContentAnalyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LibContentAnalyzer.kt new file mode 100644 index 0000000..312fe75 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LibContentAnalyzer.kt @@ -0,0 +1,90 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.ApkComponentProcessor +import com.grab.sizer.analyzer.model.Contributor +import com.grab.sizer.analyzer.model.FileInfo +import com.grab.sizer.di.NAMED_LIB_NAME +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.DataParser +import com.grab.sizer.report.Report +import com.grab.sizer.report.Row +import java.io.File +import javax.inject.Inject +import javax.inject.Named + +/** + * A specific implementation of the Analyzer interface with a focus on analysis a library content. + * This class handles [com.grab.sizer.AnalyticsOption.LIB_CONTENT] and generates a detail report on the library content + * + * @property apkComponentProcessor Responsible for processing APK, AAR, or JAR files to compile a list of contributors. + * @property dataParser Parse APK, AAR, or JAR files. + */ +internal class LibContentAnalyzer @Inject constructor( + private val apkComponentProcessor: ApkComponentProcessor, + private val dataParser: DataParser, + @Named(NAMED_LIB_NAME) + private val libName: String? +) : Analyzer { + override fun process(): Report { + val processedData = apkComponentProcessor.process( + dataParser.apks, + dataParser.libAars, + dataParser.libJars + ) + return generateReport(processedData.contributors) + } + + private fun generateReport(contributors: Set): Report { + val library = contributors.find { File(it.originalOwner.path).nameWithoutExtension == libName } + ?: throw RuntimeException("Can not find the $libName") + val resourceRows = library.resources.toReportRows("Resource") + val assetRows = library.assets.toReportRows("Asset") + val nativeLibRows = library.nativeLibs.toReportRows("Native") + val otherRows = library.others.toReportRows("Other") + // todo : calculating the download sizer from the analytic process + val classRows = library.classes + .toReportRows("Class") + return Report( + id = LIB_CONTENT_METRICS_ID, + name = LIB_CONTENT_METRICS_ID, + rows = resourceRows + assetRows + nativeLibRows + otherRows + classRows, + ) + } + + private fun Collection.toReportRows(type: String): List = map { + createRow( + rowName = it.name, + name = it.name, + value = it.downloadSize, + tag = type + ) + } + +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LibrariesAnalyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LibrariesAnalyzer.kt new file mode 100644 index 0000000..e0a2260 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/LibrariesAnalyzer.kt @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.ApkComponentProcessor +import com.grab.sizer.analyzer.model.Contributor +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.DataParser +import com.grab.sizer.report.Report +import java.io.File +import javax.inject.Inject + + +/** + * An implementation of the Analyzer interface, focused on analyzing all libraries within the project. + * This class is designed to handle [com.grab.sizer.AnalyticsOption.LIBRARIES]. + * The resulting report lists all libraries in the project along with their respective contributions to the total app download size. + * + * @property apkComponentProcessor An instance for processing APK, AAR, or JAR files to produce a list of contributors. + * @property dataParser Parses APK, AAR, and JAR files for analysis. + */ +internal class LibrariesAnalyzer @Inject constructor( + private val apkComponentProcessor: ApkComponentProcessor, + private val dataParser: DataParser +) : Analyzer { + override fun process(): Report { + val processedData = apkComponentProcessor.process( + dataParser.apks, + dataParser.libAars, + dataParser.libJars + ) + return generateReport(processedData.contributors) + } + + private fun generateReport(contributors: Set): Report { + val contributorList = contributors.sortedBy { it.getDownloadSize() } + val listOfReport = reportPerLibrary(contributorList) + return Report( + id = LIBRARY_METRICS_ID, + name = LIBRARY_METRICS_ID, + rows = listOfReport.map { reportItem -> createRow(reportItem.name, reportItem.totalDownloadSize) }, + ) + } + + private fun Contributor.toReportItem(): ReportItem = ReportItem( + name = tag, + extraInfo = path.substring(path.indexOf("files-2.1/") + 9), + id = File(path).nameWithoutExtension, + totalDownloadSize = getDownloadSize(), + classesDownloadSize = classDownloadSize, + nativeLibDownloadSize = nativeLibDownloadSize, + resourceDownloadSize = resourcesDownloadSize, + assetDownloadSize = assetsDownloadSize, + otherDownloadSize = othersDownloadSize, + ) + + private fun reportPerLibrary(data: List): List = + data.map { it.toReportItem() } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/MetricIds.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/MetricIds.kt new file mode 100644 index 0000000..05a9ef8 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/MetricIds.kt @@ -0,0 +1,76 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.report.* + +internal const val LIBRARY_METRICS_ID = "library" +internal const val METRICS_ID_APK = "apk" +internal const val METRICS_ID_BASIC = "apk_basic" +internal const val METRICS_ID_CODEBASE = "team" +internal const val METRICS_ID_LARGE_FILES = "large_file" +internal const val LIB_CONTENT_METRICS_ID = "library_content" +internal const val METRICS_ID_MODULES = "module" +internal const val NOT_AVAILABLE_VALUE = "NA" + +internal fun createRow( + name: String, + value: Long, + owner: String? = null, + tag: String? = null, + rowName: String? = null +): Row = Row( + fields = mutableListOf( + TagField( + name = FIELD_KEY_CONTRIBUTOR, + value = name + ), + DefaultField( + name = FIELD_KEY_SIZE, + value = value + ) + ).apply { + if (owner != null) { + add( + TagField( + name = FIELD_KEY_OWNER, + value = owner + ) + ) + } + if (tag != null) { + add( + TagField( + name = FIELD_KEY_TAG, + value = tag + ) + ) + } + }, + name = rowName ?: name +) \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ModuleAnalyzer.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ModuleAnalyzer.kt new file mode 100644 index 0000000..f1b436c --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ModuleAnalyzer.kt @@ -0,0 +1,100 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.ApkComponentProcessor +import com.grab.sizer.analyzer.model.* +import com.grab.sizer.parser.DataParser +import com.grab.sizer.parser.getAars +import com.grab.sizer.parser.getJars +import com.grab.sizer.report.Report +import javax.inject.Inject + + +/** + * An implementation of the Analyzer interface, focused on analyzing all modules within the project. + * This class is designed to handle [com.grab.sizer.AnalyticsOption.MODULES]. + * The resulting report lists all module in the project along with their respective contributions to the total app download size. + * The list of modules could be grouped by the owner. + * + * @property apkComponentProcessor An instance for processing APK, AAR, or JAR files to produce a list of contributors. + * @property dataParser Parses APK, AAR, and JAR files for analysis. + * @property teamMapping Maps module to their corresponding team and vise versa + */ +internal class ModuleAnalyzer @Inject constructor( + private val apkComponentProcessor: ApkComponentProcessor, + private val dataParser: DataParser, + private val teamMapping: TeamMapping, +) : Analyzer { + override fun process(): Report { + /** + * Process the whole project to get the app module information + */ + val wholeProject = apkComponentProcessor.process( + dataParser.apks, + dataParser.getAars(), + dataParser.getJars() + ) + val appModule = Contributor( + originalOwner = createAppInfo(), + assets = wholeProject.noOwnerAssets.castToRawFile(), + resources = wholeProject.noOwnerResources.castToRawFile(), + nativeLibs = wholeProject.noOwnerNativeLibs.castToRawFile(), + classes = wholeProject.noOwnerClasses.castToClass(), + others = wholeProject.noOwnerOthers.castToRawFile() + ) + + val processedData = apkComponentProcessor.process( + dataParser.apks, + dataParser.moduleAars, + dataParser.moduleJars + ) + return generateReport(processedData.contributors + appModule) + } + + private fun generateReport(contributors: Set): Report = contributors.toModules() + .run { + val sortedTeamsReport = sortedBy { it.getDownloadSize() } + .map { it.toReportItem(teamMapping.moduleToTeamMap) } + return Report( + id = METRICS_ID_MODULES, + name = METRICS_ID_MODULES, + rows = toReportRows(sortedTeamsReport), + ) + } + + private fun toReportRows(reportItems: List) = + reportItems.map { reportItem -> + createRow( + name = reportItem.id, + value = reportItem.totalDownloadSize, + owner = reportItem.owner ?: NOT_AVAILABLE_VALUE, + rowName = reportItem.name + ) + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ReportItem.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ReportItem.kt new file mode 100644 index 0000000..c166b25 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/ReportItem.kt @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + + +internal data class ReportItem( + val id: String, + val totalDownloadSize: Long, + val name: String = "", + val extraInfo: String = "", + val owner: String? = null, + val classesDownloadSize: Long = 0L, + val nativeLibDownloadSize: Long = 0L, + val resourceDownloadSize: Long = 0L, + val assetDownloadSize: Long = 0L, + val otherDownloadSize: Long = 0L, +) \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/TeamMapping.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/TeamMapping.kt new file mode 100644 index 0000000..cc73f69 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/TeamMapping.kt @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer +import java.io.File + + +/** + * An interface that represents a bi-directional mapping between modules and teams. + * It allows the retrieval of the associated team for a given module and vice versa. + */ +interface TeamMapping { + val teamToModuleMap: Map> + val moduleToTeamMap: Map +} + +class DummyTeamMapping : TeamMapping { + override val teamToModuleMap: Map> = emptyMap() + override val moduleToTeamMap: Map = emptyMap() +} + + +class YmlTeamMapping( + private val ymlFile: File +) : TeamMapping { + override val teamToModuleMap: Map> by lazy { + loadTeamToModuleMap() + .mapValues { entry -> + entry.value.map { it.trim(':') } + } + } + override val moduleToTeamMap: Map by lazy { + mutableMapOf().apply { + teamToModuleMap.forEach { (team, modules) -> + modules.forEach { put(it, team) } + } + } + } + + private fun loadTeamToModuleMap(): Map> { + val lines = ymlFile.readLines() + val result = mutableMapOf>() + var currentTeam: String? = null + + for (line in lines) { + val trimmedLine = line.trim() + when { + trimmedLine.isEmpty() || trimmedLine.startsWith("#") -> continue // Skip empty lines and comments + trimmedLine.endsWith(":") -> { + currentTeam = trimmedLine.dropLast(1) + result[currentTeam] = mutableListOf() + } + trimmedLine.startsWith("- ") -> { + currentTeam?.let { + result[it]?.add(trimmedLine.removePrefix("- ").trim()) + } + } + } + } + + return result + } + +} diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ApkComponentProcessor.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ApkComponentProcessor.kt new file mode 100644 index 0000000..7180078 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ApkComponentProcessor.kt @@ -0,0 +1,165 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.model.Contributor +import com.grab.sizer.analyzer.model.FileInfo +import com.grab.sizer.analyzer.model.castToClass +import com.grab.sizer.analyzer.model.castToRawFile +import com.grab.sizer.di.AnalyzerClass +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.BinaryFileInfo +import com.grab.sizer.parser.JarFileInfo +import javax.inject.Inject + + +internal interface ApkComponentProcessor { + /** + * Processes a set of apk, aar and jar files. This method maps contributors (aar & jar files) to the apk files. + * Analyses the given files and returns a ComponentProcessorResult containing a set of Contributor objects. + * Each Contributor object represents an aar or jar file and the components it contributes to an apk files. + * + * @param apks the set of APK files to process + * @param aars the set of AAR files to process + * @param jars the set of JAR files to process + * @return a ComponentProcessorResult containing a list of Contributors and non-owned component files + */ + fun process(apks: Set, aars: Set, jars: Set): ComponentProcessorResult +} + +/** + * Holds the result of processing apk, aar, and jar files in ApkComponentProcessor. + * Contains sets of Contributors and non-owned component files. + * + * @property contributors a set of Contributor objects each representing an aar or jar file and its contribution to an apk file. + * @property noOwnerAssets a set of FileInfo objects representing assets not owned by any contributor. + * @property noOwnerResources a set of FileInfo objects representing resources not owned by any contributor. + * @property noOwnerNativeLibs a set of FileInfo objects representing native libs not owned by any contributor. + * @property noOwnerClasses a set of FileInfo objects representing classes not owned by any contributor. + * @property noOwnerOthers a set of FileInfo objects representing other components not owned by any contributor. + */ +internal data class ComponentProcessorResult( + val contributors: Set, + val noOwnerAssets: Set, + val noOwnerResources: Set, + val noOwnerNativeLibs: Set, + val noOwnerClasses: Set, + val noOwnerOthers: Set, +) + +internal class DefaultApkComponentProcessor @Inject constructor(private val mappers: Map) : + ApkComponentProcessor { + + override fun process( + apks: Set, + aars: Set, + jars: Set + ): ComponentProcessorResult { + val rawContributorMap = mappers.mapValues { + with(it.value) { + apks.mapTo(aars, jars) + } + } + val contributors = mutableMapOf().apply { + createAssetContributors(rawContributorMap) + createResourceContributors(rawContributorMap) + createNativeLibsContributors(rawContributorMap) + createOtherContributors(rawContributorMap) + createClassContributors(rawContributorMap) + }.values.toSet() + return ComponentProcessorResult( + contributors = contributors, + noOwnerAssets = rawContributorMap.getNoOwnerData(AssetComponentMapper::class.java), + noOwnerResources = rawContributorMap.getNoOwnerData(ResourceComponentMapper::class.java), + noOwnerNativeLibs = rawContributorMap.getNoOwnerData(NativeLibComponentMapper::class.java), + noOwnerClasses = rawContributorMap.getNoOwnerData(ClassComponentMapper::class.java), + noOwnerOthers = rawContributorMap.getNoOwnerData(OtherComponentMapper::class.java), + ) + } + + private fun Map.getNoOwnerData(clazz: Class<*>): Set = + get(clazz)?.noOwnerData ?: emptySet() + + private fun MutableMap.createAssetContributors(rawContributorMap: Map) { + rawContributorMap[AssetComponentMapper::class.java]?.contributors?.forEach { rawEntry -> + val lib = rawEntry.key + val assets = rawEntry.value + var contributor = get(lib) + contributor = contributor?.copy(assets = assets.castToRawFile()) + ?: Contributor(originalOwner = lib, assets = assets.castToRawFile()) + put(lib, contributor) + } + } + + private fun MutableMap.createResourceContributors(rawContributorMap: Map) { + rawContributorMap[ResourceComponentMapper::class.java]?.contributors?.forEach { rawEntry -> + val lib = rawEntry.key + val data = rawEntry.value + var contributor = get(lib) + contributor = contributor?.copy(resources = data.castToRawFile()) + ?: Contributor(originalOwner = lib, resources = data.castToRawFile()) + put(lib, contributor) + } + } + + private fun MutableMap.createNativeLibsContributors(rawContributorMap: Map) { + rawContributorMap[NativeLibComponentMapper::class.java]?.contributors?.forEach { rawEntry -> + val lib = rawEntry.key + val data = rawEntry.value + var contributor = get(lib) + contributor = contributor?.copy(nativeLibs = data.castToRawFile()) + ?: Contributor(originalOwner = lib, nativeLibs = data.castToRawFile()) + put(lib, contributor) + } + } + + private fun MutableMap.createOtherContributors(rawContributorMap: Map) { + rawContributorMap[OtherComponentMapper::class.java]?.contributors?.forEach { rawEntry -> + val lib = rawEntry.key + val data = rawEntry.value + var contributor = get(lib) + contributor = contributor?.copy(others = data.castToRawFile()) + ?: Contributor(originalOwner = lib, others = data.castToRawFile()) + put(lib, contributor) + } + } + + private fun MutableMap.createClassContributors(rawContributorMap: Map) { + rawContributorMap[ClassComponentMapper::class.java]?.contributors?.forEach { rawEntry -> + val lib = rawEntry.key + val data = rawEntry.value + var contributor = get(lib) + contributor = contributor?.copy(classes = data.castToClass()) + ?: Contributor(originalOwner = lib, classes = data.castToClass()) + put(lib, contributor) + } + } +} + + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/AssetComponentMapper.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/AssetComponentMapper.kt new file mode 100644 index 0000000..f5edf9b --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/AssetComponentMapper.kt @@ -0,0 +1,69 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + + +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.BinaryFileInfo +import com.grab.sizer.parser.JarFileInfo +import javax.inject.Inject + +/** + * Analyzes, maps and creates a ComponentMapperResult focusing on assets. + */ +internal class AssetComponentMapper @Inject constructor() : ComponentMapper { + override fun Set.mapTo(aars: Set, jars: Set): ComponentMapperResult { + val apkAssets = flatMap { apk -> apk.assets } + + val aarsAssetMap = mutableMapOf().apply { + aars.forEach { aar -> + aar.assets.forEach { file -> + put(file, aar) + } + } + } + val noOwnerAssets = mutableSetOf() + val contributors = mutableMapOf>().apply { + apkAssets.forEach { asset -> + val aar = aarsAssetMap[asset] + if (aar != null) { + putIfAbsent(aar, mutableSetOf()) + get(aar)?.add(asset) + } else { + noOwnerAssets.add(asset) + } + } + } + return ComponentMapperResult( + contributors = contributors, + noOwnerData = noOwnerAssets + ) + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ClassComponentMapper.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ClassComponentMapper.kt new file mode 100644 index 0000000..d2f18e9 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ClassComponentMapper.kt @@ -0,0 +1,104 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + + +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.BinaryFileInfo +import com.grab.sizer.parser.JarFileInfo +import javax.inject.Inject + +private const val AUTO_GENERATION_LAMBDA = "-\$\$Lambda\$" +private const val AUTO_GENERATION_LAMBDA2 = "$" + +/** + * Analyzes, maps and creates a ComponentMapperResult focusing on classes. + */ +internal class ClassComponentMapper @Inject constructor() : ComponentMapper { + override fun Set.mapTo( + aars: Set, + jars: Set + ): ComponentMapperResult { + val apkClasses = flatMap { apk -> apk.dexes }.flatMap { dex -> dex.classes } + val libClassMap = mutableMapOf().apply { + aars.forEach { aar -> + aar.jars.forEach { jar -> + jar.classes.forEach { clazz -> + put(clazz, aar) + } + } + } + jars.forEach { jar -> + jar.classes.forEach { clazz -> + put(clazz, jar) + } + } + } + val noOwnerClasses = mutableSetOf() + val contributors = mutableMapOf>().apply { + apkClasses.forEach { clazz -> + val lib = libClassMap[clazz] ?: libClassMap[clazz.tryOriginalClass()] + if (lib != null) { + putIfAbsent(lib, mutableSetOf()) + get(lib)?.add(clazz) + } else { + noOwnerClasses.add(clazz) + } + } + } + return ComponentMapperResult( + contributors = contributors, + noOwnerData = noOwnerClasses + ) + } + + private fun ClassFileInfo.tryOriginalClass(): ClassFileInfo { + /** + * Auto generated lambda. + * Ex: androidx.core.widget.-$$Lambda$ContentLoadingProgressBar$aW9csiS0dCdsR2nrqov9CuXAmGo + */ + if (name.contains(AUTO_GENERATION_LAMBDA)) { + var newName = name.replace(AUTO_GENERATION_LAMBDA, "") + if (newName.lastIndexOf("$") > 0) + newName = newName.removeRange(newName.lastIndexOf("$"), newName.length) + return copy(name = newName) + } + + /** + * Handle these cases + * androidx.appcompat.app.AppCompatDelegate$$ExternalSyntheticLambda0 + * androidx.appcompat.app.AppCompatDelegateImpl$Api24Impl$$ExternalSyntheticApiModelOutline0 + */ + if(name.contains(AUTO_GENERATION_LAMBDA2)){ + return copy(name = name.substring(0, name.indexOf("$"))) + } + return this + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ComponentMapper.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ComponentMapper.kt new file mode 100644 index 0000000..c9c246b --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ComponentMapper.kt @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.model.FileInfo +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.BinaryFileInfo +import com.grab.sizer.parser.JarFileInfo + +/** + * Type alias for a map containing input files (aar/jar) and the set of files associated with each input. + */ +internal typealias RawContributors = Map> + + +/** + * Holds the results of a component mapping process. + * Contains a set of non-owned component files and raw contributors with their associated FileInfo. + * + * @property noOwnerData Set of FileInfo representing files not owned by any contributor. + * @property contributors Map of raw contributors and their associated FileInfo. + */ +internal data class ComponentMapperResult( + val noOwnerData: Set, + val contributors: RawContributors +) + +/** + * Interface for component-specific mappers that target specific file types such as resources, assets, or native libraries, etc. + * The implementation maps files from APKs to aar & jar files and outputs a ComponentMapperResult. + */ +internal interface ComponentMapper { + /** + * Maps files from the provided APKs to AARs and JARs + * Outputs a ComponentMapperResult with mapped contributors and files that can not find an owner. + * + * @param aars The set of AAR files to analyze. + * @param jars The set of JAR files to analyze. + * @return a ComponentMapperResult which contains a map of aar/jar file to its own set of FileInfo. + */ + fun Set.mapTo(aars: Set, jars: Set): ComponentMapperResult +} + + + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/NativeLibComponentMapper.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/NativeLibComponentMapper.kt new file mode 100644 index 0000000..1dec3fa --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/NativeLibComponentMapper.kt @@ -0,0 +1,92 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.BinaryFileInfo +import com.grab.sizer.parser.JarFileInfo +import java.io.File +import javax.inject.Inject + +/** + * Analyzes, maps and creates a ComponentMapperResult focusing on native libraries. + */ +internal class NativeLibComponentMapper @Inject constructor() : ComponentMapper { + override fun Set.mapTo( + aars: Set, + jars: Set + ): ComponentMapperResult { + val apkLibs = flatMap { apk -> apk.nativeLibs } + + val libraryMap = mutableMapOf().apply { + aars.forEach { aar -> + aar.nativeLibs.forEach { file -> + put(file.trimPath(), aar) + } + } + jars.forEach { jar -> + jar.nativeLibs.forEach { file -> + put(file.trimPath(), jar) + } + } + } + val noOwnerNativeLib = mutableSetOf() + val contributors = mutableMapOf>().apply { + apkLibs.forEach { nativeLib -> + val lib = libraryMap[nativeLib.trimPath()] + if (lib != null) { + putIfAbsent(lib, mutableSetOf()) + get(lib)?.add(nativeLib) + } else { + noOwnerNativeLib.add(nativeLib) + } + } + } + return ComponentMapperResult( + contributors = contributors, + noOwnerData = noOwnerNativeLib + ) + } + + /** + * There are different between APK and AAR native file path. + * This method will remove the pre-fix path for the so file, to ensure the mapping working as expected + * Example, + * APK: /lib/armeabi-v7a/sample.so -> armeabi-v7a/sample.so + * AAR: /jni/armeabi-v7a/sample.so -> armeabi-v7a/sample.so + */ + private fun RawFileInfo.trimPath(): RawFileInfo { + val file = File(path) + val parent = File(path).parentFile.name + return copy( + path = "/$parent/${file.name}" + ) + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/OtherComponentMapper.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/OtherComponentMapper.kt new file mode 100644 index 0000000..e271ee8 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/OtherComponentMapper.kt @@ -0,0 +1,44 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + + +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.JarFileInfo +import javax.inject.Inject + +internal class OtherComponentMapper @Inject constructor() : ComponentMapper { + override fun Set.mapTo(aars: Set, jars: Set): ComponentMapperResult { + // Todo: Add logic to map others + return ComponentMapperResult( + contributors = emptyMap(), + noOwnerData = flatMap { it.others }.toSet() + ) + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ResourceComponentMapper.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ResourceComponentMapper.kt new file mode 100644 index 0000000..b0569b7 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/mapper/ResourceComponentMapper.kt @@ -0,0 +1,104 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.BinaryFileInfo +import com.grab.sizer.parser.JarFileInfo +import java.io.File +import javax.inject.Inject + +private const val RESOURCE_VERSION_EXTENSION = "-v\\d\\d" + +/** + * Analyzes, maps and creates a ComponentMapperResult focusing on resource. + */ +internal class ResourceComponentMapper @Inject constructor() : ComponentMapper { + override fun Set.mapTo( + aars: Set, + jars: Set + ): ComponentMapperResult { + val apkResource = flatMap { it.resources } + val aarsToResMap = mutableMapOf().apply { + aars.forEach { aar -> + aar.resources.forEach { file -> put(file, aar) } + } + } + val noOwnerResources = mutableSetOf() + val contributors = mutableMapOf>().apply { + apkResource.forEach { resource -> + val aarName = aarsToResMap[resource] + ?: aarsToResMap[resource.tryWithRemoveSpecialChar()] + ?: aarsToResMap[resource.tryWithRemoveVersionExtension()] + ?: aarsToResMap[resource.tryWithRemoveSpecialChar().tryWithRemoveVersionExtension()] + if (aarName != null) { + putIfAbsent(aarName, mutableSetOf()) + get(aarName)?.add(resource) + } else { + noOwnerResources.add(resource) + } + } + } + return ComponentMapperResult( + contributors = contributors, + noOwnerData = noOwnerResources + ) + } + + private fun RawFileInfo.tryWithRemoveSpecialChar(): RawFileInfo { + if (path.contains("$")) { + /** + * There are cases the resources files are renamed, not sure why and how. + * Here is an example: "/res/drawable/$bg_network_error__0.xml" + */ + val newPath = path.replace("$", "") + return copy(path = newPath.removeRange(newPath.lastIndexOf("__"), newPath.lastIndexOf('.'))) + } + return this + } + + private fun RawFileInfo.tryWithRemoveVersionExtension(): RawFileInfo { + if (path.contains(Regex(RESOURCE_VERSION_EXTENSION))) { + /** + * There are cases the resource directory were added with the min support sdk version + * Ex : /res/drawable-v22/ic_geo_pickup_notes.xml + */ + val file = File(path) + val dir = file.parentFile + if (dir.name.contains(Regex(RESOURCE_VERSION_EXTENSION))) { + val dirName = dir.name + val newDirName = dirName.removeRange(dirName.lastIndexOf('-'), dirName.length) + val newDir = File(dir.parentFile, newDirName) + return copy(path = File(newDir, file.name).path) + } + } + return this + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/ClassFileInfo.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/ClassFileInfo.kt new file mode 100644 index 0000000..c9a7bed --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/ClassFileInfo.kt @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.model + + +/** + * Represents a class file in the aar/jar/apk file + * + * @property name The name of the class. + * @property size The original, uncompressed size of the class file. + * @property downloadSize The size of the class file in the binary that is downloadable from Google Play. + */ +data class ClassFileInfo( + override val name: String, + override val size: Long, + override val downloadSize: Long = 0, +) : FileInfo { + override fun equals(other: Any?): Boolean { + if (other is ClassFileInfo) return name == other.name + return super.equals(other) + } + + override fun hashCode(): Int = name.hashCode() +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/Contributor.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/Contributor.kt new file mode 100644 index 0000000..db052d9 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/Contributor.kt @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.model + +import com.grab.sizer.parser.BinaryFileInfo + +/** + * Represents a aar or a jar file, and their components (assets, resources, native libraries, classes, and others). + * These component are files and classes, each component should provide the sizes it contributes to the apk. + * + * @property originalOwner aar/jar file + * @property assets a set of assets files + * @property resources a set of resources files + * @property nativeLibs a set of native libraries files (*.so files) + * @property classes a set of classes + * @property others a set of other files not categorized as assets, resources, native libraries or classes. + */ +data class Contributor( + val originalOwner: BinaryFileInfo, + val assets: Set = emptySet(), + val resources: Set = emptySet(), + val nativeLibs: Set = emptySet(), + val classes: Set = emptySet(), + val others: Set = emptySet(), +) { + val tag : String + get() = originalOwner.tag + val path: String + get() = originalOwner.path + // Calculates the sum of the download sizes of all resources + val resourcesDownloadSize: Long by lazy { resources.sumOf { resource -> resource.downloadSize } } + + // Calculates the sum of the download sizes of all native libraries + val nativeLibDownloadSize: Long by lazy { nativeLibs.sumOf { lib -> lib.downloadSize } } + + // Calculates the sum of the download sizes of all assets + val assetsDownloadSize: Long by lazy { assets.sumOf { asset -> asset.downloadSize } } + + // Calculates the sum of the download sizes of all "other" files + val othersDownloadSize: Long by lazy { others.sumOf { other -> other.downloadSize } } + + // Calculates the sum of the sizes of all classes + val classDownloadSize: Long by lazy { classes.sumOf { clazz -> clazz.downloadSize } } + + // Calculates the total downloadable size of all component types (assets, resources, native libraries, classes, others). + fun getDownloadSize(): Long = + resourcesDownloadSize + nativeLibDownloadSize + assetsDownloadSize + othersDownloadSize + classDownloadSize + + override fun equals(other: Any?): Boolean { + if (other is Contributor) { + return path == other.path + } + + return super.equals(other) + } + + override fun hashCode(): Int = path.hashCode() +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/RawFileInfo.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/RawFileInfo.kt new file mode 100644 index 0000000..ef95b7f --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/RawFileInfo.kt @@ -0,0 +1,87 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.model + +import java.io.File + + +/** + * Specifies the type of file extracted from a jar or aar file. + */ +enum class FileType { + RESOURCE, NATIVE_LIB, ASSET, DEX, JAR, OTHERS, CLASS +} + +/** + * Represents a file/class within a jar, aar or apk file and provides its various sizes. + * + * @property name The name of the file/class. + * @property downloadSize The size of the file in the binary that is downloadable from Google Play.(zipped APKs) + * @property size The original size of the file. + */ +interface FileInfo { + val name: String + val downloadSize: Long + val size: Long +} + +/** + * Defines a raw file which is not a class in a jar, aar or apk file + * + * @property path The path to the raw file within the aar/jar file. + * @property downloadSize The size of the file in the downloadable binary from Google Play. + * @property size The original, uncompressed size of the file. + */ +data class RawFileInfo( + val path: String, + override val downloadSize: Long, + override val size: Long +) : FileInfo { + val type: FileType + get() = when { + path.startsWith("/res/") -> FileType.RESOURCE + path.endsWith(".so", true) -> FileType.NATIVE_LIB + path.startsWith("/assets/") -> FileType.ASSET + path.endsWith(".dex") -> FileType.DEX + path.endsWith(".jar") -> FileType.JAR + path.endsWith(".class") -> FileType.CLASS + else -> FileType.OTHERS + } + override val name: String + get() = File(path).name + + override fun equals(other: Any?): Boolean { + if (other is RawFileInfo) return path == other.path + return super.equals(other) + } + + override fun hashCode(): Int = path.hashCode() +} + +internal fun Set.castToClass(): Set = filterIsInstance().toSet() +internal fun Set.castToRawFile(): Set = filterIsInstance().toSet() \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/Team.kt b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/Team.kt new file mode 100644 index 0000000..68ca52a --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/analyzer/model/Team.kt @@ -0,0 +1,107 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.model + +import com.grab.sizer.analyzer.ReportItem +import com.grab.sizer.analyzer.TeamMapping +import com.grab.sizer.parser.BinaryFileInfo + +data class Team( + val name: String, + val modules: List +) { + val resourcesDownloadSize: Long by lazy { modules.sumOf { contributor -> contributor.resourcesDownloadSize } } + val nativeLibDownloadSize: Long by lazy { modules.sumOf { contributor -> contributor.nativeLibDownloadSize } } + val assetsDownloadSize: Long by lazy { modules.sumOf { contributor -> contributor.assetsDownloadSize } } + val othersDownloadSize: Long by lazy { modules.sumOf { contributor -> contributor.othersDownloadSize } } + val classDownloadSize: Long by lazy { modules.sumOf { contributor -> contributor.classDownloadSize } } + fun getDownloadSize(): Long = modules.sumOf { it.getDownloadSize() } +} + +data class Module( + private val owner: BinaryFileInfo, + val contributors: List +) { + val tag: String + get() = owner.tag + val path: String + get() = owner.path + val resourcesDownloadSize: Long by lazy { contributors.sumOf { contributor -> contributor.resourcesDownloadSize } } + val nativeLibDownloadSize: Long by lazy { contributors.sumOf { contributor -> contributor.nativeLibDownloadSize } } + val assetsDownloadSize: Long by lazy { contributors.sumOf { contributor -> contributor.assetsDownloadSize } } + val othersDownloadSize: Long by lazy { contributors.sumOf { contributor -> contributor.othersDownloadSize } } + val classDownloadSize: Long by lazy { contributors.sumOf { contributor -> contributor.classDownloadSize } } + + fun getDownloadSize(): Long = + resourcesDownloadSize + nativeLibDownloadSize + assetsDownloadSize + othersDownloadSize + classDownloadSize +} + +internal fun Set.toTeams(teamMapping: TeamMapping): List { + val modules = toModules() + return teamMapping.teamToModuleMap.mapValues { teamToModule -> + teamToModule.value.mapNotNull { moduleNameFromTeamMapping -> + modules.find { module -> module.tag == moduleNameFromTeamMapping } + } + }.map { Team(it.key, it.value) } +} + +internal fun List.sort(): List { + return sortedWith { o1, o2 -> + val size1 = o1.getDownloadSize() + val size2 = o2.getDownloadSize() + if (size1 > size2) -1 + else if (size1 < size2) 1 + else 0 + } +} + +internal fun Set.toModules(): List { + return asSequence() + .map { contributor -> contributor.originalOwner to contributor } + .groupBy { it.first } + .mapValues { item -> + item.value.map { it.second } + }.map { + Module(it.key, it.value) + } +} + +internal fun Module.toReportItem(moduleToTeamMap: Map): ReportItem = + ReportItem( + name = tag, + id = tag, + owner = moduleToTeamMap[tag], + extraInfo = "Sum up all codebase for $tag", + totalDownloadSize = getDownloadSize(), + classesDownloadSize = classDownloadSize, + nativeLibDownloadSize = nativeLibDownloadSize, + resourceDownloadSize = resourcesDownloadSize, + assetDownloadSize = assetsDownloadSize, + otherDownloadSize = othersDownloadSize + ) + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyticsOptionKey.kt b/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyticsOptionKey.kt new file mode 100644 index 0000000..57997fd --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyticsOptionKey.kt @@ -0,0 +1,44 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.di + +import com.grab.sizer.AnalyticsOption +import dagger.MapKey + +/** A [MapKey] annotation for maps with [AnalyticsOption] keys. */ + +@MustBeDocumented +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.FIELD +) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class AnalyticsOptionKey(val value: AnalyticsOption) \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyzerComponent.kt b/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyzerComponent.kt new file mode 100644 index 0000000..c2668e0 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyzerComponent.kt @@ -0,0 +1,69 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.di + +import com.grab.sizer.AnalyticsOption +import com.grab.sizer.analyzer.Analyzer +import com.grab.sizer.report.ReportModule +import com.grab.sizer.report.ReportModuleBinder +import com.grab.sizer.report.ReportWriter +import com.grab.sizer.utils.InputProvider +import com.grab.sizer.utils.Logger +import com.grab.sizer.utils.OutputProvider +import dagger.BindsInstance +import dagger.Component +import javax.inject.Named + +internal const val NAMED_LIB_NAME = "lib_name" + +@Component( + modules = [ + AnalyzerModule::class, + ComponentMapperModule::class, + AnalyzerBinder::class, + ParserBinder::class, + ReportModule::class, + ReportModuleBinder::class + ] +) +@AppScope +interface AnalyzerComponent { + fun analyzerMap(): Map + fun reportWriters(): Set<@JvmSuppressWildcards ReportWriter> + + @Component.Factory + interface Factory { + fun create( + @BindsInstance inputProvider: InputProvider, + @BindsInstance outputProvider: OutputProvider, + @BindsInstance @Named(NAMED_LIB_NAME) libName: String?, + @BindsInstance logger: Logger, + ): AnalyzerComponent + } +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyzerModule.kt b/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyzerModule.kt new file mode 100644 index 0000000..6a2b025 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/di/AnalyzerModule.kt @@ -0,0 +1,113 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.di + +import com.android.tools.apk.analyzer.ApkSizeCalculator +import com.google.gson.Gson +import com.grab.sizer.AnalyticsOption +import com.grab.sizer.analyzer.* +import com.grab.sizer.parser.DataParser +import com.grab.sizer.parser.DefaultDataParser +import com.grab.sizer.utils.InputProvider +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import javax.inject.Named +import javax.inject.Scope + +@Scope +@Retention +annotation class AppScope + +@Module +object AnalyzerModule { + @Provides + @AppScope + fun provideApkSizeCalculator(): ApkSizeCalculator = ApkSizeCalculator.getDefault() + + @Provides + @AppScope + fun provideGson() = Gson() + + @Provides + fun provideTeamMapping( + inputProvider: InputProvider + ): TeamMapping { + // Todo : Remove this logic from dagger module, possible remove DummyTeamMapping + val ownerMapping = inputProvider.provideTeamMappingFile() + return if (ownerMapping == null) DummyTeamMapping() + else YmlTeamMapping(ownerMapping) + } + + @Provides + @Named("largeFileThreshold") + fun provideLargeFileThreshold(inputProvider: InputProvider): Long = inputProvider.provideLargeFileThreshold() +} + + +@Module +internal interface AnalyzerBinder { + @Binds + fun bindDataParser(parser: DefaultDataParser): DataParser + + @Binds + @IntoMap + @AnalyticsOptionKey(AnalyticsOption.CODEBASE) + fun bindGeneralAnalyzer(analyzer: CodebaseAnalyzer): Analyzer + + @Binds + @IntoMap + @AnalyticsOptionKey(AnalyticsOption.LIBRARIES) + fun bindLibrariesAnalyzer(analyzer: LibrariesAnalyzer): Analyzer + + @Binds + @IntoMap + @AnalyticsOptionKey(AnalyticsOption.LIB_CONTENT) + fun bindAnalyzer(analyzer: LibContentAnalyzer): Analyzer + + @Binds + @IntoMap + @AnalyticsOptionKey(AnalyticsOption.BASIC) + fun bindBasicApkAnalyzer(analyzer: BasicApkAnalyzer): Analyzer + + @Binds + @IntoMap + @AnalyticsOptionKey(AnalyticsOption.MODULES) + fun bindModuleAnalyzer(analyzer: ModuleAnalyzer): Analyzer + + @Binds + @IntoMap + @AnalyticsOptionKey(AnalyticsOption.APK) + fun bindApkAnalyzer(analyzer: ApkAnalyzer): Analyzer + + @Binds + @IntoMap + @AnalyticsOptionKey(AnalyticsOption.LARGE_FILE) + fun bindLargeFileAnalyzer(analyser: LargeFileAnalyzer): Analyzer +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/di/ComponentMapperModule.kt b/app-sizer/src/main/kotlin/com/grab/sizer/di/ComponentMapperModule.kt new file mode 100644 index 0000000..2f7c8e3 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/di/ComponentMapperModule.kt @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.di + +import com.grab.sizer.analyzer.mapper.* +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + + +typealias AnalyzerClass = Class<*> + +@Module +internal interface ComponentMapperModule { + @Binds + @IntoMap + @ClassKey(ResourceComponentMapper::class) + fun bindResourceAnalyzer(mapper: ResourceComponentMapper): ComponentMapper + + @Binds + @IntoMap + @ClassKey(NativeLibComponentMapper::class) + fun bindNativeLibAnalyzer(mapper: NativeLibComponentMapper): ComponentMapper + + @Binds + @IntoMap + @ClassKey(AssetComponentMapper::class) + fun bindAssetsAnalyzer(mapper: AssetComponentMapper): ComponentMapper + + @Binds + @IntoMap + @ClassKey(ClassComponentMapper::class) + fun bindClassesAnalyzer(mapper: ClassComponentMapper): ComponentMapper + + @Binds + @IntoMap + @ClassKey(OtherComponentMapper::class) + fun bindOtherAnalyzer(other: OtherComponentMapper): ComponentMapper + + @Binds + fun bindApkComponentProcessor(processor: DefaultApkComponentProcessor): ApkComponentProcessor +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/di/ParserBinder.kt b/app-sizer/src/main/kotlin/com/grab/sizer/di/ParserBinder.kt new file mode 100644 index 0000000..1a52a74 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/di/ParserBinder.kt @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.di + +import com.grab.sizer.parser.* +import dagger.Binds +import dagger.Module + +@Module +internal interface ParserBinder { + @Binds + fun bindDexFileParser(parser: DefaultDexFileParser): DexFileParser + + @Binds + fun bindApkParser(parser: DefaultApkFileParser): ApkFileParser + + @Binds + fun bindJarStreamParser(parser: DefaultJarStreamParser): JarStreamParser + + @Binds + fun bindJarFileParser(parser: DefaultJarFileParser): JarFileParser + + @Binds + fun bindAarFileParser(parser: DefaultAarFileParser): AarFileParser + + @Binds + fun bindProguardFileParser(parser: DefaultProguardFileParser): ProguardFileParser +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/AarFileInfo.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/AarFileInfo.kt new file mode 100644 index 0000000..846efe7 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/AarFileInfo.kt @@ -0,0 +1,55 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.analyzer.model.RawFileInfo + +interface BinaryFileInfo { + val name: String + val path: String + val tag: String +} + +data class AarFileInfo( + override val name: String, + override val path: String, + override val tag: String, + val resources: Set, + val nativeLibs: Set, + val assets: Set, + val others: Set, + val jars: Set +) : BinaryFileInfo { + override fun toString(): String = path + override fun hashCode(): Int = path.hashCode() + override fun equals(other: Any?): Boolean { + if (other !is AarFileInfo) return false + return path == other.path + } +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/AarFileParser.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/AarFileParser.kt new file mode 100644 index 0000000..2a01c7f --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/AarFileParser.kt @@ -0,0 +1,98 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.analyzer.model.FileType +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.di.AppScope +import com.grab.sizer.utils.SizerInputFile +import java.util.zip.ZipFile +import javax.inject.Inject + +/** + * The AarFileParser interface provides the method to parse a sequence of AAR files into a set of [AarFileInfo]. + * Note: For native libraries located in the "jni" folder, their paths will be converted with "jni" replaced by "lib". + * This adjustment ensures the path inside the AAR file matches the path in the APK file. + */ +interface AarFileParser { + fun parseAars(files: Sequence): Set +} + +/** + * Default implementation of [AarFileParser]. + * For more about the AAR file format, see: http://tools.android.com/tech-docs/new-build-system/aar-format + */ +@AppScope +class DefaultAarFileParser @Inject constructor(private val jarParser: JarStreamParser) : AarFileParser { + + private fun parse(sizerInputFile: SizerInputFile): AarFileInfo { + ZipFile(sizerInputFile.file).use { zipFile -> + val entries = zipFile.entries() + val resources = mutableSetOf() + val assets = mutableSetOf() + val nativeLibs = mutableSetOf() + val others = mutableSetOf() + val jars = mutableSetOf() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + val fileInfo = RawFileInfo( + path = entry.getPath(), + size = entry.size, + downloadSize = -1, + ) + + when (fileInfo.type) { + FileType.RESOURCE -> resources.add(fileInfo) + FileType.ASSET -> assets.add(fileInfo) + FileType.NATIVE_LIB -> nativeLibs.add(fileInfo) + FileType.JAR -> { + jars.add(jarParser.parse(entry, zipFile.getInputStream(entry))) + } + + else -> others.add(fileInfo) + } + } + return AarFileInfo( + name = sizerInputFile.file.name, + path = sizerInputFile.file.path, + tag = sizerInputFile.tag, + resources = resources, + assets = assets, + nativeLibs = nativeLibs, + others = others, + jars = jars + ) + } + } + + override fun parseAars(files: Sequence): Set { + return files.map { file -> parse(file) }.toSet() + } +} + + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/ApkFileInfo.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/ApkFileInfo.kt new file mode 100644 index 0000000..25e3447 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/ApkFileInfo.kt @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.analyzer.model.FileInfo +import com.grab.sizer.analyzer.model.RawFileInfo + +/** + * A data class that represents the output after parse an APK file by [ApkFileParser]. + * It encapsulates the parsed data from an APK file, including information such as its name, + * size, download size, and the set of resources, native libraries, assets, others, and dex files it contains. + * + * @property name The name of the APK file. + * @property resources A set of resources contained in the APK file. + * @property nativeLibs A set of native libraries contained in the APK file. + * @property assets A set of assets files contained in the APK file. + * @property others A set of FileInfo objects representing other files in the APK file. + * @property dexes A set of [DexFileInfo] objects representing dex files in the APK file. + */ +data class ApkFileInfo( + val name: String, + val resources: Set, + val nativeLibs: Set, + val assets: Set, + val others: Set, + val dexes: Set +) \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/ApkFileParser.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/ApkFileParser.kt new file mode 100644 index 0000000..a16945d --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/ApkFileParser.kt @@ -0,0 +1,132 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.android.tools.apk.analyzer.ApkSizeCalculator +import com.grab.sizer.analyzer.model.FileType +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.di.AppScope +import shadow.bundletool.com.android.tools.proguard.ProguardMap +import java.io.File +import java.nio.file.Path +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import javax.inject.Inject + + +internal interface ApkFileParser { + /** + * Parses a sequence of APK files and use R8 mapping file to extract and return the set of APK file information. + * This method de-obfuscates class names to make them readable, and estimates the download size of each file + * in the [ApkFileInfo] output. + * + * @param apks A sequence of APK files to be parsed. + * @param proguardMap A ProguardMap used for de-obfuscating class names in the APK files. + * @return A set of ApkFileInfo instances, each representing information about a parsed APK file. + */ + fun parseApks(apks: Sequence, proguardMap: ProguardMap): Set +} + +@AppScope +internal class DefaultApkFileParser @Inject constructor( + private val dexFileParser: DexFileParser, + private val apkSizeCalculator: ApkSizeCalculator +) : ApkFileParser { + override fun parseApks(apks: Sequence, proguardMap: ProguardMap): Set = apks + .map { apkFile -> parse(apkFile, proguardMap) } + .toSet() + + private fun parse(file: File, proguardMap: ProguardMap): ApkFileInfo { + val apkSizeInfo = apkSizeCalculator.parseSize(file.toPath()) + return parseApkFile(file, apkSizeInfo, proguardMap) + } + + private fun parseApkFile(file: File, apkSizeInfo: ApkSizeInfo, proguardMap: ProguardMap): ApkFileInfo { + ZipFile(file).use { zipFile -> + val entries = zipFile.entries() + val resources = mutableSetOf() + val assets = mutableSetOf() + val nativeLibs = mutableSetOf() + val others = mutableSetOf() + val dexes = mutableSetOf() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + val path = entry.getPath() + val downloadSize = apkSizeInfo.downloadFileSizeMap[path] ?: 0 + val rawSize = apkSizeInfo.rawFileSizeMap[path] ?: 0 + + val fileInfo = RawFileInfo( + path = path, + downloadSize = downloadSize, + size = rawSize + ) + + when (fileInfo.type) { + FileType.RESOURCE -> resources.add(fileInfo) + FileType.ASSET -> assets.add(fileInfo) + FileType.NATIVE_LIB -> nativeLibs.add(fileInfo) + FileType.DEX -> dexes.add( + dexFileParser.parse( + entry, + zipFile.getInputStream(entry), + apkSizeInfo, + proguardMap + ) + ) + + else -> others.add(fileInfo) + } + } + + return ApkFileInfo( + name = file.name, + resources = resources, + assets = assets, + nativeLibs = nativeLibs, + others = others, + dexes = dexes, + ) + } + } + + private fun ApkSizeCalculator.parseSize(path: Path): ApkSizeInfo = ApkSizeInfo( + downloadSize = getFullApkDownloadSize(path), + size = getFullApkDownloadSize(path), + downloadFileSizeMap = getDownloadSizePerFile(path), + rawFileSizeMap = getRawSizePerFile(path) + ) +} + +internal fun ZipEntry.getPath() = "/$name" + +internal class ApkSizeInfo( + val downloadSize: Long, + val size: Long, + val downloadFileSizeMap: Map, + val rawFileSizeMap: Map +) \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/DataParser.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/DataParser.kt new file mode 100644 index 0000000..82f76c6 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/DataParser.kt @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.di.AppScope +import com.grab.sizer.utils.InputProvider +import javax.inject.Inject + + +/** + * The DataParser interface is used to parse all binary files, including AAR, JAR and APK files. + * It acts as a provider for all binaries that the app-sizer needs for analysis and output reports. + * It delivers a set of parsed APKs, all parsed AAR & JAR libraries, and all parsed AAR & JAR modules. + */ +internal interface DataParser { + val apks: Set + val libAars: Set + val libJars: Set + val moduleAars: Set + val moduleJars: Set +} + +/** + * Combine all AAR library and module files into one set. + */ +internal fun DataParser.getAars() = moduleAars + libAars + +/** + * Combine all JAR library and module files into one set. + */ +internal fun DataParser.getJars() = libJars + moduleJars + +/** + * This default implementation of [DataParser] will cache all the binaries content after parsed. + * The parsed values will be reused by different [com.grab.sizer.analyzer.Analyzer] + */ +@AppScope +internal class DefaultDataParser @Inject constructor( + private val apkFileParser: ApkFileParser, + private val aarFileParser: AarFileParser, + private val jarFileParser: JarFileParser, + private val inputProvider: InputProvider, + private val proguardParser: ProguardFileParser +) : DataParser { + override val apks: Set by lazy { + apkFileParser.parseApks( + inputProvider.provideApkFiles(), + proguardParser.parse(inputProvider.provideR8MappingFile()) + ) + } + override val libAars: Set by lazy { + aarFileParser.parseAars(inputProvider.provideLibraryAar()) + } + override val libJars: Set by lazy { + jarFileParser.parseJars(inputProvider.provideLibraryJar()) + } + override val moduleAars: Set by lazy { + aarFileParser.parseAars(inputProvider.provideModuleAar()) + } + override val moduleJars: Set by lazy { + jarFileParser.parseJars(inputProvider.provideModuleJar()) + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/DexFileInfo.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/DexFileInfo.kt new file mode 100644 index 0000000..1d06715 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/DexFileInfo.kt @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.analyzer.model.RawFileInfo + +/** + * A data class that represents a dex file parsed from the APK by [ApkFileParser]. + * It contains details like the dex file name, download size, set of class files, and normal size. + * + * @property name The name of the dex file. + * @property downloadSize The download size of the dex file. + * @property classes A set of ClassFileInfo objects representing classes contained in the dex file. + * @property size The size of the dex file. + */ +data class DexFileInfo( + val name: String, + val downloadSize: Long, + val classes: Set, + val others: Set = emptySet(), + val size: Long, +) { + // The total size of classes and other files in the dex file (computed lazily). + val classSize: Long by lazy { classes.sumOf { it.size } + others.sumOf { it.size } } +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/DexFileParser.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/DexFileParser.kt new file mode 100644 index 0000000..2ee1439 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/DexFileParser.kt @@ -0,0 +1,102 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + + +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.di.AppScope +import com.grab.sizer.utils.Logger +import com.grab.sizer.utils.log +import org.jf.dexlib2.dexbacked.DexBackedClassDef +import org.jf.dexlib2.dexbacked.DexBackedDexFile +import shadow.bundletool.com.android.tools.proguard.ProguardMap +import java.io.BufferedInputStream +import java.io.InputStream +import java.util.zip.ZipEntry +import javax.inject.Inject + + +/** + * DexFileParser interface provides a method for parsing a dex file located within an APK file. + */ +internal interface DexFileParser { + /** + * Parses a dex file within the APK. + * Reads input stream of the dex file, retrieves class details and converts the file attributes into a DexFileInfo object. + */ + fun parse( + entry: ZipEntry, + inputStream: InputStream, + apkSizeInfo: ApkSizeInfo, + proguardMap: ProguardMap? = null + ): DexFileInfo +} + +/** + * DefaultDexFileParser is the default implementation of the DexFileParser interface. + * It utilizes the DexBackedDexFile class from the 'org.smali:dexlib2' library to perform dex file parsing. + */ +@AppScope +internal class DefaultDexFileParser @Inject constructor( + private val logger: Logger +) : DexFileParser { + override fun parse( + entry: ZipEntry, + inputStream: InputStream, + apkSizeInfo: ApkSizeInfo, + proguardMap: ProguardMap? + ): DexFileInfo { + val dexBackedDexFile = DexBackedDexFile.fromInputStream(null, BufferedInputStream(inputStream)) + + val classes = dexBackedDexFile.classes + .map { classDef -> fromDex(classDef, proguardMap) } + .toSet() + + val path = entry.getPath() + val dexDownloadSize = apkSizeInfo.downloadFileSizeMap[path] ?: 0 + val dexClassesSize = classes.sumOf { it.size } + val ratio = dexDownloadSize.toDouble() / dexClassesSize + return DexFileInfo( + name = path, + downloadSize = dexDownloadSize, + size = apkSizeInfo.rawFileSizeMap[path] ?: 0, + classes = classes.map { it.copy(downloadSize = (it.size * ratio).toLong()) }.toSet() + ) + } + + private fun fromDex(classDef: DexBackedClassDef, proguardMap: ProguardMap?): ClassFileInfo { + val className = classDef.type.removePrefix("L").replace('/', '.').removeSuffix(";") + if (proguardMap != null && proguardMap.getClassName(className) == null) { + logger.log("Can not find $className in from proguard mapping file") + } + return ClassFileInfo( + name = proguardMap?.getClassName(className) ?: className, + size = classDef.size.toLong() + ) + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarFileInfo.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarFileInfo.kt new file mode 100644 index 0000000..0a81603 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarFileInfo.kt @@ -0,0 +1,51 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.analyzer.model.RawFileInfo + +/** + * A data class that represents a jar file parsed from the jar by [JarFileParser] or [JarStreamParser]. + * It contains details like the classes, path to the file, native libs and others + */ +data class JarFileInfo( + override val name: String, + override val path: String, + override val tag: String, + val classes: Set, + val nativeLibs: Set, + val others: Set = emptySet() +) : BinaryFileInfo { + override fun hashCode(): Int = path.hashCode() + + override fun equals(other: Any?): Boolean { + if (other !is JarFileInfo) return false + return path == other.path + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarFileParser.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarFileParser.kt new file mode 100644 index 0000000..0dbd7c6 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarFileParser.kt @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.analyzer.model.FileType +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.di.AppScope +import com.grab.sizer.utils.SizerInputFile +import java.util.zip.ZipFile +import javax.inject.Inject + + +/** + * JarFileParser interface provides a method to parse a sequence of JAR files into a set of [JarFileInfo]. + * Note: For native libraries (*.so), their paths will be adjusted to ensure the files reside under the "lib" folder. + * This modification facilitates mapping to native libraries in the APK file. + */ +interface JarFileParser { + fun parseJars(files: Sequence): Set +} + +@AppScope +class DefaultJarFileParser @Inject constructor() : JarFileParser { + private fun parse(sizerInputFile: SizerInputFile): JarFileInfo { + ZipFile(sizerInputFile.file).use { zipFile -> + val entries = zipFile.entries() + val nativeLibs = mutableSetOf() + val others = mutableSetOf() + val classes = mutableSetOf() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + val fileInfo = RawFileInfo( + path = entry.getPath(), + size = entry.size, + downloadSize = -1 + ) + when (fileInfo.type) { + FileType.NATIVE_LIB -> nativeLibs.add(fileInfo) + FileType.CLASS -> classes.add(entry.toClass()) + else -> others.add(fileInfo) + } + } + return JarFileInfo( + name = sizerInputFile.file.name, + path = sizerInputFile.file.path, + tag = sizerInputFile.tag, + others = others, + nativeLibs = nativeLibs, + classes = classes + ) + } + } + + override fun parseJars(files: Sequence): Set { + return files.map { file -> parse(file) } + .toSet() + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarStreamParser.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarStreamParser.kt new file mode 100644 index 0000000..c899e08 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/JarStreamParser.kt @@ -0,0 +1,96 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.analyzer.model.FileType +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.di.AppScope +import java.io.InputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import javax.inject.Inject + + +/** + * JarStreamParser interface provides a method to parse a JAR file within an AAR. + * It uses the ZipEntry of the JAR file and a provided InputStream to access JAR content within the AAR file. + */ +interface JarStreamParser { + /** + * Parses the contents of a JAR file within the AAR file. + * @param jarEntry ZipEntry of the JAR file within the AAR file. + * @param inputStream InputStream to access the JAR content. + * @return A JarFileInfo object containing the properties of parsed JAR file. + */ + fun parse(jarEntry: ZipEntry, inputStream: InputStream): JarFileInfo +} + +@AppScope +class DefaultJarStreamParser @Inject constructor() : JarStreamParser { + override fun parse(jarEntry: ZipEntry, inputStream: InputStream): JarFileInfo { + ZipInputStream(inputStream).use { entries -> + val others = mutableSetOf() + val classes = mutableSetOf() + var entry = entries.nextEntry + while (entry != null) { + val fileInfo = RawFileInfo( + path = entry.getPath(), + size = entry.size, + downloadSize = -1 + ) + when (fileInfo.type) { + FileType.CLASS -> classes.add(entry.toClass()) + else -> others.add(fileInfo) + } + entry = entries.nextEntry + } + return JarFileInfo( + name = jarEntry.getPath(), + path = "", + tag = "", + others = others, + nativeLibs = emptySet(), + classes = classes + ) + } + } +} + +internal fun ZipEntry.toClass(): ClassFileInfo { + return ClassFileInfo( + /** + * Convert ZipEntry name to class name + * Example: "/com/grab/sample/dummy/DummyClass1.class" -> "com.grab.sample.dummy.DummyClass1" + */ + name = name.replace('/', '.').removeSuffix(".class"), + size = size + ) +} + + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/parser/ProguardFileParser.kt b/app-sizer/src/main/kotlin/com/grab/sizer/parser/ProguardFileParser.kt new file mode 100644 index 0000000..c86a928 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/parser/ProguardFileParser.kt @@ -0,0 +1,66 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.parser + +import com.google.common.base.Charsets +import com.grab.sizer.utils.Logger +import com.grab.sizer.utils.log +import shadow.bundletool.com.android.tools.proguard.ProguardMap +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.nio.file.Files +import java.text.ParseException +import javax.inject.Inject + + +/** + * ProguardFileParser interface provides a method to generate a ProguardMap from a Proguard file. + * If the provided file is null, an empty ProguardMap is returned. + * Note: The ProguardMap is part of the bundletool library ("com.android.tools.build:bundletool"). + */ +interface ProguardFileParser { + fun parse(proguardFile: File?): ProguardMap +} + +class DefaultProguardFileParser @Inject constructor( + private val logger: Logger +) : ProguardFileParser { + override fun parse(proguardFile: File?): ProguardMap = ProguardMap().apply { + if (proguardFile == null) return@apply + try { + Files.newInputStream(proguardFile.toPath()).use { + readFromReader(InputStreamReader(it, Charsets.UTF_8)) + } + } catch (e: IOException) { + logger.log(e) + } catch (e: ParseException) { + logger.log(e) + } + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/DatabaseReportWriter.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/DatabaseReportWriter.kt new file mode 100644 index 0000000..35efd78 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/DatabaseReportWriter.kt @@ -0,0 +1,80 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report + +import com.grab.sizer.report.db.ReportDao +import dagger.Lazy +import javax.inject.Inject + +class DatabaseReportWriter @Inject constructor( + private val reportDaoSet: Lazy>, + private val projectInfo: ProjectInfo, + private val customProperties: CustomProperties +) : ReportWriter { + override fun write(report: Report) { + /** + * Add fields from [projectInfo] and [customProperties] to the report before write to the database + */ + val addedCommonValueReport = report.copy( + rows = report.rows.map { row -> + row.copy( + fields = row.fields + customProperties.toTags() + projectInfo.toTags() + report.typeField() + ) + } + ) + reportDaoSet.get().forEach { + it.addReport(addedCommonValueReport) + } + } + + private fun CustomProperties.toTags(): List = map { property -> + DefaultField(property.key, property.value) + } + + private fun Report.typeField() = TagField("type", id) + + private fun ProjectInfo.toTags(): List = + listOf( + TagField( + name = "project", + value = projectName, + ), + TagField( + name = "app_version", + value = versionName, + ), + TagField( + name = "build_type", + value = buildType, + ), + TagField( + name = "device_name", + value = deviceName, + ) + ) +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/MarkdownReportWriter.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/MarkdownReportWriter.kt new file mode 100644 index 0000000..68e0194 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/MarkdownReportWriter.kt @@ -0,0 +1,123 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report + +import java.io.File +import java.util.* +import javax.inject.Inject +import javax.inject.Named + +private const val KILO_BYTE = 1024L +private const val MEGA_BYTE = 1024L * 1024L + +internal const val NAMED_OUTPUT_DIR = "output_dir" + +class MarkdownReportWriter @Inject constructor( + @Named(NAMED_OUTPUT_DIR) private val outputDirectory: File, + private val projectInfo: ProjectInfo, +) : ReportWriter { + override fun write(report: Report) { + File(File(outputDirectory, projectInfo.deviceName), "${report.id}-report.md").apply { + initOutPutFile() + writeText( + MarkdownTable(report.createHeader()).apply { + report.rows.sortedBy { it.size() }.forEach { row -> + addRow(row.toMarkDown()) + } + }.generate() + ) + } + } + + private fun Row.toMarkDown(): List { + return fields.map { field -> + when (field.value) { + is Long -> (field.value as Long).reportSize() + else -> field.value.toString() + } + } + } + + private fun Report.createHeader(): List { + return rows.firstOrNull() + ?.fields + ?.map { field -> + field.name.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + } ?: emptyList() + } + + private fun File.initOutPutFile() { + if (!exists()) { + if (!parentFile.exists()) + parentFile.mkdirs() + createNewFile() + } + } + + private fun Row.size(): Long = fields.find { it.name == "size" }?.value as Long +} + +internal fun Long.reportSize(): String = when { + this < KILO_BYTE -> "$this bytes" + this < MEGA_BYTE -> "%.3f KB".format(this.toDouble() / KILO_BYTE) + else -> "%.3f MB".format(this.toDouble() / MEGA_BYTE) +} + +/** + * A simple class to generate mark-down table + * It was generated by Chatgpt + */ +class MarkdownTable(private val headers: List) { + private val rows: MutableList> = mutableListOf() + + fun addRow(row: List) { + require(row.size == headers.size) { "Row has different number of columns compared to headers. $headers vs $row" } + rows.add(row) + } + + fun generate(): String { + val sb = StringBuilder() + sb.append("| ") + headers.forEach { header -> + sb.append(header) + sb.append(" | ") + } + sb.append("\n| ") + repeat(headers.size) { sb.append("--- | ") } + rows.forEach { row -> + sb.append("\n| ") + row.forEach { item -> + sb.append(item) + sb.append(" | ") + } + } + return sb.toString() + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportExt.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportExt.kt new file mode 100644 index 0000000..cf2f2ab --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportExt.kt @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report + +import com.grab.sizer.analyzer.ReportItem +import com.grab.sizer.parser.ApkFileInfo + +internal fun Set.apksSizeReport(): ReportItem { + val resourceDownloadSize = flatMap { it.resources }.sumOf { it.downloadSize } + val nativeLibDownloadSize = flatMap { it.nativeLibs }.sumOf { it.downloadSize } + val assetDownloadSize = flatMap { it.assets }.sumOf { it.downloadSize } + val otherDownloadSize = flatMap { it.others }.sumOf { it.downloadSize } + val classDownloadSize = flatMap { it.dexes }.flatMap { it.classes }.sumOf { it.downloadSize } + val total = + resourceDownloadSize + nativeLibDownloadSize + assetDownloadSize + otherDownloadSize + classDownloadSize + + return ReportItem( + id = "apk", + totalDownloadSize = total, + name = "Apks", + resourceDownloadSize = resourceDownloadSize, + nativeLibDownloadSize = nativeLibDownloadSize, + assetDownloadSize = assetDownloadSize, + otherDownloadSize = otherDownloadSize, + classesDownloadSize = classDownloadSize, + extraInfo = "Apk breakdown by component sizer" + ) +} + +internal fun Set.toReportField(): List { + val resourceDownloadSize = flatMap { it.resources }.sumOf { it.downloadSize } + val nativeLibDownloadSize = flatMap { it.nativeLibs }.sumOf { it.downloadSize } + val assetDownloadSize = flatMap { it.assets }.sumOf { it.downloadSize } + val otherDownloadSize = flatMap { it.others }.sumOf { it.downloadSize } + val classDownloadSize = flatMap { it.dexes }.flatMap { it.classes }.sumOf { it.downloadSize } + val total = + resourceDownloadSize + nativeLibDownloadSize + assetDownloadSize + otherDownloadSize + classDownloadSize + return listOf( + TagField( + name = FIELD_KEY_CONTRIBUTOR, + value = "apk" + ), + DefaultField( + name = FIELD_KEY_SIZE, + value = total + ) + ) +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportModule.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportModule.kt new file mode 100644 index 0000000..9fb8fca --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportModule.kt @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report + +import com.grab.sizer.di.AppScope +import com.grab.sizer.report.db.DbReportDaoFactory +import com.grab.sizer.report.db.ReportDao +import com.grab.sizer.report.json.JsonReportWriter +import com.grab.sizer.utils.OutputProvider +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import java.io.File +import javax.inject.Named + +@Module +object ReportModule { + @Provides + fun provideCustomProperties(outputProvider: OutputProvider): CustomProperties = + outputProvider.provideCustomProperties() + + @Provides + @Named(NAMED_OUTPUT_DIR) + fun provideOutputDirectory(outputProvider: OutputProvider): File = outputProvider.provideOutPutDirectory() + + @Provides + fun provideProjectInfo(outputProvider: OutputProvider): ProjectInfo = outputProvider.provideProjectInfo() + + @Provides + @AppScope + fun provideReportDaoSet(reportDaoFactory: DbReportDaoFactory): Set = reportDaoFactory.create() +} + +@Module +interface ReportModuleBinder { + @IntoSet + @Binds + fun bindMarkdownReportWriter(writer: MarkdownReportWriter): ReportWriter + + @Binds + @IntoSet + fun bindJsonReportWriter(writer: JsonReportWriter): ReportWriter + + @Binds + @IntoSet + fun bindDatabaseReportWriter(writer: DatabaseReportWriter): ReportWriter +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportWriter.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportWriter.kt new file mode 100644 index 0000000..380328b --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/ReportWriter.kt @@ -0,0 +1,112 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report + + +internal const val FIELD_KEY_CONTRIBUTOR = "contributor" +internal const val FIELD_KEY_SIZE = "size" +internal const val FIELD_KEY_OWNER = "owner" +internal const val FIELD_KEY_TAG = "tag" + + +typealias CustomProperties = Map + +/** + * A data class that encapsulates information about a project. + * It includes information such as the version name, project name, device name, and build type. + * All these attributes will be treated as tags in the database. + * + * @property versionName The version name of the application. + * @property projectName The name of the project. + * @property deviceName The name of the device where the application is analysis. + * @property buildType The type of the build (defaults to "production"). + */ +data class ProjectInfo( + val versionName: String, + val projectName: String, + val deviceName: String, + val buildType: String = "production" +) + + +data class Row( + val name: String, + val fields: List +) + +interface Field { + val name: String + val value: Any + + companion object { + fun createDefault(name: String, value: Any): Field = + DefaultField(name, value) + } +} + +/** + * Those of items which having a large set of values which is not suitable for tag in a database + */ +data class DefaultField( + override val name: String, + override val value: Any, +) : Field + +/** + * Those of items which having a small set of values which is suitable for tag in a database + */ +data class TagField( + override val name: String, + override val value: Any, +) : Field + +/** + * + */ +data class Report( + val id: String, + val name: String, + val rows: List +) + +/** + * The ReportWriter is an abstraction layer for the reporting process. It's allowing for flexibility in the reporting logics + * It could be implemented to send reports to database, markdown, json file, etc. + * + * This interface is utilized by the [com.grab.sizer.AppSizer] to report the output. + * [com.grab.sizer.AppSizer] will consume a set of [ReportWriter] instances provided by the [ReportModule] Dagger module. + * + * Implement this interface to add a new reporting method, and add it to the [ReportModule] + * The new implementation will then be automatically consumed by all [com.grab.sizer.analyzer.Analyzer]. + */ +interface ReportWriter { + fun write(report: Report) +} + + + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/db/DbReportDaoFactory.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/db/DbReportDaoFactory.kt new file mode 100644 index 0000000..ba8caac --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/db/DbReportDaoFactory.kt @@ -0,0 +1,45 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report.db + +import com.grab.sizer.utils.Logger +import com.grab.sizer.utils.OutputProvider +import org.influxdb.InfluxDB +import org.influxdb.InfluxDBIOException +import java.net.ConnectException +import javax.inject.Inject + +class DbReportDaoFactory @Inject constructor( + private val outputProvider: OutputProvider, + private val logger: Logger +) { + fun create(): Set = outputProvider.provideInfluxDbConfig()?.run { + val influxClient = InfluxDBFactory().create(this) + setOf(InfluxDbReportDao(influxClient, this)) + } ?: emptySet() +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/db/InfluxDbReportDao.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/db/InfluxDbReportDao.kt new file mode 100644 index 0000000..18d740b --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/db/InfluxDbReportDao.kt @@ -0,0 +1,195 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report.db + +import com.grab.sizer.report.DefaultField +import com.grab.sizer.report.Report +import com.grab.sizer.report.TagField +import org.influxdb.BatchOptions +import org.influxdb.InfluxDB +import org.influxdb.dto.BatchPoints +import org.influxdb.dto.Point +import org.influxdb.dto.Query +import org.influxdb.dto.QueryResult +import org.influxdb.impl.Preconditions +import java.io.Serializable +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private const val SHOW_DATABASE_COMMAND = "SHOW DATABASES" +private const val DEFAULT_TABLE = "app_size" +private const val DEFAULT_DATABASE = "sizer" + + +data class InfluxDBConfig( + private val dbName: String?, + val url: String, + val username: String?, + val password: String?, + val reportTableName: String?, + val databaseRetentionPolicy: DatabaseRetentionPolicy? +) : Serializable { + val databaseName: String = dbName ?: DEFAULT_DATABASE +} + +data class DatabaseRetentionPolicy( + val name: String, + val duration: String, + val shardDuration: String, + val replicationFactor: Int, + val isDefault: Boolean, +) : Serializable { + companion object { + fun createDefault() = DatabaseRetentionPolicy( + name = "app_sizer", + duration = "360d", + shardDuration = "0m", + replicationFactor = 2, + isDefault = true + ) + } +} + +class InfluxDBFactory { + fun create(config: InfluxDBConfig): InfluxDB { + return org.influxdb.InfluxDBFactory.connect(config.url, config.username, config.password).apply { + setLogLevel(InfluxDB.LogLevel.BASIC) + enableBatch( + BatchOptions.DEFAULTS + .threadFactory { runnable: Runnable? -> + val thread = Thread(runnable) + thread.setDaemon(true) + thread + } + ) + Runtime.getRuntime().addShutdownHook(Thread(::close)) + } + } +} + +class InfluxDbReportDao @Inject constructor( + private val influxDB: InfluxDB, + private val config: InfluxDBConfig +) : ReportDao { + init { + + if (databaseExists(config.databaseName)) { + influxDB.setDatabase(config.databaseName) + } else { + createDatabase(config) + influxDB.setDatabase(config.databaseName) + } + } + + private fun createDatabase(influxDBConfig: InfluxDBConfig) { + Preconditions.checkNonEmptyString(influxDBConfig.databaseName, "name") + influxDB.query( + Query( + "CREATE DATABASE ${influxDBConfig.databaseName}" + ) + ) + + influxDBConfig.databaseRetentionPolicy?.run { + influxDB.query( + Query( + """CREATE RETENTION POLICY $name + |ON ${influxDBConfig.databaseName} + |DURATION $duration + |REPLICATION $replicationFactor + |SHARD DURATION $shardDuration + |${if (isDefault) "DEFAULT" else ""} + |""".trimMargin() + ) + ) + } + } + + private fun describeDatabases(): List { + val result: QueryResult = influxDB.query(Query(SHOW_DATABASE_COMMAND)) + // {"results":[{"series":[{"name":"databases","columns":["name"],"values":[["mydb"]]}]}]} + // Series [name=databases, columns=[name], values=[[mydb], [unittest_1433605300968]]] + val databaseNames = result.results[0].series[0].values + val databases: MutableList = ArrayList() + if (databaseNames != null) { + for (database in databaseNames) { + databases.add(database[0].toString()) + } + } + return databases + } + + private fun databaseExists(name: String): Boolean { + val databases = describeDatabases() + for (databaseName in databases) { + if (databaseName.trim { it <= ' ' } == name) { + return true + } + } + return false + } + + override fun addReport(report: Report) { + val pointsBuilder = BatchPoints.builder() + report.rows.forEachIndexed { index, row -> + val point = Point.measurement(config.reportTableName ?: DEFAULT_TABLE) + .apply { + /** + * A small hack (add index to time) to prevent InfluxDb remove duplicate + * (similar time + tags rows will be removed) + **/ + time(System.currentTimeMillis() + index, TimeUnit.MILLISECONDS) + row.fields.forEach { field -> + when (field) { + is DefaultField -> { + when (val value = field.value) { + is Long -> addField(field.name, value) + is Int -> addField(field.name, value) + is Boolean -> addField(field.name, value) + is Double -> addField(field.name, value) + is Short -> addField(field.name, value) + is Float -> addField(field.name, value) + else -> addField(field.name, value.toString()) + } + } + + is TagField -> { + tag(field.name, field.value.toString()) + } + } + } + }.build() + pointsBuilder.point(point) + } + influxDB.write(pointsBuilder.build()) + } + + + override fun getReportById(id: String) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/db/ReportDao.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/db/ReportDao.kt new file mode 100644 index 0000000..cbdda86 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/db/ReportDao.kt @@ -0,0 +1,36 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report.db + +import com.grab.sizer.report.Report + +interface ReportDao { + fun addReport(report: Report) + fun getReportById(id : String) +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/json/JsonReportWriter.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/json/JsonReportWriter.kt new file mode 100644 index 0000000..d40f9b7 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/json/JsonReportWriter.kt @@ -0,0 +1,136 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report.json + +import com.google.gson.Gson +import com.grab.sizer.report.* +import java.io.File +import java.io.FileWriter +import javax.inject.Inject +import javax.inject.Named + +typealias ReportField = com.grab.sizer.report.Field + +class JsonReportWriter @Inject constructor( + @Named(NAMED_OUTPUT_DIR) private val outputDirectory: File, + private val projectInfo: ProjectInfo, + private val customProperties: CustomProperties, + private val gson: Gson = Gson() +) : ReportWriter { + override fun write(report: Report) { + File(File(outputDirectory, projectInfo.deviceName), "${report.id}-metrics.json").apply { + initOutPutFile() + FileWriter(this).use { fileWriter -> + gson.toJson( + report.rows.flatMap { row -> + row.toMetrics(projectInfo, customProperties, report.id) + }, + fileWriter + ) + } + } + } + + + private fun File.initOutPutFile() { + if (!exists()) { + if (!parentFile.exists()) + parentFile.mkdirs() + createNewFile() + } + } + + private fun Row.toMetrics( + projectInfo: ProjectInfo, + customProperties: CustomProperties, + metricsId: String + ): List = listOf( + Metrics( + fields = fields.toMetricsFields() + customProperties.toCommonFields(), + tags = fields.toMetricsTags() + projectInfo.toCommonTags(), + timestamp = System.currentTimeMillis(), + name = metricsId + ) + ) + + private fun List.toMetricsFields() = this.filterIsInstance() + .map { field -> + Field( + name = field.name, + value = field.value.toString(), + valueType = field.toMetricsType() + ) + } + + private fun List.toMetricsTags() = this.filterIsInstance().map { field -> + Tag( + name = field.name, + value = field.value.toString(), + valueType = field.toMetricsType() + ) + } + + private fun CustomProperties.toCommonFields(): List = map { + Field( + name = it.key, + value = it.value, + valueType = "string" + ) + } + + + private fun ProjectInfo.toCommonTags(): List = + listOf( + Tag( + name = "project", + value = projectName, + valueType = "string" + ), + Tag( + name = "app_version", + value = versionName, + valueType = "string" + ), + Tag( + name = "build_type", + value = buildType, + valueType = "string" + ), + Tag( + name = "device_name", + value = deviceName, + valueType = "string" + ) + ) +} + +private fun ReportField.toMetricsType(): String = when (value) { + is Int -> "integer" + is Long -> "integer" + else -> "string" +} diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/report/json/Metrics.kt b/app-sizer/src/main/kotlin/com/grab/sizer/report/json/Metrics.kt new file mode 100644 index 0000000..03a93f0 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/report/json/Metrics.kt @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.report.json + +import com.google.gson.annotations.SerializedName + +data class Metrics( + @SerializedName("name") + val name: String, + @SerializedName("fields") + val fields: List, + @SerializedName("tags") + val tags: List, + @SerializedName("timestamp") + val timestamp: Long +) + +data class Field( + @SerializedName("name") + val name: String, + @SerializedName("value") + val value: String, + /** + * Values: "float", "string", "integer", "boolean" + */ + @SerializedName("value_type") + val valueType: String +) + +data class Tag( + @SerializedName("name") + val name: String, + @SerializedName("value") + val value: String, + @SerializedName("value_type") + val valueType: String +) \ No newline at end of file diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/utils/InputProvider.kt b/app-sizer/src/main/kotlin/com/grab/sizer/utils/InputProvider.kt new file mode 100644 index 0000000..8a64c93 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/utils/InputProvider.kt @@ -0,0 +1,69 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.utils + +import com.grab.sizer.report.CustomProperties +import com.grab.sizer.report.ProjectInfo +import com.grab.sizer.report.db.InfluxDBConfig +import java.io.File + +/** + * The InputProvider interface is used to provide all necessary inputs for the app-sizer tool to process. + * The client of the app-sizer should provide these information details for the tool to process. + * Currently, the interface is implemented in two modules: the command-line tool (cli) and the Gradle plugin. + */ +interface InputProvider { + fun provideModuleAar(): Sequence + fun provideModuleJar(): Sequence + fun provideLibraryJar(): Sequence + fun provideLibraryAar(): Sequence + fun provideApkFiles(): Sequence + fun provideR8MappingFile(): File? + fun provideTeamMappingFile(): File? + fun provideLargeFileThreshold(): Long +} + +data class SizerInputFile( + val file: File, + val tag: String, +) + +/** + * The OutputProvider interface is responsible for providing all output configurations + * for the app-sizer tool to correctly export its output. + * The client of the app-sizer should provide these configuration details for the tool to process. + * Like the InputProvider, this interface is currently implemented by the command-line tool (cli) and the Gradle plugin. + */ +interface OutputProvider { + fun provideInfluxDbConfig(): InfluxDBConfig? + fun provideOutPutDirectory(): File + + fun provideProjectInfo(): ProjectInfo + fun provideCustomProperties(): CustomProperties +} + diff --git a/app-sizer/src/main/kotlin/com/grab/sizer/utils/Logger.kt b/app-sizer/src/main/kotlin/com/grab/sizer/utils/Logger.kt new file mode 100644 index 0000000..b97f943 --- /dev/null +++ b/app-sizer/src/main/kotlin/com/grab/sizer/utils/Logger.kt @@ -0,0 +1,48 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.utils + + +const val DEFAULT_TAG = "AppSize" + +interface Logger { + fun log(tag: String, message: String) + fun log(tag: String, e: Exception) + fun logDebug(tag: String, message: String) +} + +fun Logger.logDebug(message: String) { + logDebug(DEFAULT_TAG, message) +} +fun Logger.log(message: String) { + log(DEFAULT_TAG, message) +} + +fun Logger.log(e: Exception) { + log(DEFAULT_TAG, e) +} diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/ApkAnalyzerTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/ApkAnalyzerTest.kt new file mode 100644 index 0000000..259d382 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/ApkAnalyzerTest.kt @@ -0,0 +1,195 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.report.* +import org.junit.Test +import kotlin.test.assertEquals + +/** + * This is more likely an integration test, not just unit test + */ +class ApkAnalyzerTest { + private val mapperComponent = MapperComponent() + private val project1Data = Project1Data() + private val apkAnalyzer = ApkAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project1Data.fakeDataPasser + ) + + @Test + fun testApkAnalyzerWithProject1Data() { + val report = apkAnalyzer.process().sort() + assertEquals(expectedProject1Report.sort(), report) + } + + @Test + fun testApkAnalyzerShouldReportProperApkSize() { + val report = apkAnalyzer.process() + val apkRow = report.rows.find { it.name == "Apk" } + assertEquals(expectApkRow, apkRow) + } + + @Test + fun testApkAnalyzerShouldReportProperCodebaseKotlinJavaSize() { + val report = apkAnalyzer.process() + val codebaseKotlinJavaRow = report.rows.find { it.name == "codebase-kotlin-java" } + assertEquals(expectCodebaseKotlinJavaRow, codebaseKotlinJavaRow) + } + + @Test + fun testApkAnalyzerShouldReportProperCodebaseResourcesSize() { + val report = apkAnalyzer.process() + val codebaseResourcesRow = report.rows.find { it.name == "codebase-resources" } + assertEquals(expectCodebaseResourcesRow, codebaseResourcesRow) + } + + @Test + fun testApkAnalyzerShouldReportProperCodebaseAssetsSize() { + val report = apkAnalyzer.process() + val codebaseAssetsRow = report.rows.find { it.name == "codebase-assets" } + assertEquals(expectCodebaseAssetsRow, codebaseAssetsRow) + } + + @Test + fun testApkAnalyzerShouldReportProperCodebaseNativeSize() { + val report = apkAnalyzer.process() + val codebaseNativeRow = report.rows.find { it.name == "codebase-native" } + assertEquals(expectCodebaseNativeRow, codebaseNativeRow) + } + + @Test + fun testApkAnalyzerShouldReportProperOthersSize() { + val report = apkAnalyzer.process() + val othersRow = report.rows.find { it.name == "others" } + assertEquals(expectOthersRow, othersRow) + } + + @Test + fun testApkAnalyzerShouldReportProperAndroidJavaLibrariesSize() { + val report = apkAnalyzer.process() + val androidJavaLibrariesRow = report.rows.find { it.name == "android-java-libraries" } + assertEquals(expectAndroidJavaLibrariesRow, androidJavaLibrariesRow) + } + + @Test + fun testApkAnalyzerShouldReportProperNativeLibrariesSize() { + val report = apkAnalyzer.process() + val nativeLibrariesRow = report.rows.find { it.name == "native-libraries" } + assertEquals(expectNativeLibrariesRow, nativeLibrariesRow) + } + + private val expectApkRow = Row( + name = "Apk", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "apk"), + DefaultField(name = FIELD_KEY_SIZE, value = 292L) + ) + ) + + private val expectCodebaseKotlinJavaRow = Row( + name = "codebase-kotlin-java", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "codebase-kotlin-java"), + DefaultField(name = FIELD_KEY_SIZE, value = 40L) + ) + ) + + private val expectCodebaseResourcesRow = Row( + name = "codebase-resources", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "codebase-resources"), + DefaultField(name = FIELD_KEY_SIZE, value = 40L) + ) + ) + + private val expectCodebaseAssetsRow = Row( + name = "codebase-assets", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "codebase-assets"), + DefaultField(name = FIELD_KEY_SIZE, value = 50L) + ) + ) + + private val expectCodebaseNativeRow = Row( + name = "codebase-native", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "codebase-native"), + DefaultField(name = FIELD_KEY_SIZE, value = 80L) + ) + ) + + private val expectOthersRow = Row( + name = "others", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "others"), + DefaultField(name = FIELD_KEY_SIZE, value = 20L) + ) + ) + + private val expectAndroidJavaLibrariesRow = Row( + name = "android-java-libraries", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "android-java-libraries"), + DefaultField(name = FIELD_KEY_SIZE, value = 52L) + ) + ) + + private val expectNativeLibrariesRow = Row( + name = "native-libraries", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "native-libraries"), + DefaultField(name = FIELD_KEY_SIZE, value = 10L) + ) + ) + + private val expectedProject1Report = Report( + id = "apk", + name = "apk", + rows = listOf( + expectApkRow, + expectCodebaseKotlinJavaRow, + expectCodebaseResourcesRow, + expectCodebaseAssetsRow, + expectCodebaseNativeRow, + expectOthersRow, + expectAndroidJavaLibrariesRow, + expectNativeLibrariesRow + ) + ) +} + +internal fun Report.sort(): Report { + return this.copy( + rows = this.rows.map { row -> + row.copy( + fields = row.fields.sortedBy { it.name } + ) + }.sortedBy { it.name } + ) +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/BasicApkAnalyzerTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/BasicApkAnalyzerTest.kt new file mode 100644 index 0000000..fc31a5a --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/BasicApkAnalyzerTest.kt @@ -0,0 +1,146 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.report.* +import org.junit.Assert.assertEquals +import org.junit.Test + +class BasicApkAnalyzerTest { + private val project1Data = Project1Data() + private val apkAnalyzer = BasicApkAnalyzer(dataParser = project1Data.fakeDataPasser) + + @Test + fun testBasicApkAnalyzerWithProject1Data() { + val report = apkAnalyzer.process().sort() + assertEquals(expectedProject1Report.sort(), report) + } + + @Test + fun testBasicApkAnalyzerShouldReportProperApkSize() { + val report = apkAnalyzer.process() + val apkRow = report.rows.find { it.name == "apk" } + assertEquals(expectApkRow, apkRow) + } + + @Test + fun testBasicApkAnalyzerShouldReportProperResourceSize() { + val report = apkAnalyzer.process() + val resourceRow = report.rows.find { it.name == "resource" } + assertEquals(expectResourceRow, resourceRow) + } + + @Test + fun testBasicApkAnalyzerShouldReportProperNativeLibSize() { + val report = apkAnalyzer.process() + val nativeLibRow = report.rows.find { it.name == "native_lib" } + assertEquals(expectNativeLibRow, nativeLibRow) + } + + @Test + fun testBasicApkAnalyzerShouldReportProperAssetSize() { + val report = apkAnalyzer.process() + val assetRow = report.rows.find { it.name == "asset" } + assertEquals(expectAssetRow, assetRow) + } + + @Test + fun testBasicApkAnalyzerShouldReportProperOtherSize() { + val report = apkAnalyzer.process() + val otherRow = report.rows.find { it.name == "other" } + assertEquals(expectOtherRow, otherRow) + } + + @Test + fun testBasicApkAnalyzerShouldReportProperCodeSize() { + val report = apkAnalyzer.process() + val codeRow = report.rows.find { it.name == "code" } + assertEquals(expectCodeRow, codeRow) + } + + private val expectApkRow = Row( + name = "apk", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "apk"), + DefaultField(name = FIELD_KEY_SIZE, value = 292L) + ) + ) + + private val expectResourceRow = Row( + name = "resource", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "resource"), + DefaultField(name = FIELD_KEY_SIZE, value = 70L) + ) + ) + + private val expectNativeLibRow = Row( + name = "native_lib", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "native_lib"), + DefaultField(name = FIELD_KEY_SIZE, value = 90L) + ) + ) + + private val expectAssetRow = Row( + name = "asset", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "asset"), + DefaultField(name = FIELD_KEY_SIZE, value = 60L) + ) + ) + + private val expectOtherRow = Row( + name = "other", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "other"), + DefaultField(name = FIELD_KEY_SIZE, value = 20L) + ) + ) + + private val expectCodeRow = Row( + name = "code", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "code"), + DefaultField(name = FIELD_KEY_SIZE, value = 52L) + ) + ) + + private val expectedProject1Report = Report( + id = "apk_basic", + name = "apk_basic", + rows = listOf( + expectApkRow, + expectResourceRow, + expectNativeLibRow, + expectAssetRow, + expectOtherRow, + expectCodeRow + ) + ) +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/CodebaseAnalyzerTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/CodebaseAnalyzerTest.kt new file mode 100644 index 0000000..8d20d18 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/CodebaseAnalyzerTest.kt @@ -0,0 +1,157 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.JarFileInfo +import com.grab.sizer.report.* +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class CodebaseAnalyzerTest { + private val mapperComponent = MapperComponent() + private val project1Data = Project1Data() + private val analyzer = CodebaseAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project1Data.fakeDataPasser, + teamMapping = project1Data.teamMapping + ) + + @Test + fun testCodebaseAnalyzerWithProject1Data() { + val report = analyzer.process().sort() + assertEquals(expectedProject1Report.sort(), report) + } + + @Test + fun testCodebaseAnalyzerShouldReportCorrectNumberOfTeams() { + val report = analyzer.process() + assertEquals(2, report.rows.size, "Project1 report should contain exactly 2 teams") + } + + @Test + fun testCodebaseAnalyzerShouldReportCorrectTeamNames() { + val report = analyzer.process() + val teamNames = report.rows.map { it.name }.toSet() + val expectedTeamNames = setOf("team1", "team2") + assertEquals(expectedTeamNames, teamNames, "Should report the correct team names") + } + + @Test + fun testCodebaseAnalyzerShouldReportCorrectTeam1Size() { + val report = analyzer.process() + val team1Row = report.rows.find { it.name == "team1" } + assertNotNull(team1Row, "Project1 report should contain team1") + assertEquals(103L, team1Row.fields.find { it.name == FIELD_KEY_SIZE }?.value) + } + + @Test + fun testCodebaseAnalyzerShouldReportCorrectTeam2Size() { + val report = analyzer.process() + val team2Row = report.rows.find { it.name == "team2" } + assertNotNull(team2Row, "Project1 report should contain team2") + assertEquals(107L, team2Row.fields.find { it.name == FIELD_KEY_SIZE }?.value) + } + + @Test + fun testCodebaseAnalyzerShouldReportCorrectContributorFields() { + val report = analyzer.process() + report.rows.forEach { row -> + val contributorField = row.fields.find { it.name == FIELD_KEY_CONTRIBUTOR } + assertNotNull(contributorField, "Each row should have a contributor field") + assertEquals(row.name, contributorField.value, "Contributor should match the team name") + } + } + + @Test + fun testCodebaseAnalyzerShouldHandleModuleAarNotBelongToBuildFolder() { + val project2Data = Project2Data() + val project2Analyzer = CodebaseAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project2Data.fakeDataPasser, + teamMapping = project2Data.teamMapping + ) + + val report = project2Analyzer.process() + assertEquals(expectedProject2Report, report) + } + + private val expectedProject1Report = Report( + id = "team", + name = "team", + rows = listOf( + Row( + name = "team2", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "team2"), + DefaultField(name = FIELD_KEY_SIZE, value = 107L) + ) + ), + Row( + name = "team1", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "team1"), + DefaultField(name = FIELD_KEY_SIZE, value = 103L) + ) + ) + ) + ) + + private val expectedProject2Report = Report( + id = "team", + name = "team", + rows = listOf( + Row( + name = "team2", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "team2"), + DefaultField(name = FIELD_KEY_SIZE, value = 107L) + ) + ), + Row( + name = "team1", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "team1"), + DefaultField(name = FIELD_KEY_SIZE, value = 103L) + ) + ) + ) + ) +} + +class Project2Data : Project1Data() { + override val moduleAar1: AarFileInfo + get() = super.moduleAar1.copy(path = "aar/moduleAar1.aar") + override val moduleAar2: AarFileInfo + get() = super.moduleAar2.copy(path = "aar/moduleAar2.aar") + override val moduleJar1: JarFileInfo + get() = super.moduleJar1.copy(path = "jar/moduleJar1.jar") + override val moduleJar2: JarFileInfo + get() = super.moduleJar2.copy(path = "jar/moduleJar2.jar") +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LargeFileAnalyzerTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LargeFileAnalyzerTest.kt new file mode 100644 index 0000000..794ba16 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LargeFileAnalyzerTest.kt @@ -0,0 +1,147 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.report.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +private const val THRESHOLD = 20L + +class LargeFileAnalyzerTest { + private val mapperComponent = MapperComponent() + private val project1Data = Project1Data() + private val analyzer = LargeFileAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project1Data.fakeDataPasser, + teamMapping = project1Data.teamMapping, + largeFileThreshold = THRESHOLD + ) + + @Test + fun testLargeFileAnalyzerWithProject1Data() { + val report = analyzer.process().sort() + assertEquals( + expectedProject1Report.sort(), + report + ) + } + + @Test + fun testLargeFileAnalyzerShouldReportCorrectNumberOfLargeFiles() { + val report = analyzer.process() + assertEquals("Should report 4 large files", 4, report.rows.size.toLong()) + } + + @Test + fun testLargeFileAnalyzerShouldReportCorrectFileNames() { + val report = analyzer.process() + val fileNames = report.rows.map { it.name }.toSet() + val expectedFileNames = + setOf("test_font.xml", "asset_resource_2.xml", "test_animator.xml", "asset_resource_3.xml") + assertEquals("Should report the correct file names", expectedFileNames, fileNames) + } + + @Test + fun testLargeFileAnalyzerShouldReportCorrectTeamOwnership() { + val report = analyzer.process() + val team1Files = + report.rows.filter { it.fields.find { field -> field.name == FIELD_KEY_OWNER }?.value == "team1" } + val team2Files = + report.rows.filter { it.fields.find { field -> field.name == FIELD_KEY_OWNER }?.value == "team2" } + + assertEquals("Team1 should own 2 large files", 2, team1Files.size.toLong()) + assertEquals("Team2 should own 2 large files", 2, team2Files.size.toLong()) + } + + @Test + fun testLargeFileAnalyzerShouldReportCorrectModuleTags() { + val report = analyzer.process() + val moduleAar1Files = + report.rows.filter { it.fields.find { field -> field.name == FIELD_KEY_TAG }?.value == "moduleAar1" } + val moduleAar2Files = + report.rows.filter { it.fields.find { field -> field.name == FIELD_KEY_TAG }?.value == "moduleAar2" } + + assertEquals("ModuleAar1 should contain 2 large files", 2, moduleAar1Files.size.toLong()) + assertEquals("ModuleAar2 should contain 2 large files", 2, moduleAar2Files.size.toLong()) + } + + @Test + fun testLargeFileAnalyzerShouldReportFileSizeLargerOrEqualToThreshold() { + val report = analyzer.process() + report.rows.forEach { row -> + val size = row.fields.find { it.name == FIELD_KEY_SIZE }?.value as? Long + assertTrue("All reported files should be at least 20 bytes", size != null && size >= THRESHOLD) + } + } + + + private val expectedProject1Report = Report( + id = "large_file", + name = "large_file", + rows = listOf( + Row( + name = "test_font.xml", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "test_font.xml"), + TagField(name = FIELD_KEY_OWNER, value = "team1"), + TagField(name = FIELD_KEY_TAG, value = "moduleAar1"), + DefaultField(name = FIELD_KEY_SIZE, value = 20L) + ) + ), + Row( + name = "asset_resource_2.xml", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "asset_resource_2.xml"), + TagField(name = FIELD_KEY_OWNER, value = "team1"), + TagField(name = FIELD_KEY_TAG, value = "moduleAar1"), + DefaultField(name = FIELD_KEY_SIZE, value = 20L) + ) + ), + Row( + name = "test_animator.xml", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "test_animator.xml"), + TagField(name = FIELD_KEY_OWNER, value = "team2"), + TagField(name = FIELD_KEY_TAG, value = "moduleAar2"), + DefaultField(name = FIELD_KEY_SIZE, value = 20L) + ) + ), + Row( + name = "asset_resource_3.xml", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "asset_resource_3.xml"), + TagField(name = FIELD_KEY_OWNER, value = "team2"), + TagField(name = FIELD_KEY_TAG, value = "moduleAar2"), + DefaultField(name = FIELD_KEY_SIZE, value = 30L) + ) + ) + ) + ) +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LibContentAnalyzerTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LibContentAnalyzerTest.kt new file mode 100644 index 0000000..cc4dd69 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LibContentAnalyzerTest.kt @@ -0,0 +1,123 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.google.gson.Gson +import com.grab.sizer.report.* +import org.junit.Assert.assertEquals +import org.junit.Test + +class LibContentAnalyzerTest { + private val mapperComponent = MapperComponent() + private val project1Data = Project1Data() + private val analyzer = LibContentAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project1Data.fakeDataPasser, + libName = project1Data.libAar1.name + ) + + @Test + fun testLibContentAnalyzerWithProject1Data() { + val report = analyzer.process().sort() + assertEquals(expectedProject1Report.sort(), report) + } + + @Test + fun testLibContentAnalyzerShouldReportCorrectNumberOfItems() { + val report = analyzer.process() + assertEquals("Should report 2 items in the library", 2, report.rows.size.toLong()) + } + + @Test + fun testLibContentAnalyzerShouldReportCorrectItemNames() { + val report = analyzer.process() + val itemNames = report.rows.map { it.name }.toSet() + val expectedNames = setOf("asset_resource_1.xml", "com.grab.test.HelloWorld") + assertEquals("Should report the correct item names", expectedNames, itemNames) + } + + @Test + fun testLibContentAnalyzerShouldReportCorrectItemTypes() { + val report = analyzer.process() + val assetItem = report.rows.find { it.name == "asset_resource_1.xml" } + val classItem = report.rows.find { it.name == "com.grab.test.HelloWorld" } + + assertEquals( + "asset_resource_1.xml should be tagged as Asset", + "Asset", + assetItem?.fields?.find { it.name == FIELD_KEY_TAG }?.value + ) + assertEquals( + "com.grab.test.HelloWorld should be tagged as Class", + "Class", + classItem?.fields?.find { it.name == FIELD_KEY_TAG }?.value + ) + } + + @Test + fun testLibContentAnalyzerShouldReportCorrectItemSizes() { + val report = analyzer.process() + val assetItem = report.rows.find { it.name == "asset_resource_1.xml" } + val classItem = report.rows.find { it.name == "com.grab.test.HelloWorld" } + + assertEquals( + "asset_resource_1.xml should have size 10", + 10L, + assetItem?.fields?.find { it.name == FIELD_KEY_SIZE }?.value + ) + assertEquals( + "com.grab.test.HelloWorld should have size 5", + 5L, + classItem?.fields?.find { it.name == FIELD_KEY_SIZE }?.value + ) + } + + + private val expectedProject1Report = Report( + id = "library_content", + name = "library_content", + rows = listOf( + Row( + name = "asset_resource_1.xml", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "asset_resource_1.xml"), + TagField(name = FIELD_KEY_TAG, value = "Asset"), + DefaultField(name = FIELD_KEY_SIZE, value = 10L) + ) + ), + Row( + name = "com.grab.test.HelloWorld", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "com.grab.test.HelloWorld"), + TagField(name = FIELD_KEY_TAG, value = "Class"), + DefaultField(name = FIELD_KEY_SIZE, value = 5L) + ) + ) + ) + ) +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LibrariesAnalyzerTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LibrariesAnalyzerTest.kt new file mode 100644 index 0000000..3dc8647 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/LibrariesAnalyzerTest.kt @@ -0,0 +1,101 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.report.* +import org.junit.Test +import kotlin.test.assertEquals + +class LibrariesAnalyzerTest { + private val mapperComponent = MapperComponent() + private val project1Data = Project1Data() + private val analyzer = LibrariesAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project1Data.fakeDataPasser + ) + + @Test + fun testApkAnalyzerWithProject1Data() { + val report = analyzer.process().sort() + assertEquals(expectedProject1Report.sort(), report) + } + + @Test + fun testLibrariesAnalyzerShouldReportCorrectNumberOfLibraries() { + val report = analyzer.process() + assertEquals(3, report.rows.size, "Should report 3 libraries") + } + + @Test + fun testLibrariesAnalyzerShouldReportCorrectLibraryNames() { + val report = analyzer.process() + val libraryNames = report.rows.map { it.name }.toSet() + val expectedNames = setOf("libJar", "libAar1", "libAar2") + assertEquals(expectedNames, libraryNames, "Should report the correct library names") + } + + @Test + fun testLibrariesAnalyzerShouldReportCorrectLibrarySizes() { + val report = analyzer.process() + val libJar = report.rows.find { it.name == "libJar" } + val libAar1 = report.rows.find { it.name == "libAar1" } + val libAar2 = report.rows.find { it.name == "libAar2" } + + assertEquals(7L, libJar?.fields?.find { it.name == FIELD_KEY_SIZE }?.value, "libJar should have size 7") + assertEquals(15L, libAar1?.fields?.find { it.name == FIELD_KEY_SIZE }?.value, "libAar1 should have size 15") + assertEquals(40L, libAar2?.fields?.find { it.name == FIELD_KEY_SIZE }?.value, "libAar2 should have size 40") + } + + private val expectedProject1Report = Report( + id = "library", + name = "library", + rows = listOf( + Row( + name = "libJar", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "libJar"), + DefaultField(name = FIELD_KEY_SIZE, value = 7L) + ) + ), + Row( + name = "libAar1", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "libAar1"), + DefaultField(name = FIELD_KEY_SIZE, value = 15L) + ) + ), + Row( + name = "libAar2", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "libAar2"), + DefaultField(name = FIELD_KEY_SIZE, value = 40L) + ) + ) + ) + ) +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/ModuleAnalyzerTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/ModuleAnalyzerTest.kt new file mode 100644 index 0000000..8892a4e --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/ModuleAnalyzerTest.kt @@ -0,0 +1,231 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.report.* +import org.junit.Assert.assertEquals +import org.junit.Test + +class ModuleAnalyzerTest { + private val mapperComponent = MapperComponent() + private val project1Data = Project1Data() + private val analyzer = ModuleAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project1Data.fakeDataPasser, + teamMapping = project1Data.teamMapping + ) + + @Test + fun testModuleAnalyzerWithProject1Data() { + val report = analyzer.process().sort() + assertEquals(expectedProject1Report.sort(), report) + } + + + @Test + fun testModuleAnalyzerShouldReportCorrectNumberOfModules() { + val report = analyzer.process() + assertEquals("Should report 5 modules", 5, report.rows.size) + } + + @Test + fun testModuleAnalyzerShouldReportCorrectModuleNames() { + val report = analyzer.process() + val moduleNames = report.rows.map { it.name }.toSet() + val expectedNames = setOf("moduleJar1", "moduleJar2", "app", "moduleAar2", "moduleAar1") + assertEquals("Should report the correct module names", expectedNames, moduleNames) + } + + @Test + fun testModuleAnalyzerShouldReportCorrectModuleSizes() { + val report = analyzer.process() + assertEquals( + "moduleJar1 should have size 13", + 13L, + report.rows.find { it.name == "moduleJar1" }?.fields?.find { it.name == FIELD_KEY_SIZE }?.value + ) + assertEquals( + "moduleJar2 should have size 18", + 18L, + report.rows.find { it.name == "moduleJar2" }?.fields?.find { it.name == FIELD_KEY_SIZE }?.value + ) + assertEquals( + "app should have size 20", + 20L, + report.rows.find { it.name == "app" }?.fields?.find { it.name == FIELD_KEY_SIZE }?.value + ) + assertEquals( + "moduleAar2 should have size 89", + 89L, + report.rows.find { it.name == "moduleAar2" }?.fields?.find { it.name == FIELD_KEY_SIZE }?.value + ) + assertEquals( + "moduleAar1 should have size 90", + 90L, + report.rows.find { it.name == "moduleAar1" }?.fields?.find { it.name == FIELD_KEY_SIZE }?.value + ) + } + + @Test + fun testModuleAnalyzerShouldReportCorrectTeamOwnership() { + val report = analyzer.process() + assertEquals( + "moduleJar1 should be owned by team1", + "team1", + report.rows.find { it.name == "moduleJar1" }?.fields?.find { it.name == FIELD_KEY_OWNER }?.value + ) + assertEquals( + "moduleJar2 should be owned by team2", + "team2", + report.rows.find { it.name == "moduleJar2" }?.fields?.find { it.name == FIELD_KEY_OWNER }?.value + ) + assertEquals( + "app should have no team ownership", + "NA", + report.rows.find { it.name == "app" }?.fields?.find { it.name == FIELD_KEY_OWNER }?.value + ) + assertEquals( + "moduleAar2 should be owned by team2", + "team2", + report.rows.find { it.name == "moduleAar2" }?.fields?.find { it.name == FIELD_KEY_OWNER }?.value + ) + assertEquals( + "moduleAar1 should be owned by team1", + "team1", + report.rows.find { it.name == "moduleAar1" }?.fields?.find { it.name == FIELD_KEY_OWNER }?.value + ) + } + + + @Test + fun testModuleAnalyzerShouldHandleModuleAarNotBelongToBuildFolder() { + val project2Data = Project2Data() + val analyzer = ModuleAnalyzer( + apkComponentProcessor = mapperComponent.apkComponentProcessor, + dataParser = project2Data.fakeDataPasser, + teamMapping = project2Data.teamMapping + ) + + val report = analyzer.process().sort() + assertEquals(expectedProject2Report.sort(), report) + } + + private val expectedProject1Report = Report( + id = "module", + name = "module", + rows = listOf( + Row( + name = "moduleJar1", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleJar1"), + TagField(name = FIELD_KEY_OWNER, value = "team1"), + DefaultField(name = FIELD_KEY_SIZE, value = 13L) + ) + ), + Row( + name = "moduleJar2", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleJar2"), + TagField(name = FIELD_KEY_OWNER, value = "team2"), + DefaultField(name = FIELD_KEY_SIZE, value = 18L) + ) + ), + Row( + name = "app", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "app"), + TagField(name = FIELD_KEY_OWNER, value = "NA"), + DefaultField(name = FIELD_KEY_SIZE, value = 20L) + ) + ), + Row( + name = "moduleAar2", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleAar2"), + TagField(name = FIELD_KEY_OWNER, value = "team2"), + DefaultField(name = FIELD_KEY_SIZE, value = 89L) + ) + ), + Row( + name = "moduleAar1", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleAar1"), + TagField(name = FIELD_KEY_OWNER, value = "team1"), + DefaultField(name = FIELD_KEY_SIZE, value = 90L) + ) + ) + ) + ) + + private val expectedProject2Report = Report( + id = "module", + name = "module", + rows = listOf( + Row( + name = "moduleJar1", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleJar1"), + TagField(name = FIELD_KEY_OWNER, value = "team1"), + DefaultField(name = FIELD_KEY_SIZE, value = 13L) + ) + ), + Row( + name = "moduleJar2", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleJar2"), + TagField(name = FIELD_KEY_OWNER, value = "team2"), + DefaultField(name = FIELD_KEY_SIZE, value = 18L) + ) + ), + Row( + name = "app", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "app"), + TagField(name = FIELD_KEY_OWNER, value = "NA"), + DefaultField(name = FIELD_KEY_SIZE, value = 20L) + ) + ), + Row( + name = "moduleAar2", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleAar2"), + TagField(name = FIELD_KEY_OWNER, value = "team2"), + DefaultField(name = FIELD_KEY_SIZE, value = 89L) + ) + ), + Row( + name = "moduleAar1", + fields = listOf( + TagField(name = FIELD_KEY_CONTRIBUTOR, value = "moduleAar1"), + TagField(name = FIELD_KEY_OWNER, value = "team1"), + DefaultField(name = FIELD_KEY_SIZE, value = 90L) + ) + ) + ) + ) +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/Project1Data.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/Project1Data.kt new file mode 100644 index 0000000..959c880 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/Project1Data.kt @@ -0,0 +1,191 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.createEmptyAar +import com.grab.sizer.analyzer.mapper.createEmptyApkInfo +import com.grab.sizer.analyzer.mapper.createEmptyJar +import com.grab.sizer.parser.DexFileInfo + +private const val TEAM_1 = "team1" +private const val TEAM_2 = "team2" + +open class Project1Data { + private object Assets { + val libAar1Asset1 = createRawFileInfo(path = "/assets/asset_resource_1.xml", downloadSize = 10, size = 30) + val moduleAar1Asset1 = createRawFileInfo(path = "/assets/asset_resource_2.xml", downloadSize = 20, size = 60) + val moduleAar2Asset1 = createRawFileInfo(path = "/assets/asset_resource_3.xml", downloadSize = 30, size = 90) + } + + private object Classes { + val libAar1Class1 = createClassFileInfo(name = "com.grab.test.HelloWorld", downloadSize = 5, size = 20) + val libJar1Class1 = createClassFileInfo(name = "com.grab.test.TestClass2", downloadSize = 7, size = 30) + val moduleAar2Class1 = createClassFileInfo(name = "com.grab.test.TestClass3", downloadSize = 9, size = 40) + val moduleJar1Class1 = createClassFileInfo(name = "com.grab.test.TestClass4", downloadSize = 13, size = 60) + val moduleJar2Class1 = createClassFileInfo(name = "com.grab.test.TestClass5", downloadSize = 18, size = 80) + } + + private object NativeLibs { + val libAar2NativeLib1 = createRawFileInfo(path = "/lib/armeabi-v7a/sample.so", downloadSize = 10, size = 20) + val moduleAar1NativeLib1 = + createRawFileInfo(path = "/lib/armeabi-v7a/sample2.so", downloadSize = 50, size = 100) + val moduleAar2NativeLib1 = createRawFileInfo(path = "/lib/armeabi-v7a/sample3.so", downloadSize = 30, size = 90) + } + + private object Resources { + val libAar2Resource1 = + createRawFileInfo(path = "/res/drawable-xlarge-port-hdpi-v4/ic_test.xml", downloadSize = 30, size = 90) + val moduleAar2Resource1 = + createRawFileInfo(path = "/res/animator/test_animator.xml", downloadSize = 20, size = 40) + val moduleAar1Resource1 = createRawFileInfo(path = "/res/font/test_font.xml", downloadSize = 20, size = 40) + } + + private object Others { + val moduleJar2Other1 = + createRawFileInfo(path = "play-services-detection.properties", downloadSize = 10, size = 20) + val moduleAar1Other1 = createRawFileInfo(path = "build-data.properties", downloadSize = 10, size = 30) + } + + private val dexFile = DexFileInfo( + name = "dex1", + downloadSize = 52, + classes = setOf( + Classes.libAar1Class1, + Classes.libJar1Class1, + Classes.moduleAar2Class1, + Classes.moduleJar1Class1, + Classes.moduleJar2Class1 + ), + size = 200 + ) + + /** + * Total download size 292 bytes + */ + private val apk = createEmptyApkInfo("apk1").copy( + assets = setOf(Assets.libAar1Asset1, Assets.moduleAar1Asset1, Assets.moduleAar2Asset1), // 60b + dexes = setOf(dexFile), // 52b + nativeLibs = setOf( + NativeLibs.libAar2NativeLib1, + NativeLibs.moduleAar1NativeLib1, + NativeLibs.moduleAar2NativeLib1 + ), // 90b + resources = setOf( + Resources.libAar2Resource1, + Resources.moduleAar1Resource1, + Resources.moduleAar2Resource1 + ), // 70b + others = setOf(Others.moduleJar2Other1, Others.moduleAar1Other1) // 20b + ) + + /** + * Total download size 15 bytes + */ + val libAar1 = createEmptyAar("libAar1").copy( + assets = setOf(Assets.libAar1Asset1), + jars = setOf(createEmptyJar("jar").copy(classes = setOf(Classes.libAar1Class1))) + ) + + /** + * Total download size 40 bytes + */ + val libAar2 = createEmptyAar("libAar2").copy( + resources = setOf(Resources.libAar2Resource1), + nativeLibs = setOf(NativeLibs.libAar2NativeLib1) + ) + + /** + * Total download size 7 bytes + */ + val libJar1 = createEmptyJar("libJar").copy( + classes = setOf(Classes.libJar1Class1) + ) + + /** + * Total download size 90 bytes + * Resources: 20 bytes + * Assets: 20 bytes + */ + open val moduleAar1 = createEmptyAar(name = "moduleAar1", path = "moduleAar1/build/outputs/aar").copy( + nativeLibs = setOf(NativeLibs.moduleAar1NativeLib1), + resources = setOf(Resources.moduleAar1Resource1), + assets = setOf(Assets.moduleAar1Asset1), + others = setOf(Others.moduleAar1Other1) + ) + + /** + * Total download size 89 byte + * Codebase: 9 byte + * Resources: 20 bytes + * Assets: 30 bytes + */ + open val moduleAar2 = createEmptyAar(name = "moduleAar2", path = "moduleAar2/build/outputs/aar").copy( + nativeLibs = setOf(NativeLibs.moduleAar2NativeLib1), + assets = setOf(Assets.moduleAar2Asset1), + resources = setOf(Resources.moduleAar2Resource1), + jars = setOf(createEmptyJar("jar").copy(classes = setOf(Classes.moduleAar2Class1))) + ) + + /** + * Total download size 13 byte + * Codebase: 13 byte + */ + open val moduleJar1 = createEmptyJar(name = "moduleJar1", path = "moduleJar1/build/libs").copy( + classes = setOf(Classes.moduleJar1Class1) + ) + + /** + * Total download size 28 byte + * Codebase: 18 byte + */ + open val moduleJar2 = createEmptyJar(name = "moduleJar2", path = "moduleJar2/build/libs").copy( + classes = setOf(Classes.moduleJar2Class1), + others = setOf(Others.moduleJar2Other1) + ) + + val fakeDataPasser = FakeDataPasser( + apks = mutableSetOf(apk), + libAars = mutableSetOf(libAar1, libAar2), + libJars = mutableSetOf(libJar1), + moduleAars = mutableSetOf(moduleAar1, moduleAar2), + moduleJars = mutableSetOf(moduleJar1, moduleJar2) + ) + + val teamMapping = object : TeamMapping { + override val teamToModuleMap: Map> = mapOf( + TEAM_1 to listOf(moduleAar1.name, moduleJar1.name), + TEAM_2 to listOf(moduleAar2.name, moduleJar2.name), + ) + override val moduleToTeamMap: Map = mapOf( + moduleAar1.name to TEAM_1, + moduleJar1.name to TEAM_1, + moduleAar2.name to TEAM_2, + moduleJar2.name to TEAM_2 + ) + } +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/Utils.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/Utils.kt new file mode 100644 index 0000000..4200d40 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/Utils.kt @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer + +import com.grab.sizer.analyzer.mapper.* +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.analyzer.model.RawFileInfo +import com.grab.sizer.parser.* + +class FakeDataPasser( + override val apks: MutableSet = mutableSetOf(), + override val libAars: MutableSet = mutableSetOf(), + override val libJars: MutableSet = mutableSetOf(), + override val moduleAars: MutableSet = mutableSetOf(), + override val moduleJars: MutableSet = mutableSetOf() +) : DataParser + + +internal class MapperComponent { + private val mapOfComponentMapper = mapOf( + ResourceComponentMapper::class to ResourceComponentMapper(), + NativeLibComponentMapper::class to NativeLibComponentMapper(), + AssetComponentMapper::class to AssetComponentMapper(), + ClassComponentMapper::class to ClassComponentMapper(), + OtherComponentMapper::class to OtherComponentMapper(), + ) + + val apkComponentProcessor = DefaultApkComponentProcessor(mapOfComponentMapper.mapKeys { (k, _) -> k.java }) +} + + +internal fun createRawFileInfo(path: String, downloadSize: Long = 50, size: Long = 150) = + RawFileInfo(path = path, downloadSize = downloadSize, size = size) + +internal fun createEmptyDexFileInfo(name: String = "dex"): DexFileInfo = DexFileInfo( + name = name, + downloadSize = 100, + size = 200, + classes = emptySet(), +) + +internal fun createClassFileInfo(name: String, downloadSize: Long = 100, size: Long = 300) = + ClassFileInfo(name = name, downloadSize = downloadSize, size = size) \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/YmlTeamMappingTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/YmlTeamMappingTest.kt new file mode 100644 index 0000000..d283506 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/YmlTeamMappingTest.kt @@ -0,0 +1,105 @@ +package com.grab.sizer.analyzer + +import org.junit.Test +import org.junit.Assert.* +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File + +class YmlTeamMappingTest { + + @get:Rule + val tempFolder = TemporaryFolder() + + @Test + fun testTeamToModuleMapping() { + val ymlContent = """ + Team1: + - :module1 + - :group1:module2 + Team2: + - :module3 + - :group2:module4 + """.trimIndent() + + val ymlFile = tempFolder.newFile("team_mapping.yml").apply { + writeText(ymlContent) + } + + val teamMapping = YmlTeamMapping(ymlFile) + + assertEquals(2, teamMapping.teamToModuleMap.size) + assertEquals(listOf("module1", "group1:module2"), teamMapping.teamToModuleMap["Team1"]) + assertEquals(listOf("module3", "group2:module4"), teamMapping.teamToModuleMap["Team2"]) + } + + @Test + fun testModuleToTeamMapping() { + val ymlContent = """ + Team1: + - :module1 + - :group1:module2 + Team2: + - :module3 + - :group2:module4 + """.trimIndent() + + val ymlFile = tempFolder.newFile("team_mapping.yml").apply { + writeText(ymlContent) + } + + val teamMapping = YmlTeamMapping(ymlFile) + + assertEquals(4, teamMapping.moduleToTeamMap.size) + assertEquals("Team1", teamMapping.moduleToTeamMap["module1"]) + assertEquals("Team1", teamMapping.moduleToTeamMap["group1:module2"]) + assertEquals("Team2", teamMapping.moduleToTeamMap["module3"]) + assertEquals("Team2", teamMapping.moduleToTeamMap["group2:module4"]) + } + + @Test + fun testYamlFileWithNoModules() { + val ymlContent = """ + Team1: + Team2: + """.trimIndent() + + val ymlFile = tempFolder.newFile("no_modules_mapping.yml").apply { + writeText(ymlContent) + } + + val teamMapping = YmlTeamMapping(ymlFile) + + assertEquals(2, teamMapping.teamToModuleMap.size) + assertTrue(teamMapping.teamToModuleMap["Team1"]?.isEmpty() ?: false) + assertTrue(teamMapping.teamToModuleMap["Team2"]?.isEmpty() ?: false) + assertTrue(teamMapping.moduleToTeamMap.isEmpty()) + } + + @Test + fun testTrimmingOfColonInModuleNames() { + val ymlContent = """ + Team1: + - :module1 + - :group1:module2 + Team2: + - :module3 + - :group2:module4 + """.trimIndent() + + val ymlFile = tempFolder.newFile("trim_colon_mapping.yml").apply { + writeText(ymlContent) + } + + val teamMapping = YmlTeamMapping(ymlFile) + assertEquals(listOf("module1", "group1:module2"), teamMapping.teamToModuleMap["Team1"]) + assertEquals("Team1", teamMapping.moduleToTeamMap["module1"]) + assertEquals("Team1", teamMapping.moduleToTeamMap["group1:module2"]) + } + + @Test(expected = Exception::class) + fun testNonExistentFileThrowsException() { + val nonExistentFile = File("non_existent_file.yml") + YmlTeamMapping(nonExistentFile).teamToModuleMap + } +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/AssetComponentMapperTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/AssetComponentMapperTest.kt new file mode 100644 index 0000000..3b07696 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/AssetComponentMapperTest.kt @@ -0,0 +1,109 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.createRawFileInfo +import com.grab.sizer.parser.AarFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.JarFileInfo +import org.junit.Assert +import org.junit.Test + +class AssetComponentMapperTest { + private val asset1 = createRawFileInfo(path = "/assets/asset_resource_1.xml") + private val asset2 = createRawFileInfo(path = "/assets/asset_resource_2.xml") + private val asset3 = createRawFileInfo(path = "/assets/asset_resource_3.xml") + private val noOwnerAsset = createRawFileInfo(path = "/assets/asset_resource_5.xml") + + @Test + fun assetsAnalyzerShouldResultNoOwnerAssetProperly() { + val apk = createEmptyApkInfo().copy( + assets = setOf(asset1.copy(), asset2.copy(), noOwnerAsset.copy()) + ) + val aar1 = createEmptyAar("aar1").copy(assets = setOf(asset1.copy())) + val aar2 = createEmptyAar("aar2").copy(assets = setOf(asset2.copy(), asset3.copy())) + val aar3 = createEmptyAar("aar3") + val jar1 = createEmptyJar("jar1") + val result = AssetComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2, aar3), setOf(jar1)) + } + Assert.assertTrue(result.noOwnerData.contains(noOwnerAsset)) + Assert.assertFalse(result.noOwnerData.contains(asset1)) + } + + @Test + fun assetsAnalyzerShouldResultTheProperAssetFilesContributedToApk() { + val apk = createEmptyApkInfo().copy( + assets = setOf(asset1.copy(), asset2.copy(), noOwnerAsset.copy()) + ) + val aar1 = createEmptyAar("aar1").copy(assets = setOf(asset1.copy())) + val aar2 = createEmptyAar("aar2").copy(assets = setOf(asset2.copy(), asset3.copy())) + val aar3 = createEmptyAar("aar3") + val jar1 = createEmptyJar("jar1") + val result = AssetComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2, aar3), setOf(jar1)) + } + + Assert.assertFalse(result.contributors.containsKey(aar3)) + Assert.assertFalse(result.contributors.containsKey(jar1)) + + Assert.assertEquals(1, result.contributors[aar2]?.size) + Assert.assertEquals(asset2, result.contributors[aar2]?.first()) + Assert.assertEquals(asset1, result.contributors[aar1]?.first()) + } +} + +internal fun createEmptyJar(name: String = "jar", path: String = "jar/path"): JarFileInfo = JarFileInfo( + name = name, + path = "$path/$name", + classes = emptySet(), + nativeLibs = emptySet(), + others = emptySet(), + tag = name +) + +internal fun createEmptyAar(name: String = "aar", path: String = "aar/path"): AarFileInfo = AarFileInfo( + name = name, + path = "$path/$name.aar", + resources = emptySet(), + nativeLibs = emptySet(), + assets = emptySet(), + others = emptySet(), + jars = emptySet(), + tag = name +) + +internal fun createEmptyApkInfo(name: String = "apk1"): ApkFileInfo = + ApkFileInfo( + name = name, + resources = emptySet(), + nativeLibs = emptySet(), + others = emptySet(), + dexes = emptySet(), + assets = emptySet(), + ) \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/ClassComponentMapperTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/ClassComponentMapperTest.kt new file mode 100644 index 0000000..a82d01b --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/ClassComponentMapperTest.kt @@ -0,0 +1,198 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.createClassFileInfo +import com.grab.sizer.analyzer.createEmptyDexFileInfo +import com.grab.sizer.analyzer.model.ClassFileInfo +import com.grab.sizer.parser.ApkFileInfo +import com.grab.sizer.parser.DexFileInfo +import org.junit.Assert +import org.junit.Test + +class ClassComponentMapperTest { + private val class1 = createClassFileInfo(name = "com.grab.test.HelloWorld") + private val class2 = createClassFileInfo(name = "com.grab.test.TestClass2") + private val class3 = createClassFileInfo(name = "com.grab.test.TestClass3") + private val noOwnerClass = createClassFileInfo(name = "com.grab.test.TestClassNoOwner") + + @Test + fun classMapperShouldHandleGeneratedInnerLambda() { + val lambdaClassInApk1 = createClassFileInfo( + name = "androidx.appcompat.app.AppCompatDelegateImpl\$Api24Impl" + ) + val lambdaClassInApk2 = createClassFileInfo( + name = "androidx.appcompat.app.AppCompatDelegate\$\$ExternalSyntheticLambda0" + ) + + val dex1 = createEmptyDexFileInfo().copy( + classes = setOf(lambdaClassInApk1, lambdaClassInApk2) + ) + val apk = createEmptyApkInfo().copy(dexes = setOf(dex1)) + + val classInAar1 = createClassFileInfo(name = "androidx.appcompat.app.AppCompatDelegateImpl") + val classInAar2 = createClassFileInfo(name = "androidx.appcompat.app.AppCompatDelegate") + val classInAar3 = createClassFileInfo(name = "androidx.appcompat.app.AppLocalesStorageHelper") + val aar = createEmptyAar("aar").copy( + jars = setOf( + createEmptyJar().copy( + classes = setOf(classInAar1, classInAar2, classInAar3) + ) + ) + ) + + val result = ClassComponentMapper().run { + setOf(apk).mapTo(setOf(aar), emptySet()) + } + Assert.assertEquals(0, result.noOwnerData.size) + Assert.assertTrue(result.contributors.containsKey(aar)) + Assert.assertEquals(true, result.contributors[aar]?.contains(lambdaClassInApk1)) + Assert.assertEquals(true, result.contributors[aar]?.contains(lambdaClassInApk2)) + } + + @Test + fun classMapperShouldHandleGeneratedLambdaProperly() { + val lambdaClassInApk = createClassFileInfo( + name = "androidx.core.widget.-\$\$Lambda\$ContentLoadingProgressBar\$aW9csiS0dCdsR2nrqov9CuXAmGo" + ) + val dex1 = createEmptyDexFileInfo().copy( + classes = setOf(lambdaClassInApk) + ) + val apk = createEmptyApkInfo().copy(dexes = setOf(dex1)) + + val classInAar = createClassFileInfo(name = "androidx.core.widget.ContentLoadingProgressBar") + val aar = createEmptyAar("aar").copy( + jars = setOf( + createEmptyJar().copy( + classes = setOf(classInAar) + ) + ) + ) + + val result = ClassComponentMapper().run { + setOf(apk).mapTo(setOf(aar), emptySet()) + } + Assert.assertEquals(0, result.noOwnerData.size) + Assert.assertTrue(result.contributors.containsKey(aar)) + Assert.assertEquals(true, result.contributors[aar]?.contains(lambdaClassInApk)) + } + + @Test + fun classMapperShouldResultNoOwnerClassProperly() { + val apk = createApkWithClass1Class2AndNoOwner() + val aar1 = createAarWithClass1AndClass3() + val jar1 = createEmptyJar().copy(classes = setOf(class2)) + val result = ClassComponentMapper().run { + setOf(apk).mapTo(setOf(aar1), setOf(jar1)) + } + Assert.assertEquals(1, result.noOwnerData.size) + Assert.assertEquals(noOwnerClass, result.noOwnerData.first()) + } + + private fun createAarWithClass1AndClass3() = createEmptyAar("Class1AndClass3").copy( + jars = setOf( + createEmptyJar().copy( + classes = setOf(class1, class3) + ) + ) + ) + + private fun createApkWithClass1Class2AndNoOwner(): ApkFileInfo { + val dex1 = createEmptyDexFileInfo().copy( + classes = setOf(class1) + ) + val dex2 = createEmptyDexFileInfo().copy( + classes = setOf(class2, noOwnerClass) + ) + return createEmptyApkInfo().copy( + dexes = setOf(dex1, dex2) + ) + } + + @Test + fun classMapperShouldResultTheProperBinaryContributedToApk() { + val apk = createApkWithClass1Class2AndNoOwner() + val aar1 = createEmptyAar("aar1").copy( + jars = setOf( + createEmptyJar().copy( + classes = setOf(class1) + ) + ) + ) + val aar2 = createEmptyAar("aar2").copy( + jars = setOf( + createEmptyJar().copy( + classes = setOf(class3) + ) + ) + ) + val jar1 = createEmptyJar().copy( + classes = setOf(class2) + ) + + val result = ClassComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), setOf(jar1)) + } + + Assert.assertEquals(2, result.contributors.size) + Assert.assertTrue(result.contributors.containsKey(aar1)) + Assert.assertTrue(result.contributors.containsKey(jar1)) + Assert.assertFalse(result.contributors.containsKey(aar2)) + } + + @Test + fun classMapperShouldResultTheCorrectClassesContributedToApk() { + val apk = createApkWithClass1Class2AndNoOwner() + val aar1 = createEmptyAar("aar1").copy( + jars = setOf( + createEmptyJar().copy( + classes = setOf(class1) + ) + ) + ) + val aar2 = createEmptyAar("aar2").copy( + jars = setOf( + createEmptyJar().copy( + classes = setOf(class3) + ) + ) + ) + val jar1 = createEmptyJar("jar1").copy( + classes = setOf(class2) + ) + + val result = ClassComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), setOf(jar1)) + } + + Assert.assertEquals(2, result.contributors.size) + Assert.assertEquals(true, result.contributors[aar1]?.contains(class1)) + Assert.assertEquals(true, result.contributors[jar1]?.contains(class2)) + Assert.assertNull(result.contributors[aar2]) + } +} diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/NativeLibComponentMapperTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/NativeLibComponentMapperTest.kt new file mode 100644 index 0000000..c8c0814 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/NativeLibComponentMapperTest.kt @@ -0,0 +1,126 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.createRawFileInfo +import org.junit.Assert +import org.junit.Test + +class NativeLibComponentMapperTest { + private val nativeLib1 = createRawFileInfo(path = "/lib/armeabi-v7a/sample.so") + private val nativeLib2 = createRawFileInfo(path = "/lib/armeabi-v7a/sample2.so") + private val nativeLib3 = createRawFileInfo(path = "/lib/armeabi-v7a/sample3.so") + private val nativeLib1Aar = createRawFileInfo(path = "/jni/armeabi-v7a/sample.so") + private val noOwnerLib = createRawFileInfo(path = "/lib/armeabi-v7a/noOwnerLib.so") + + @Test + fun nativeLibMapperShouldResultProperContributors() { + val apk = createEmptyApkInfo().copy( + nativeLibs = setOf(nativeLib3.copy(), nativeLib2.copy(), noOwnerLib.copy()) + ) + + val aar1 = createEmptyAar("aar1").copy( + nativeLibs = setOf(nativeLib2.copy()) // path is different + ) + + val aar2 = createEmptyAar("aar2").copy(nativeLibs = setOf(nativeLib3.copy())) + + val result = NativeLibComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), emptySet()) + } + Assert.assertEquals(2, result.contributors.size) + Assert.assertNotNull(result.contributors[aar1]) + Assert.assertNotNull(result.contributors[aar2]) + Assert.assertEquals(nativeLib2, result.contributors[aar1]?.first()) + Assert.assertEquals(nativeLib3, result.contributors[aar2]?.first()) + } + + @Test + fun nativeLibMapperShouldHandleDiffOnPathBetweenApkAndJar() { + /** + * There are different between APK and AAR native file path. + * This method will remove the pre-fix path for the so file, to ensure the mapping working as expected + * Example, + * APK: /lib/armeabi-v7a/sample.so + * AAR: /jni/armeabi-v7a/sample.so + */ + val apk = createEmptyApkInfo().copy( + nativeLibs = setOf(nativeLib1.copy(), nativeLib2.copy(), noOwnerLib.copy()) + ) + + val aar1 = createEmptyAar("aar1").copy( + nativeLibs = setOf(nativeLib1Aar.copy()) // path is different with nativeLib1 + ) + + val aar2 = createEmptyAar("aar2").copy(nativeLibs = setOf(nativeLib3.copy())) + + val result = NativeLibComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), emptySet()) + } + Assert.assertEquals(1, result.contributors.size) + Assert.assertNotNull(result.contributors[aar1]) + Assert.assertEquals(nativeLib1, result.contributors[aar1]?.first()) + } + + @Test + fun nativeLibMapperShouldNotResultNoUseNativeLib() { + val apk = createEmptyApkInfo().copy( + nativeLibs = setOf(nativeLib1.copy(), nativeLib2.copy(), noOwnerLib.copy()) + ) + + val aar1 = createEmptyAar("aar1").copy( + nativeLibs = setOf(nativeLib1Aar.copy()) // path is different + ) + + val aar2 = createEmptyAar("aar2").copy(nativeLibs = setOf(nativeLib3.copy())) + + val result = NativeLibComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), emptySet()) + } + Assert.assertNull(result.contributors[aar2]) + } + + @Test + fun nativeLibMapperShouldResultProperNoOwnerLip() { + val apk = createEmptyApkInfo().copy( + nativeLibs = setOf(nativeLib1.copy(), noOwnerLib.copy()) + ) + + val aar1 = createEmptyAar("aar1").copy( + nativeLibs = setOf(nativeLib1Aar.copy()) // path is different + ) + + val aar2 = createEmptyAar("aar2").copy(nativeLibs = setOf(nativeLib3.copy())) + + val result = NativeLibComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), emptySet()) + } + Assert.assertEquals(1, result.noOwnerData.size) + Assert.assertEquals(noOwnerLib, result.noOwnerData.first()) + } +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/ResourceComponentMapperTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/ResourceComponentMapperTest.kt new file mode 100644 index 0000000..6e4a27e --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/analyzer/mapper/ResourceComponentMapperTest.kt @@ -0,0 +1,202 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.analyzer.mapper + +import com.grab.sizer.analyzer.createRawFileInfo +import com.grab.sizer.analyzer.model.RawFileInfo +import org.junit.Assert +import org.junit.Test + +class ResourceComponentMapperTest { + private val resourceDrawable = createRawFileInfo(path = "/res/drawable-xlarge-port-hdpi-v4/ic_test.xml") + private val resourceAnimation = createRawFileInfo(path = "/res/animator/test_animator.xml") + private val resourceFont = createRawFileInfo(path = "/res/font/test_font.xml") + private val noOwnerResource = createRawFileInfo(path = "/res/layout/test_layout.xml") + + @Test + fun resourceMapperShouldResultProperContributors() { + val apk = createEmptyApkInfo().copy( + resources = setOf( + resourceDrawable.copy(), + resourceAnimation.copy(), + resourceFont.copy(), + noOwnerResource.copy() + ) + ) + + val aar1 = createEmptyAar("aar1").copy( + resources = setOf(resourceDrawable.copy(), resourceAnimation.copy()) + ) + + val aar2 = createEmptyAar("aar2").copy( + resources = setOf( + resourceFont.copy() + ) + ) + + val result = ResourceComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), emptySet()) + } + Assert.assertEquals(2, result.contributors.size) + Assert.assertEquals(true, result.contributors[aar1]?.contains(resourceDrawable)) + Assert.assertEquals(true, result.contributors[aar1]?.contains(resourceAnimation)) + Assert.assertEquals(true, result.contributors[aar2]?.contains(resourceFont)) + } + + @Test + fun resourceMapperShouldFlagOutNoOwnerItem() { + val apk = createEmptyApkInfo().copy( + resources = setOf( + resourceDrawable.copy(), + resourceAnimation.copy(), + resourceFont.copy(), + noOwnerResource.copy() + ) + ) + + val aar1 = createEmptyAar("aar1").copy( + resources = setOf(resourceDrawable.copy(), resourceAnimation.copy()) + ) + + val aar2 = createEmptyAar("aar2").copy( + resources = setOf( + resourceFont.copy() + ) + ) + + val result = ResourceComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), emptySet()) + } + Assert.assertEquals(1, result.noOwnerData.size) + Assert.assertEquals(noOwnerResource, result.noOwnerData.first()) + } + + @Test + fun resourceMapperShouldNotIncludeNoUseResource() { + val apk = createEmptyApkInfo().copy( + resources = setOf( + resourceDrawable.copy(), + noOwnerResource.copy(), + resourceFont.copy() + ) + ) + + val aar1 = createEmptyAar("aar1").copy( + resources = setOf(resourceDrawable.copy(), resourceAnimation.copy()) + ) + + val aar2 = createEmptyAar("aar2").copy( + resources = setOf( + resourceFont.copy() + ) + ) + + val result = ResourceComponentMapper().run { + setOf(apk).mapTo(setOf(aar1, aar2), emptySet()) + } + Assert.assertEquals(1, result.contributors[aar1]?.size) + Assert.assertEquals(false, result.contributors[aar1]?.contains(resourceAnimation)) + } + + @Test + fun classMapperShouldHandleDiffOnPathWithSpecialCharacter() { + /** + * There are cases the resources files are renamed, not sure why and how. + * Here is an example: "/res/drawable/$bg_network_error__0.xml" + */ + val apkResourceWithSpecialChar = createRawFileInfo(path = "/res/drawable/\$bg_network_error__0.xml") + + val apk = createEmptyApkInfo().copy( + resources = setOf(apkResourceWithSpecialChar, resourceDrawable) + ) + + val aarResource = createRawFileInfo(path = "/res/drawable/bg_network_error.xml") + + val aar1 = createEmptyAar().copy( + resources = setOf(aarResource) + ) + + val result = ResourceComponentMapper().run { + setOf(apk).mapTo(setOf(aar1), emptySet()) + } + Assert.assertEquals(1, result.contributors.size) + Assert.assertNotNull(result.contributors[aar1]) + Assert.assertEquals(apkResourceWithSpecialChar, result.contributors[aar1]?.first()) + } + + @Test + fun classMapperShouldHandleDiffOnPathMinSupportSdkVersion() { + /** + * There are cases the resource directory were added with the min support sdk version + * Ex : /res/drawable-v22/ic_geo_pickup_notes.xml + */ + val apkResourceWithSpecialChar = createRawFileInfo(path = "/res/drawable-v22/ic_geo_pickup_notes.xml") + + val apk = createEmptyApkInfo().copy( + resources = setOf(apkResourceWithSpecialChar, resourceDrawable) + ) + + val aarResource = createRawFileInfo(path = "/res/drawable/ic_geo_pickup_notes.xml") + + val aar1 = createEmptyAar().copy( + resources = setOf(aarResource) + ) + + val result = ResourceComponentMapper().run { + setOf(apk).mapTo(setOf(aar1), emptySet()) + } + Assert.assertEquals(1, result.contributors.size) + Assert.assertNotNull(result.contributors[aar1]) + Assert.assertEquals(apkResourceWithSpecialChar, result.contributors[aar1]?.first()) + } + + @Test + fun classMapperShouldHandleDiffOnPathForAllCases() { + /** + * Handle both the $ sign and version extension + */ + val apkResourceWithSpecialChar = createRawFileInfo(path = "/res/drawable-v22/\$bg_network_error__0.xml") + + val apk = createEmptyApkInfo().copy( + resources = setOf(apkResourceWithSpecialChar, resourceDrawable.copy()) + ) + + val aarResource = createRawFileInfo(path = "/res/drawable/bg_network_error.xml") + + val aar1 = createEmptyAar().copy( + resources = setOf(aarResource) + ) + + val result = ResourceComponentMapper().run { + setOf(apk).mapTo(setOf(aar1), emptySet()) + } + Assert.assertEquals(1, result.contributors.size) + Assert.assertNotNull(result.contributors[aar1]) + Assert.assertEquals(apkResourceWithSpecialChar, result.contributors[aar1]?.first()) + } +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultAarFileParserTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultAarFileParserTest.kt new file mode 100644 index 0000000..4f4b3e4 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultAarFileParserTest.kt @@ -0,0 +1,174 @@ +package com.grab.sizer.parser + +import com.grab.sizer.utils.SizerInputFile +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.InputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class DefaultAarFileParserTest { + + @get:Rule + val tempFolder = TemporaryFolder() + + private lateinit var mockJarParser: MockJarStreamParser + private lateinit var aarFileParser: DefaultAarFileParser + + @Before + fun setup() { + mockJarParser = MockJarStreamParser() + aarFileParser = DefaultAarFileParser(mockJarParser) + } + + @Test + fun parseAarsShouldCorrectlyParseResourceFiles() { + val aar = createTestAar("test.aar", listOf("res/layout/activity_main.xml", "res/values/strings.xml")) + val result = aarFileParser.parseAars(sequenceOf(aar)) + + assertEquals(1, result.size) + val aarInfo = result.first() + assertEquals(2, aarInfo.resources.size) + assertTrue(aarInfo.resources.any { it.path == "/res/layout/activity_main.xml" }) + assertTrue(aarInfo.resources.any { it.path == "/res/values/strings.xml" }) + } + + @Test + fun parseAarsShouldCorrectlyParseAssetFiles() { + val aar = createTestAar("test.aar", listOf("assets/fonts/roboto.ttf", "assets/images/logo.png")) + val result = aarFileParser.parseAars(sequenceOf(aar)) + + assertEquals(1, result.size) + val aarInfo = result.first() + assertEquals(2, aarInfo.assets.size) + assertTrue(aarInfo.assets.any { it.path == "/assets/fonts/roboto.ttf" }) + assertTrue(aarInfo.assets.any { it.path == "/assets/images/logo.png" }) + } + + @Test + fun parseAarsShouldCorrectlyParseNativeLibraries() { + val aar = createTestAar("test.aar", listOf("jni/x86/libtest.so", "jni/arm64-v8a/libtest.so")) + val result = aarFileParser.parseAars(sequenceOf(aar)) + + assertEquals(1, result.size) + val aarInfo = result.first() + assertEquals(2, aarInfo.nativeLibs.size) + assertTrue(aarInfo.nativeLibs.any { it.path == "/jni/x86/libtest.so" }) + assertTrue(aarInfo.nativeLibs.any { it.path == "/jni/arm64-v8a/libtest.so" }) + } + + @Test + fun parseAarsShouldCorrectlyParseJarFiles() { + val jarEntry = "libs/example.jar" + mockJarParser.mockJarInfo = + JarFileInfo("example.jar", "libs/example.jar", "", emptySet(), emptySet(), emptySet()) + + val aar = createTestAar("test.aar", listOf(jarEntry)) + val result = aarFileParser.parseAars(sequenceOf(aar)) + + assertEquals(1, result.size) + val aarInfo = result.first() + assertEquals(1, aarInfo.jars.size) + assertEquals("example.jar", aarInfo.jars.first().name) + assertEquals(jarEntry, mockJarParser.lastParsedEntry?.name) + } + + @Test + fun parseAarsShouldCorrectlyParseOtherFiles() { + val aar = createTestAar("test.aar", listOf("META-INF/MANIFEST.MF", "proguard.txt")) + val result = aarFileParser.parseAars(sequenceOf(aar)) + + assertEquals(1, result.size) + val aarInfo = result.first() + assertEquals(2, aarInfo.others.size) + assertTrue(aarInfo.others.any { it.path == "/META-INF/MANIFEST.MF" }) + assertTrue(aarInfo.others.any { it.path == "/proguard.txt" }) + } + + @Test + fun parseAarsShouldCorrectlyHandleMixedContent() { + val aar = createTestAar( + "test.aar", listOf( + "res/layout/activity_main.xml", + "assets/fonts/roboto.ttf", + "jni/x86/libtest.so", + "libs/example.jar", + "proguard.txt" + ) + ) + + mockJarParser.mockJarInfo = + JarFileInfo("example.jar", "libs/example.jar", "", emptySet(), emptySet(), emptySet()) + + val result = aarFileParser.parseAars(sequenceOf(aar)) + + assertEquals(1, result.size) + val aarInfo = result.first() + assertEquals(1, aarInfo.resources.size) + assertEquals(1, aarInfo.assets.size) + assertEquals(1, aarInfo.nativeLibs.size) + assertEquals(1, aarInfo.jars.size) + assertEquals(1, aarInfo.others.size) + } + + @Test + fun parseAarsShouldCorrectlyParseMultipleAars() { + val aar1 = createTestAar("test1.aar", listOf("res/layout/activity_main.xml")) + val aar2 = createTestAar("test2.aar", listOf("jni/x86/libtest.so")) + val result = aarFileParser.parseAars(sequenceOf(aar1, aar2)) + + assertEquals(2, result.size) + val aarInfo1 = result.find { it.name == "test1.aar" } + val aarInfo2 = result.find { it.name == "test2.aar" } + + assertNotNull(aarInfo1) + assertEquals(1, aarInfo1!!.resources.size) + + assertNotNull(aarInfo2) + assertEquals(1, aarInfo2!!.nativeLibs.size) + } + + @Test + fun parseAarsShouldHandleEmptyAarFiles() { + val emptyAar = createTestAar("empty.aar", listOf()) + val result = aarFileParser.parseAars(sequenceOf(emptyAar)) + + assertEquals(1, result.size) + val aarInfo = result.first() + assertEquals("empty.aar", aarInfo.name) + assertEquals("empty", aarInfo.tag) + assertTrue(aarInfo.resources.isEmpty()) + assertTrue(aarInfo.assets.isEmpty()) + assertTrue(aarInfo.nativeLibs.isEmpty()) + assertTrue(aarInfo.jars.isEmpty()) + assertTrue(aarInfo.others.isEmpty()) + } + + private fun createTestAar(name: String, entries: List): SizerInputFile { + val aarFile = tempFolder.newFile(name) + ZipOutputStream(aarFile.outputStream()).use { zos -> + for (entry in entries) { + zos.putNextEntry(ZipEntry(entry)) + zos.write("test content".toByteArray()) + zos.closeEntry() + } + } + return SizerInputFile( + file = aarFile, + tag = aarFile.nameWithoutExtension + ) + } + + private class MockJarStreamParser : JarStreamParser { + var mockJarInfo: JarFileInfo? = null + var lastParsedEntry: ZipEntry? = null + + override fun parse(jarEntry: ZipEntry, inputStream: InputStream): JarFileInfo { + lastParsedEntry = jarEntry + return mockJarInfo ?: throw IllegalStateException("MockJarInfo not set") + } + } +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultApkFileParserTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultApkFileParserTest.kt new file mode 100644 index 0000000..64171ea --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultApkFileParserTest.kt @@ -0,0 +1,194 @@ +package com.grab.sizer.parser + +import com.android.tools.apk.analyzer.ApkSizeCalculator +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import shadow.bundletool.com.android.tools.proguard.ProguardMap +import java.io.File +import java.io.InputStream +import java.nio.file.Path +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class DefaultApkFileParserTest { + + @get:Rule + val tempFolder = TemporaryFolder() + + private lateinit var mockDexFileParser: MockDexFileParser + private lateinit var mockApkSizeCalculator: MockApkSizeCalculator + private lateinit var apkFileParser: DefaultApkFileParser + + @Before + fun setup() { + mockDexFileParser = MockDexFileParser() + mockApkSizeCalculator = MockApkSizeCalculator() + apkFileParser = DefaultApkFileParser(mockDexFileParser, mockApkSizeCalculator) + } + + @Test + fun parseApksShouldCorrectlyParseResourceFiles() { + val apk = createTestApk("test.apk", listOf("res/layout/activity_main.xml", "res/values/strings.xml")) + mockApkSizeCalculator.mockApkSizeInfo = createMockApkSizeInfo() + + val result = apkFileParser.parseApks(sequenceOf(apk), ProguardMap()) + + assertEquals(1, result.size) + val apkInfo = result.first() + assertEquals(2, apkInfo.resources.size) + assertTrue(apkInfo.resources.any { it.path == "/res/layout/activity_main.xml" }) + assertTrue(apkInfo.resources.any { it.path == "/res/values/strings.xml" }) + } + + @Test + fun parseApksShouldCorrectlyParseAssetFiles() { + val apk = createTestApk("test.apk", listOf("assets/fonts/roboto.ttf", "assets/images/logo.png")) + mockApkSizeCalculator.mockApkSizeInfo = createMockApkSizeInfo() + + val result = apkFileParser.parseApks(sequenceOf(apk), ProguardMap()) + + assertEquals(1, result.size) + val apkInfo = result.first() + assertEquals(2, apkInfo.assets.size) + assertTrue(apkInfo.assets.any { it.path == "/assets/fonts/roboto.ttf" }) + assertTrue(apkInfo.assets.any { it.path == "/assets/images/logo.png" }) + } + + @Test + fun parseApksShouldCorrectlyParseNativeLibraries() { + val apk = createTestApk("test.apk", listOf("lib/x86/libtest.so", "lib/arm64-v8a/libtest.so")) + mockApkSizeCalculator.mockApkSizeInfo = createMockApkSizeInfo() + + val result = apkFileParser.parseApks(sequenceOf(apk), ProguardMap()) + + assertEquals(1, result.size) + val apkInfo = result.first() + assertEquals(2, apkInfo.nativeLibs.size) + assertTrue(apkInfo.nativeLibs.any { it.path == "/lib/x86/libtest.so" }) + assertTrue(apkInfo.nativeLibs.any { it.path == "/lib/arm64-v8a/libtest.so" }) + } + + @Test + fun parseApksShouldCorrectlyParseDexFiles() { + val apk = createTestApk("test.apk", listOf("classes.dex", "classes2.dex")) + mockApkSizeCalculator.mockApkSizeInfo = createMockApkSizeInfo() + mockDexFileParser.setDexFileInfos( + listOf( + DexFileInfo("classes.dex", 1000, emptySet(), emptySet(), 500), + DexFileInfo("classes2.dex", 1000, emptySet(), emptySet(), 500) + ) + ) + + val result = apkFileParser.parseApks(sequenceOf(apk), ProguardMap()) + + assertEquals(1, result.size) + val apkInfo = result.first() + assertEquals(2, apkInfo.dexes.size) + assertTrue(apkInfo.dexes.any { it.name == "classes.dex" }) + assertTrue(apkInfo.dexes.any { it.name == "classes2.dex" }) + } + + @Test + fun parseApksShouldCorrectlyParseOtherFiles() { + val apk = createTestApk("test.apk", listOf("META-INF/MANIFEST.MF", "resources.arsc")) + mockApkSizeCalculator.mockApkSizeInfo = createMockApkSizeInfo() + + val result = apkFileParser.parseApks(sequenceOf(apk), ProguardMap()) + + assertEquals(1, result.size) + val apkInfo = result.first() + assertEquals(2, apkInfo.others.size) + assertTrue(apkInfo.others.any { it.path == "/META-INF/MANIFEST.MF" }) + assertTrue(apkInfo.others.any { it.path == "/resources.arsc" }) + } + + @Test + fun parseApksShouldCorrectlyHandleMixedContent() { + val apk = createTestApk("test.apk", listOf( + "res/layout/activity_main.xml", + "assets/fonts/roboto.ttf", + "lib/x86/libtest.so", + "classes.dex", + "resources.arsc" + )) + mockApkSizeCalculator.mockApkSizeInfo = createMockApkSizeInfo() + mockDexFileParser.setDexFileInfos( + listOf(DexFileInfo("classes.dex", 1000, emptySet(), emptySet(), 500)) + ) + + val result = apkFileParser.parseApks(sequenceOf(apk), ProguardMap()) + + assertEquals(1, result.size) + val apkInfo = result.first() + assertEquals(1, apkInfo.resources.size) + assertEquals(1, apkInfo.assets.size) + assertEquals(1, apkInfo.nativeLibs.size) + assertEquals(1, apkInfo.dexes.size) + assertEquals(1, apkInfo.others.size) + } + + @Test + fun parseApksShouldCorrectlyParseMultipleApks() { + val apk1 = createTestApk("test1.apk", listOf("res/layout/activity_main.xml")) + val apk2 = createTestApk("test2.apk", listOf("lib/x86/libtest.so")) + mockApkSizeCalculator.mockApkSizeInfo = createMockApkSizeInfo() + + val result = apkFileParser.parseApks(sequenceOf(apk1, apk2), ProguardMap()) + + assertEquals(2, result.size) + val apkInfo1 = result.find { it.name == "test1.apk" } + val apkInfo2 = result.find { it.name == "test2.apk" } + + assertEquals(1, apkInfo1?.resources?.size) + assertEquals(1, apkInfo2?.nativeLibs?.size) + } + + private fun createTestApk(name: String, entries: List): File { + val apkFile = tempFolder.newFile(name) + ZipOutputStream(apkFile.outputStream()).use { zos -> + for (entry in entries) { + zos.putNextEntry(ZipEntry(entry)) + zos.write("test content".toByteArray()) + zos.closeEntry() + } + } + return apkFile + } + + private fun createMockApkSizeInfo(): ApkSizeInfo { + return ApkSizeInfo( + downloadSize = 1000, + size = 2000, + downloadFileSizeMap = mapOf("res/layout/activity_main.xml" to 100L), + rawFileSizeMap = mapOf("res/layout/activity_main.xml" to 200L) + ) + } + + private class MockDexFileParser : DexFileParser { + private var mockDexFileInfo: MutableList = mutableListOf() + fun setDexFileInfos(values : List){ + mockDexFileInfo = values.toMutableList() + } + + override fun parse(entry: ZipEntry, inputStream: InputStream, apkSizeInfo: ApkSizeInfo, proguardMap: ProguardMap?): DexFileInfo { + if(mockDexFileInfo.isNotEmpty()) return mockDexFileInfo.removeFirst() + throw IllegalStateException("MockDexFileInfo not set") + } + } + + private class MockApkSizeCalculator : ApkSizeCalculator { + var mockApkSizeInfo: ApkSizeInfo? = null + + override fun getFullApkDownloadSize(apk: Path): Long = mockApkSizeInfo?.downloadSize ?: 0 + + override fun getFullApkRawSize(apk: Path): Long = mockApkSizeInfo?.size ?: 0 + + override fun getDownloadSizePerFile(apk: Path): Map = mockApkSizeInfo?.downloadFileSizeMap ?: emptyMap() + + override fun getRawSizePerFile(apk: Path): Map = mockApkSizeInfo?.rawFileSizeMap ?: emptyMap() + } +} \ No newline at end of file diff --git a/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultJarFileParserTest.kt b/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultJarFileParserTest.kt new file mode 100644 index 0000000..eb344e3 --- /dev/null +++ b/app-sizer/src/test/kotlin/com/grab/sizer/parser/DefaultJarFileParserTest.kt @@ -0,0 +1,124 @@ +package com.grab.sizer.parser + +import com.grab.sizer.utils.SizerInputFile +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class DefaultJarFileParserTest { + + private lateinit var jarFileParser: DefaultJarFileParser + + @get:Rule + val tempFolder = TemporaryFolder() + + @Before + fun setup() { + jarFileParser = DefaultJarFileParser() + } + + @Test + fun parseJarsShouldCorrectlyParseClassFiles() { + val jar = createTestJar("test.jar", listOf("com/example/Test1.class", "com/example/Test2.class")) + val result = jarFileParser.parseJars(sequenceOf(jar)) + + assertEquals(1, result.size) + val jarInfo = result.first() + assertEquals(2, jarInfo.classes.size) + assertTrue(jarInfo.classes.any { it.name == "com.example.Test1" }) + assertTrue(jarInfo.classes.any { it.name == "com.example.Test2" }) + } + + @Test + fun parseJarsShouldCorrectlyParseNativeLibraries() { + val jar = createTestJar("test.jar", listOf("lib/x86/libtest1.so", "lib/arm64-v8a/libtest2.so")) + val result = jarFileParser.parseJars(sequenceOf(jar)) + + assertEquals(1, result.size) + val jarInfo = result.first() + assertEquals(2, jarInfo.nativeLibs.size) + assertTrue(jarInfo.nativeLibs.any { it.path == "/lib/x86/libtest1.so" }) + assertTrue(jarInfo.nativeLibs.any { it.path == "/lib/arm64-v8a/libtest2.so" }) + } + + @Test + fun parseJarsShouldCorrectlyParseOtherFiles() { + val jar = createTestJar("test.jar", listOf("resources/test.txt", "META-INF/MANIFEST.MF")) + val result = jarFileParser.parseJars(sequenceOf(jar)) + + assertEquals(1, result.size) + val jarInfo = result.first() + assertEquals(2, jarInfo.others.size) + assertTrue(jarInfo.others.any { it.path == "/resources/test.txt" }) + assertTrue(jarInfo.others.any { it.path == "/META-INF/MANIFEST.MF" }) + } + + @Test + fun parseJarsShouldCorrectlyHandleMixedContent() { + val jar = createTestJar( + "test.jar", listOf( + "com/example/Test.class", + "lib/x86/libtest.so", + "resources/test.txt" + ) + ) + val result = jarFileParser.parseJars(sequenceOf(jar)) + + assertEquals(1, result.size) + val jarInfo = result.first() + assertEquals(1, jarInfo.classes.size) + assertEquals(1, jarInfo.nativeLibs.size) + assertEquals(1, jarInfo.others.size) + } + + @Test + fun parseJarsShouldCorrectlyParseMultipleJars() { + val jar1 = createTestJar("test1.jar", listOf("com/example/Test1.class")) + val jar2 = createTestJar("test2.jar", listOf("lib/x86/libtest.so")) + val result = jarFileParser.parseJars(sequenceOf(jar1, jar2)) + + assertEquals(2, result.size) + val jarInfo1 = result.find { it.name == "test1.jar" } + val jarInfo2 = result.find { it.name == "test2.jar" } + + assertNotNull(jarInfo1) + assertEquals(1, jarInfo1!!.classes.size) + + assertNotNull(jarInfo2) + assertEquals(1, jarInfo2!!.nativeLibs.size) + } + + @Test + fun parseJarsShouldHandleEmptyJarFiles() { + val emptyJar = createTestJar("empty.jar", listOf()) + val result = jarFileParser.parseJars(sequenceOf(emptyJar)) + + assertEquals(1, result.size) + val jarInfo = result.first() + assertEquals("empty.jar", jarInfo.name) + assertEquals("empty", jarInfo.tag) + assertTrue(jarInfo.classes.isEmpty()) + assertTrue(jarInfo.nativeLibs.isEmpty()) + assertTrue(jarInfo.others.isEmpty()) + } + + private fun createTestJar(name: String, entries: List): SizerInputFile { + val jarFile = tempFolder.newFile(name) + ZipOutputStream(jarFile.outputStream()).use { zos -> + for (entry in entries) { + zos.putNextEntry(ZipEntry(entry)) + zos.write("test content".toByteArray()) + zos.closeEntry() + } + } + return SizerInputFile( + file = jarFile, + tag = jarFile.nameWithoutExtension + ) + + } +} \ No newline at end of file diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..1bd0804 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +plugins { + `java-gradle-plugin` + `kotlin-dsl` +} + + +repositories { + google() + mavenCentral() + maven { + url = uri("https://plugins.gradle.org/m2/") + } + maven { + setUrl("https://artifacts.gitlab.myteksi.net/artifactory/mobile--android") + credentials { + username = System.getenv("READ_USER") + password = System.getenv("READ_PASSWORD") + } + } +} + +dependencies { + implementation (libs.kotlin.gradle.plugin) +} + +gradlePlugin { + plugins { + register("sizerKotlinBuildPlugin"){ + id = "com.grab.sizer.kotlin" + implementationClass = "com.grab.sizer.buildplugin.AppSizerConfigPlugin" + } + } +} \ No newline at end of file diff --git a/build-logic/gradle b/build-logic/gradle new file mode 120000 index 0000000..3337596 --- /dev/null +++ b/build-logic/gradle @@ -0,0 +1 @@ +../gradle \ No newline at end of file diff --git a/build-logic/src/main/kotlin/com/grab/sizer/buildplugin/AppSizerConfigPlugin.kt b/build-logic/src/main/kotlin/com/grab/sizer/buildplugin/AppSizerConfigPlugin.kt new file mode 100644 index 0000000..604d6cf --- /dev/null +++ b/build-logic/src/main/kotlin/com/grab/sizer/buildplugin/AppSizerConfigPlugin.kt @@ -0,0 +1,55 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.buildplugin + +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.compile.JavaCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +/** This plugin represents a build configuration + * of java-libraries/kotlin modules. + */ + +class AppSizerConfigPlugin : Plugin { + override fun apply(project: Project) { + project.plugins.apply("org.jetbrains.kotlin.jvm") + project.plugins.apply("org.jetbrains.kotlin.kapt") + project.version = if (System.getenv("CI") != null) "SNAPSHOT-08" else "SNAPSHOT" + + project.tasks.withType(KotlinCompile::class.java).forEach { + it.kotlinOptions.jvmTarget = "11" + } + + project.tasks.withType(JavaCompile::class.java).configureEach { + sourceCompatibility = JavaVersion.VERSION_11.toString() + targetCompatibility = JavaVersion.VERSION_11.toString() + } + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..32339c1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +buildscript { + repositories { + google() + mavenCentral() + } +} + + +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.kapt) apply false + alias(libs.plugins.johnrengelman.shadow) apply false +} + diff --git a/cli-config-template.yml b/cli-config-template.yml new file mode 100644 index 0000000..d8e11cd --- /dev/null +++ b/cli-config-template.yml @@ -0,0 +1,36 @@ +project-input: + libraries-directory: "./path/to/libs/aars/jars/dir" + modules-directory: "./path/to/modules/aar/jars/dir" + modules-dir-is-project-root: true + r8-mapping-file: "./path/to/R8/mapping/file.txt" + owner-mapping-file: "./path/to/owner/mapping/file.yml" + version: "your/app/version" + large-file-threshold: 10 + project-name: "your/project/name" +apk-generation: + bundle-tool: "./path/to/bundle/tool.jar" + app-bundle-file: "./path/to/the/app/bundle.aab" + device-specs: + - "./path/to/device/spec-1.json" + - "./path/to/device/spec-n.json" + key-signing: + keystore-file: "./path/to/your/keystore/file.keystore" + keystore-pw: "keystore_password" + key-alias: "keystore_alias" + key-pw: "key_pw" +report: + output-directory: "./path/to/output/report/dir" + custom-attributes: + your-custom-key: "your_custom_value" + influx-db-config: + db-name: "database name" + url: "db server url" + username: "db-username" + password: "db-password" + report-table-name: "app_size" + retention-policy: + name: "retention-policy-name" + duration: "value" + shard-duration: "value" + replication-factor: value + is-default: true/false \ No newline at end of file diff --git a/clt/build.gradle b/clt/build.gradle new file mode 100644 index 0000000..05675c1 --- /dev/null +++ b/clt/build.gradle @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +plugins { + id "com.grab.sizer.kotlin" + id 'com.github.johnrengelman.shadow' +} + +shadowJar { + zip64 true + + manifest { + attributes 'Main-Class': 'com.grab.sizer.MainKt' + } + + archiveBaseName.set('clt-app-sizer') + archiveClassifier.set('') + archiveVersion.set('') + + mergeServiceFiles() +} + +dependencies { + implementation project(':app-sizer') + implementation libs.google.dagger + kapt libs.dagger.compiler + implementation libs.clikt + implementation libs.jackson.module.kotlin + implementation libs.jackson.dataformat.yaml + implementation libs.gson + + /** + * These two libs were compileOnly in app-sizer + */ + implementation libs.android.tools.bundletool + implementation libs.android.tools.common + + testImplementation libs.junit +} + diff --git a/clt/src/main/kotlin/com/grab/sizer/AnalyzerCommand.kt b/clt/src/main/kotlin/com/grab/sizer/AnalyzerCommand.kt new file mode 100644 index 0000000..c3da054 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/AnalyzerCommand.kt @@ -0,0 +1,96 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.grab.sizer.config.Config +import com.grab.sizer.config.ConfigYmlLoader +import com.grab.sizer.utils.CltLogger +import com.grab.sizer.utils.DefaultFileQuery +import com.grab.sizer.utils.Logger +import java.io.File + + +class AnalyzerCommand : CliktCommand() { + private val settingFile: File by option( + "-s", + "--config-file", + help = "Path to the config file" + ).convert { File(it) }.required() + + private val libName: String? by option( + "-l", + "--lib-name", + help = """ + Name of the lib/module you want to list the content contributed to the apks + Note that this param only necessary for the AnalyticsOption.LIB_CONTENT option + """.trimIndent() + ) + + + private val reportOption by option() + .switch( + "--libraries" to AnalyticsOption.LIBRARIES, + "--modules" to AnalyticsOption.MODULES, + "--apk" to AnalyticsOption.APK, + "--basic" to AnalyticsOption.BASIC, + "--codebase" to AnalyticsOption.CODEBASE, + "--large-files" to AnalyticsOption.LARGE_FILE, + "--lib-content" to AnalyticsOption.LIB_CONTENT, + ).default(AnalyticsOption.DEFAULT) + + override fun run() { + val config = ConfigYmlLoader().load(settingFile) + .also { + it.validateInput() + } + val logger: Logger = CltLogger() + DefaultApkGenerator.create(config) + .generate(config.apkGeneration.deviceSpecs) + .forEach { apkDirectory -> + AppSizer( + inputProvider = CltInputProvider( + fileQuery = DefaultFileQuery(), + config = config, + apksDirectory = apkDirectory + ), + outputProvider = CltOutputProvider(config, apkDirectory.nameWithoutExtension), + libName = libName, + logger = logger + ).process(reportOption) + } + } + + private fun Config.validateInput() { + if (reportOption == AnalyticsOption.LIB_CONTENT && libName == null) { + throw IllegalArgumentException("You have to pass the --lib-name to execute this option") + } + } +} + diff --git a/clt/src/main/kotlin/com/grab/sizer/ApkGenerator.kt b/clt/src/main/kotlin/com/grab/sizer/ApkGenerator.kt new file mode 100644 index 0000000..e7b0659 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/ApkGenerator.kt @@ -0,0 +1,135 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import com.grab.sizer.config.Config +import com.grab.sizer.config.KeySigning +import java.io.File + + +interface ApkGenerator { + /** + * Each device spec will generate a set of APKs and will return one directory per each device spec + * @param deviceSpecs: the list of files that contain the device specs + */ + fun generate(deviceSpecs: List): List +} + +class DefaultApkGenerator( + private val bundleTool: File, + private val appBundle: File, + private val outPutDirectory: File, + private val signing: KeySigning?, + private val commandExecutor: CommandExecutor = CommandExecutor() +) : ApkGenerator { + override fun generate(deviceSpecs: List): List { + if (!outPutDirectory.exists()) outPutDirectory.mkdirs() + return deviceSpecs.map { deviceSpec -> + File(outPutDirectory, deviceSpec.nameWithoutExtension).apply { + if (!exists()) { + mkdirs() + } else { + clearDirectory() + } + }.also { outputDir -> + File.createTempFile(deviceSpec.nameWithoutExtension, ".apks").also { tempFile -> + try { + generateApksFile(tempFile, deviceSpec) + extractApksToDirectory(tempFile, deviceSpec.path, outputDir) + } finally { + tempFile.delete() + } + } + } + }.toList() + } + + private fun extractApksToDirectory(apksTempFile: File, deviceSpec: String, outputDirectory: File) { + commandExecutor.execute( + listOf( + "java", + "-jar", + bundleTool.path, + "extract-apks", + "--apks=${apksTempFile.path}", + "--output-dir=${outputDirectory.path}", + "--device-spec=${deviceSpec}", + ) + ) + } + + private fun File.clearDirectory() { + if (!exists()) return + if (!isDirectory) throw RuntimeException("The ${this.path} file is not a directory") + walk().forEach { apk -> + apk.delete() + } + } + + private fun generateApksFile(output: File, deviceSpec: File) { + val signingParam = signing?.run { + listOf( + "--ks=${signing.keystoreFile}", + "--ks-pass=pass:${signing.keystorePw}", + "--key-pass=pass:${signing.keyPw}", + "--ks-key-alias=${signing.keyAlias}", + ) + } ?: listOf("--local-testing") + + commandExecutor.execute( + listOf( + "java", + "-jar", + bundleTool.path, + "build-apks", + "--bundle=${appBundle.path}", + "--output=${output.path}", + "--device-spec=${deviceSpec.path}", + "--overwrite" + ) + signingParam + ) + } + + companion object { + fun create(config: Config): DefaultApkGenerator = DefaultApkGenerator( + config.apkGeneration.bundleTool, + config.apkGeneration.appBundleFile, + outPutDirectory = File(config.report.outputDirectory, "apks"), + config.apkGeneration.keySigning + ) + } +} + +class CommandExecutor { + fun execute(command: List): Int = ProcessBuilder(command) + .directory(File(System.getProperty("user.dir"))) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + .waitFor() +} \ No newline at end of file diff --git a/clt/src/main/kotlin/com/grab/sizer/CltInputProvider.kt b/clt/src/main/kotlin/com/grab/sizer/CltInputProvider.kt new file mode 100644 index 0000000..79a0311 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/CltInputProvider.kt @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import com.grab.sizer.config.Config +import com.grab.sizer.utils.FileQuery +import com.grab.sizer.utils.InputProvider +import com.grab.sizer.utils.SizerInputFile +import java.io.File + + +internal const val EXT_AAR = "aar" +internal const val EXT_APK = "apk" +internal const val EXT_JAR = "jar" +internal const val DEFAULT_JAR_DIR = "build/libs" +internal const val GRADLE_FILE = "build.gradle" +internal const val DEFAULT_AAR_FOLDER = "/build/outputs/aar" +private const val BUILD_FOLDER = "build" + +interface FileSystem { + fun create(parent: File, path: String): File +} + +class DefaultFileSystem : FileSystem { + override fun create(parent: File, path: String): File = File(parent, path) +} + +class CltInputProvider constructor( + private val fileQuery: FileQuery, + private val config: Config, + private val apksDirectory: File, + private val fileSystem: FileSystem = DefaultFileSystem() +) : InputProvider { + override fun provideModuleAar(): Sequence { + return modulesSource(DEFAULT_AAR_FOLDER) + .flatMap { fileQuery.query(it, EXT_AAR) } + .map { file -> + SizerInputFile( + tag = getModulePath(config.projectInput.projectRoot, file), + file = file + ) + } + } + + private fun getModulePath(rootDir: File, aarFile: File): String { + return if(config.projectInput.modulesDirIsProjectRoot) { + val aarPath = aarFile.absolutePath + val rootPath = rootDir.absolutePath + val buildFolderIndex = aarPath.indexOf(File.separator + BUILD_FOLDER + File.separator) + + if (buildFolderIndex != -1) { + val modulePathFromRoot = aarPath.substring(rootPath.length, buildFolderIndex) + val modulePath = modulePathFromRoot.trim(File.separatorChar).replace(File.separatorChar, ':') + modulePath.ifEmpty { rootDir.name } + } + else{ + // If no "build" folder is found, return the file name without extension + aarFile.nameWithoutExtension + } + + } else { + aarFile.nameWithoutExtension + } + } + + private fun modulesSource(gradleDefaultFolder: String): Sequence = + if (config.projectInput.modulesDirIsProjectRoot) { + config.projectInput.modulesDirectory + .queryProjectModules() + .map { fileSystem.create(it, gradleDefaultFolder) } + .filter { it.exists() && it.isDirectory } + } else + sequenceOf(config.projectInput.modulesDirectory) + + + override fun provideModuleJar(): Sequence { + return modulesSource(DEFAULT_JAR_DIR) + .flatMap { fileQuery.query(it, EXT_JAR) } + .map { file -> + SizerInputFile( + tag = getModulePath(config.projectInput.projectRoot, file), + file = file + ) + } + } + + override fun provideLibraryJar(): Sequence = fileQuery.query( + config.projectInput.librariesDirectory, EXT_JAR + ).map { file -> + SizerInputFile( + tag = file.nameWithoutExtension, + file = file + ) + } + + override fun provideLibraryAar(): Sequence = fileQuery.query( + config.projectInput.librariesDirectory, EXT_AAR + ).map { file -> + SizerInputFile( + tag = file.nameWithoutExtension, + file = file + ) + } + + override fun provideApkFiles(): Sequence = fileQuery.query(apksDirectory, EXT_APK) + + override fun provideR8MappingFile(): File? = config.projectInput.r8MappingFile + + override fun provideTeamMappingFile(): File? = config.projectInput.ownerMappingFile + + override fun provideLargeFileThreshold(): Long = config.projectInput.largeFileThreshold +} + + +/** + * Only enter the module folder which is: + * - Parent is the project root folder + * - Folder having build.gradle file + * Any other folder that stay the same level with build.gradle folder will be ignored + */ +internal fun File.queryProjectModules(): Sequence = walk() + .onEnter { file -> + if (file.parentFile == this || file.listFiles().any { it.name == GRADLE_FILE }) true + else !file.parentFile.listFiles().any { it.name == GRADLE_FILE } + }.filter { file -> + file.isDirectory && file.listFiles().any { it.name == GRADLE_FILE } + } \ No newline at end of file diff --git a/clt/src/main/kotlin/com/grab/sizer/CltOutputProvider.kt b/clt/src/main/kotlin/com/grab/sizer/CltOutputProvider.kt new file mode 100644 index 0000000..a5edb20 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/CltOutputProvider.kt @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import com.grab.sizer.config.Config +import com.grab.sizer.report.CustomProperties +import com.grab.sizer.report.ProjectInfo +import com.grab.sizer.report.db.DatabaseRetentionPolicy +import com.grab.sizer.report.db.InfluxDBConfig +import com.grab.sizer.utils.OutputProvider +import java.io.File + +class CltOutputProvider( + private val config: Config, + private val deviceName: String +) : OutputProvider { + override fun provideInfluxDbConfig(): InfluxDBConfig? = config.report.influxDbConfig?.toSizerConfig() + override fun provideOutPutDirectory(): File = config.report.outputDirectory + override fun provideProjectInfo(): ProjectInfo { + return ProjectInfo( + projectName = config.projectInput.projectName, + versionName = config.projectInput.version, + deviceName = deviceName + ) + } + + override fun provideCustomProperties(): CustomProperties = config.report.customAttributes ?: emptyMap() +} + +private fun com.grab.sizer.config.InfluxDbConfig.toSizerConfig(): InfluxDBConfig = InfluxDBConfig( + dbName = dbName, + url = url, + username = username, + password = password, + reportTableName = reportTableName, + databaseRetentionPolicy = retentionPolicy?.toSizerConfig() ?: DatabaseRetentionPolicy.createDefault() +) + + +private fun com.grab.sizer.config.RetentionPolicy.toSizerConfig(): DatabaseRetentionPolicy = DatabaseRetentionPolicy( + name = name, + duration = duration, + shardDuration = shardDuration, + replicationFactor = replicationFactor, + isDefault = isDefault +) diff --git a/clt/src/main/kotlin/com/grab/sizer/config/ApkGenerationConfig.kt b/clt/src/main/kotlin/com/grab/sizer/config/ApkGenerationConfig.kt new file mode 100644 index 0000000..769fbb6 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/config/ApkGenerationConfig.kt @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.config + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.File + +data class ApkGenerationConfig( + @JsonProperty("bundle-tool") private val bundleToolPath: String, + @JsonProperty("app-bundle-file") private val appBundleFilePath: String, + @JsonProperty("device-specs") private val deviceSpecPaths: List, + @JsonProperty("key-signing") val keySigning: KeySigning? +) { + + @get:JsonIgnore + val deviceSpecs: List + get() = deviceSpecPaths.map { File(it) } + + @get:JsonIgnore + val bundleTool: File + get() = File(bundleToolPath) + + @get:JsonIgnore + val appBundleFile: File + get() = File(appBundleFilePath) +} + +data class KeySigning( + @JsonProperty("keystore-file") val keystoreFile: String, + @JsonProperty("keystore-pw") val keystorePw: String, + @JsonProperty("key-alias") val keyAlias: String, + @JsonProperty("key-pw") val keyPw: String +) \ No newline at end of file diff --git a/clt/src/main/kotlin/com/grab/sizer/config/Config.kt b/clt/src/main/kotlin/com/grab/sizer/config/Config.kt new file mode 100644 index 0000000..a197b01 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/config/Config.kt @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import java.io.File + +data class Config( + @JsonProperty("project-input") val projectInput: ProjectInputConfig, + @JsonProperty("apk-generation") val apkGeneration: ApkGenerationConfig, + @JsonProperty("report") val report: ReportConfig +) + + +class ConfigYmlLoader() { + fun load(configFile: File): Config = ObjectMapper(YAMLFactory()).run { + registerModule( + KotlinModule.Builder() + .build() + ) + readValue(configFile) + } +} + diff --git a/clt/src/main/kotlin/com/grab/sizer/config/ProjectInput.kt b/clt/src/main/kotlin/com/grab/sizer/config/ProjectInput.kt new file mode 100644 index 0000000..0776de5 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/config/ProjectInput.kt @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.config + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.module.kotlin.contains +import java.io.File + +private const val DEFAULT_LARGE_FILE = 10240L // 10kb + +@JsonDeserialize(using = ProjectInputConfigDeserializer::class) +data class ProjectInputConfig( + val version: String, + val projectName: String, + val largeFileThreshold: Long = DEFAULT_LARGE_FILE, + val librariesDirectory: File, + val modulesDirectory: File, + val projectRoot: File, + val r8MappingFile: File? = null, + val ownerMappingFile: File? = null, +) { + val modulesDirIsProjectRoot: Boolean + get() = modulesDirectory.path.startsWith(projectRoot.path) + +} + +class ProjectInputConfigDeserializer(vc: Class<*>? = null) : StdDeserializer(vc) { + override fun deserialize(jsonParser: JsonParser, ctxt: DeserializationContext): ProjectInputConfig = + jsonParser.codec.readTree(jsonParser).run { + ProjectInputConfig( + version = get("version").asText(), + largeFileThreshold = if (contains("large-file-threshold")) get("large-file-threshold").asLong() else DEFAULT_LARGE_FILE, + projectName = get("project-name").asText(), + librariesDirectory = File(get("libraries-directory").asText()), + modulesDirectory = File(get("modules-directory").asText()), + r8MappingFile = if (contains("r8-mapping-file")) File(get("r8-mapping-file").asText()) else null, + ownerMappingFile = if (contains("owner-mapping-file")) File(get("owner-mapping-file").asText()) else null, + projectRoot = File(get("project-root-dir").asText()), + ) + } +} \ No newline at end of file diff --git a/clt/src/main/kotlin/com/grab/sizer/config/Report.kt b/clt/src/main/kotlin/com/grab/sizer/config/Report.kt new file mode 100644 index 0000000..d3808b6 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/config/Report.kt @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.config + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.File + +data class ReportConfig( + @JsonProperty("output-directory") private val outputDirectoryPath: String, + @JsonProperty("custom-attributes") val customAttributes: Map?, + @JsonProperty("influx-db-config") val influxDbConfig: InfluxDbConfig? +) { + @get:JsonIgnore + val outputDirectory: File + get() = File(outputDirectoryPath) +} + +data class InfluxDbConfig( + @JsonProperty("retention-policy") val retentionPolicy: RetentionPolicy?, + @JsonProperty("report-table-name") val reportTableName: String?, + @JsonProperty("db-name") val dbName: String?, + val url: String, + val username: String, + val password: String, +) + +data class RetentionPolicy( + val name: String, + val duration: String, + @JsonProperty("shard-duration") val shardDuration: String, + @JsonProperty("replication-factor") val replicationFactor: Int, + @JsonProperty("is-default") val isDefault: Boolean +) \ No newline at end of file diff --git a/clt/src/main/kotlin/com/grab/sizer/main.kt b/clt/src/main/kotlin/com/grab/sizer/main.kt new file mode 100644 index 0000000..7a10842 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/main.kt @@ -0,0 +1,30 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +fun main(args: Array) = AnalyzerCommand().main(args) diff --git a/clt/src/main/kotlin/com/grab/sizer/utils/CltLogger.kt b/clt/src/main/kotlin/com/grab/sizer/utils/CltLogger.kt new file mode 100644 index 0000000..8ae9ab4 --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/utils/CltLogger.kt @@ -0,0 +1,43 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.utils + +class CltLogger : Logger { + override fun log(tag: String, message: String) { + println("$tag : $message") + } + + override fun log(tag: String, e: Exception) { + println("$tag :") + e.printStackTrace() + } + + override fun logDebug(tag: String, message: String) { + println("$tag : $message") + } +} \ No newline at end of file diff --git a/clt/src/main/kotlin/com/grab/sizer/utils/FileQuery.kt b/clt/src/main/kotlin/com/grab/sizer/utils/FileQuery.kt new file mode 100644 index 0000000..6f81bac --- /dev/null +++ b/clt/src/main/kotlin/com/grab/sizer/utils/FileQuery.kt @@ -0,0 +1,54 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer.utils + +import java.io.File +import java.io.IOException +import javax.inject.Inject + + +interface FileQuery { + fun query(dir: File, vararg extensions: String): Sequence +} + +class DefaultFileQuery @Inject constructor() : FileQuery { + + @Throws(IOException::class) + override fun query(dir: File, vararg extensions: String): Sequence { + if (dir.isFile) throw IOException("${dir.path} is not a directory") + return dir.walk() + .filter { file -> + file.isFile && extensions.any { ext -> + file.extension.equals(ext, true) + } + } + } +} + + + diff --git a/clt/src/test/kotlin/com/grab/sizer/CltInputProviderTest.kt b/clt/src/test/kotlin/com/grab/sizer/CltInputProviderTest.kt new file mode 100644 index 0000000..7690951 --- /dev/null +++ b/clt/src/test/kotlin/com/grab/sizer/CltInputProviderTest.kt @@ -0,0 +1,163 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import com.grab.sizer.utils.DefaultFileQuery +import org.junit.Assert +import org.junit.Test +import java.io.File + +class CltInputProviderTest { + private val fileQuery = DefaultFileQuery() + private val testingProject1 = TestingProject1() + private val config = testingProject1.config + + + @Test + fun provideModuleAarShouldGetAllAarFromProjectFolderWhenModulesDirIsNotProjectRoot() { + val config = config.copy( + projectInput = config.projectInput.copy(projectRoot = File("./abc")) + ) + + val cltInputProvider = CltInputProvider( + fileQuery = fileQuery, + config = config, + apksDirectory = File("FakeDir"), + fileSystem = testingProject1 + ) + val moduleAars = cltInputProvider.provideModuleAar() + .toList() + .sortedBy { it.file } + .toTypedArray() + val expectingAllAars = testingProject1.expectingAllAarsWhenNotAProjectRoot + .sortedBy { it.file } + .toTypedArray() + Assert.assertEquals(expectingAllAars.size, 3) + Assert.assertArrayEquals(moduleAars, expectingAllAars) + } + + @Test + fun provideModuleJarShouldGetAllJarFromProjectFolderWhenModulesDirIsNotProjectRoot() { + val config = config.copy( + projectInput = config.projectInput.copy(projectRoot = File("./abc")) + ) + + val cltInputProvider = CltInputProvider( + fileQuery = fileQuery, + config = config, + apksDirectory = File("FakeDir"), + fileSystem = testingProject1 + ) + val moduleJars = cltInputProvider.provideModuleJar() + .toList() + .sortedBy { it.file } + .toTypedArray() + + val expectingAllJars = testingProject1.expectingAllJarsWhenNotAProjectRoot.sortedBy { it.file }.toTypedArray() + + Assert.assertEquals(expectingAllJars.size, 2) + Assert.assertArrayEquals(moduleJars, expectingAllJars) + } + + @Test + fun provideModuleAarShouldGetCorrectAarFromProjectFolderWhenModulesDirIsProjectRoot() { + val cltInputProvider = CltInputProvider( + fileQuery = fileQuery, + config = config, + apksDirectory = File("FakeDir"), + fileSystem = testingProject1 + ) + val moduleAars = cltInputProvider.provideModuleAar() + .toList() + .sortedBy { it.file } + .toTypedArray() + val expectingModuleAars = testingProject1.expectingModuleAars + .sortedBy { it.tag } + .toTypedArray() + Assert.assertEquals(expectingModuleAars.size, 2) + Assert.assertArrayEquals(moduleAars, expectingModuleAars) + } + + @Test + fun provideModuleJarShouldGetCorrectJarFromProjectFolderWhenModulesDirIsProjectRoot() { + + val cltInputProvider = CltInputProvider( + fileQuery = fileQuery, + config = config, + apksDirectory = File("FakeDir"), + fileSystem = testingProject1 + ) + val moduleJars = cltInputProvider.provideModuleJar() + .toList() + .sortedBy { it.tag } + .toTypedArray() + val expectingModuleJars = testingProject1.expectingModuleJars + .sortedBy { it.tag } + .toTypedArray() + Assert.assertEquals(expectingModuleJars.size, 1) + Assert.assertArrayEquals(moduleJars, expectingModuleJars) + } + + @Test + fun provideLibraryAarShouldGetAllAarFromFolder() { + val cltInputProvider = CltInputProvider( + fileQuery = fileQuery, + config = config, + apksDirectory = File("FakeDir"), + fileSystem = testingProject1 + ) + val libraryAar = cltInputProvider.provideLibraryAar().toList() + .sortedBy { it.tag } + .toTypedArray() + val expectingLibAars = testingProject1.expectingLibAars + .sortedBy { it.tag } + .toTypedArray() + Assert.assertEquals(expectingLibAars.size, 2) + Assert.assertArrayEquals(libraryAar, expectingLibAars) + } + + @Test + fun provideLibraryJarShouldGetAllAarFromFolder() { + val cltInputProvider = CltInputProvider( + fileQuery = fileQuery, + config = config, + apksDirectory = File("FakeDir"), + fileSystem = testingProject1 + ) + val libraryJars = cltInputProvider.provideLibraryJar() + .toList() + .sortedBy { it.tag } + .toTypedArray() + val expectingLibJars = testingProject1.expectingLibJars + .sortedBy { it.tag } + .toTypedArray() + Assert.assertEquals(expectingLibJars.size, 2) + Assert.assertArrayEquals(libraryJars, expectingLibJars) + } + +} \ No newline at end of file diff --git a/clt/src/test/kotlin/com/grab/sizer/TestingProject1.kt b/clt/src/test/kotlin/com/grab/sizer/TestingProject1.kt new file mode 100644 index 0000000..1ecfa6f --- /dev/null +++ b/clt/src/test/kotlin/com/grab/sizer/TestingProject1.kt @@ -0,0 +1,304 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.sizer + +import com.grab.sizer.config.ApkGenerationConfig +import com.grab.sizer.config.Config +import com.grab.sizer.config.ProjectInputConfig +import com.grab.sizer.config.ReportConfig +import com.grab.sizer.utils.SizerInputFile +import java.io.File + +internal const val APP_APK = "app.apk" +internal const val MODULE1_AAR = "module1-debug.aar" +internal const val MODULE2_AAR = "module2-debug.aar" +internal const val JAVA_MODULE_JAR = "java-module.jar" +internal const val NOT_A_MODULE_AAR = "not-a-module.aar" +internal const val NOT_A_JAVA_MODULE_JAR = "not-a-java-module.jar" +internal const val BUILD_GRADLE = "build.gradle" +internal const val SECURITY_CRYPTO_SOURCES_JAR = "security-crypto-1.1.0-alpha03-sources.jar" +internal const val SECURITY_CRYPTO_POM = "security-crypto-1.1.0-alpha03.pom" +internal const val SECURITY_CRYPTO_AAR = "security-crypto-1.1.0-alpha03.aar" +internal const val WORK_MULTIPROCESS_SOURCES_JAR = "work-multiprocess-2.8.0-sources.jar" +internal const val WORK_MULTIPROCESS_AAR = "work-multiprocess-2.8.0.aar" +internal const val WORK_MULTIPROCESS_POM = "work-multiprocess-2.8.0.pom" + +/** + * This class contain a project files & folders for testing the [CltInputProvider] + * It build a project with this structure: + * ``` + * ./user-folder/root-project + * - app + * - module1 + * - group1: + * - module2 + * - java-module + * ``` + * Beside that there is two trash folders which are not project module but contains trash aar & jar files + * - ./user-folder/root-project/group1/not-a-module + * - ./user-folder/root-project/group1/not-java-module + */ +class TestingProject1 : FileSystem { + val projectDir: FakeFile = createProjectDir() + val libDir: FakeFile = createLibDir() + val allFiles = projectDir.getAll() + libDir.getAll() + val config = createConfig() + + /** + * When config.projectInput.modulesDirIsProjectRoot = false + */ + val expectingAllAarsWhenNotAProjectRoot = projectDir.getAll() + .filter { it.extension == EXT_AAR } + .map { SizerInputFile(file = it, tag = it.nameWithoutExtension) } + + /** + * When config.projectInput.modulesDirIsProjectRoot = false + */ + val expectingAllJarsWhenNotAProjectRoot = projectDir.getAll().filter { it.extension == EXT_JAR } + .map { SizerInputFile(file = it, tag = it.nameWithoutExtension) } + + val expectingAllAars = projectDir.getAll() + .filter { it.extension == EXT_AAR } + .map { + when (it.name) { + MODULE1_AAR -> SizerInputFile( + file = it, + tag = "module1" + ) + + MODULE2_AAR -> SizerInputFile( + file = it, + tag = "group1:module2" + ) + + else -> SizerInputFile(file = it, tag = it.nameWithoutExtension) + } + } + + val expectingAllJars = projectDir.getAll().filter { it.extension == EXT_JAR } + .map { + when (it.name) { + JAVA_MODULE_JAR -> SizerInputFile( + file = it, + tag = "group1:java-module" + ) + + else -> SizerInputFile(file = it, tag = it.nameWithoutExtension) + } + } + + + val expectingModuleAars = expectingAllAars.filter { it.file.name != NOT_A_MODULE_AAR } + val expectingModuleJars = expectingAllJars.filter { it.file.name != NOT_A_JAVA_MODULE_JAR } + + val expectingLibAars = libDir.getAll().filter { it.extension == EXT_AAR } + .map { + SizerInputFile( + file = it, + tag = it.nameWithoutExtension + ) + } + val expectingLibJars = libDir.getAll().filter { it.extension == EXT_JAR } + .map { + SizerInputFile( + file = it, + tag = it.nameWithoutExtension + ) + } + + + private fun createConfig(): Config { + return Config( + projectInput = ProjectInputConfig( + version = "0.0.1", + projectName = "testing01", + modulesDirectory = projectDir, + projectRoot = projectDir, + librariesDirectory = libDir + ), + apkGeneration = ApkGenerationConfig( + bundleToolPath = "bundle/path", + appBundleFilePath = "app/bundle/bundle.aab", + deviceSpecPaths = emptyList(), + keySigning = null + ), + report = ReportConfig( + outputDirectoryPath = "output", + customAttributes = null, + influxDbConfig = null + ) + ) + } + + private fun createLibDir(): FakeFile { + return FakeFile(File("."), "gradle-cache", directory = true) { + addDirectory("androidx.security") { + addDirectory("security-crypto") { + addDirectory("1.1.0-alpha03") { + addDirectory("a96855861b33f9a46ca6a1556118ae592cad2014") { + addFile(SECURITY_CRYPTO_SOURCES_JAR) + } + addDirectory("b3c8960986915ab431476ae2072273adb4b83515") { + addFile(SECURITY_CRYPTO_POM) + } + addDirectory("f54110eab7610d08d7c41c594b3a248dac488e00") { + addFile(SECURITY_CRYPTO_AAR) + } + } + } + } + + addDirectory("androidx.work") { + addDirectory("work-multiprocess") { + addDirectory("2.8.0") { + addDirectory("8547c508168f54ce7c2fa0c4b6c3fc8850d30f23") { + addFile(WORK_MULTIPROCESS_SOURCES_JAR) + } + addDirectory("90aacad73ba44fe05b25de0c5308160c703dba0b") { + addFile(WORK_MULTIPROCESS_AAR) + } + addDirectory("77a1c6094184a05d8718a77f004aaa75fd296b") { + addFile(WORK_MULTIPROCESS_POM) + } + } + } + } + } + } + + private fun createProjectDir(): FakeFile { + val projectParent = FakeFile(File("."), "user-folder") { + addDirectory("root-project") { + addFile(BUILD_GRADLE) + addDirectory("app") { + addFile(BUILD_GRADLE) + addDirectory("build") { + addDirectory("outputs") { + addDirectory("apk") { + addDirectory("debug") { + addFile(APP_APK) + } + } + } + } + } + addDirectory("module1") { + addFile(BUILD_GRADLE) + addDirectory("build") { + addDirectory("outputs") { + addDirectory("aar") { + addFile(MODULE1_AAR) + } + } + } + } + addDirectory("group1") { + addDirectory("module2") { + addFile(BUILD_GRADLE) + addDirectory("build") { + addDirectory("outputs") { + addDirectory("aar") { + addFile(MODULE2_AAR) + } + } + } + } + + addDirectory("java-module") { + addFile(BUILD_GRADLE) + addDirectory("build") { + addDirectory("libs") { + addFile(JAVA_MODULE_JAR) + } + } + } + + addDirectory("not-a-module") { + addDirectory("build") { + addDirectory("outputs") { + addDirectory("aar") { + addFile(NOT_A_MODULE_AAR) + } + } + } + } + + addDirectory("not-java-module") { + addDirectory("build") { + addDirectory("libs") { + addFile(NOT_A_JAVA_MODULE_JAR) + } + } + } + } + } + } + return projectParent.children.first() + } + + override fun create(parent: File, path: String): File { + val file = File(parent, path) + return allFiles.find { it.path == file.path } ?: file + } +} + +class FakeFile( + val parent: File, + path: String, + val directory: Boolean = false, + addChild: FakeFile.() -> Unit = {} +) : File(parent, path) { + val children: MutableList = mutableListOf() + + init { + addChild() + } + + override fun getParentFile(): File = parent + fun addDirectory(name: String, addChild: FakeFile.() -> Unit = {}) { + children.add( + FakeFile(this, name, true).also { it.addChild() } + ) + } + + override fun exists(): Boolean = true + + fun addFile(name: String) { + children.add(FakeFile(this, name, false)) + } + + override fun listFiles(): Array = children.toTypedArray() + override fun isDirectory(): Boolean = directory + override fun isFile(): Boolean = !directory + override fun createNewFile(): Boolean = true + override fun mkdirs(): Boolean = true + override fun mkdir(): Boolean = true + + fun getAll(): List = walk().toList() + +} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..e1eb231 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,76 @@ +FROM ubuntu:16.04 +LABEL maintainer="Minh Nguyen " + +ENV DEBIAN_FRONTEND noninteractive +ENV LANG C.UTF-8 + +# Default versions +ENV INFLUXDB_VERSION=1.8.2 +ENV GRAFANA_VERSION=9.0.0 + +# Grafana database type +ENV GF_DATABASE_TYPE=sqlite3 + + +WORKDIR /root + +# Clear previous sources +RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" && \ + case "${dpkgArch##*-}" in \ + amd64) ARCH='amd64';; \ + arm64) ARCH='arm64';; \ + armhf) ARCH='armhf';; \ + armel) ARCH='armel';; \ + *) echo "Unsupported architecture: ${dpkgArch}"; exit 1;; \ + esac && \ + rm /var/lib/apt/lists/* -vf \ + # Base dependencies + && apt-get -y update \ + && apt-get -y dist-upgrade \ + && apt-get -y --force-yes install \ + apt-utils \ + ca-certificates \ + curl \ + git \ + htop \ + libfontconfig \ + nano \ + net-tools \ + supervisor \ + wget \ + gnupg \ + && curl -sL https://deb.nodesource.com/setup_10.x | bash - \ + && apt-get install -y nodejs \ + && mkdir -p /var/log/supervisor \ + && rm -rf .profile \ + # Install InfluxDB + && wget --no-verbose https://dl.influxdata.com/influxdb/releases/influxdb_${INFLUXDB_VERSION}_${ARCH}.deb \ + && dpkg -i influxdb_${INFLUXDB_VERSION}_${ARCH}.deb \ + && rm influxdb_${INFLUXDB_VERSION}_${ARCH}.deb \ + # Install Grafana + && wget https://dl.grafana.com/oss/release/grafana_${GRAFANA_VERSION}_${ARCH}.deb \ + && dpkg -i grafana_${GRAFANA_VERSION}_${ARCH}.deb \ + && rm grafana_${GRAFANA_VERSION}_${ARCH}.deb \ + # Cleanup + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ + && mkdir -p /tmp/grafana + + + +# Configure Supervisord and base env +COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY bash/profile .profile + +# Configure InfluxDB +COPY influxdb/influxdb.conf /etc/influxdb/influxdb.conf + +# Configure Grafana +COPY grafana/grafana.ini /etc/grafana/grafana.ini +COPY grafana/grafana.db /tmp/grafana/grafana.db + + +COPY run.sh /run.sh +RUN ["chmod", "+x", "/run.sh"] +CMD ["/run.sh"] + diff --git a/docker/Makefile b/docker/Makefile new file mode 100644 index 0000000..b3d153e --- /dev/null +++ b/docker/Makefile @@ -0,0 +1,17 @@ +## Define a variable for the version of your Docker image +CI_REGISTRY_IMAGE = mikenguyen +VERSION = SNAPSHOT +# Define a variable for the name of your Docker image +IMAGE_NAME = sizer-influx-grafana +IMAGE_REPO = ${CI_REGISTRY_IMAGE}/${IMAGE_NAME} + +all: build tag push +# 'make build' will build your Docker image +build: + docker build -t $(IMAGE_REPO) . + +tag: + docker tag ${IMAGE_REPO} ${IMAGE_REPO}:${VERSION} + +push: + docker push ${IMAGE_REPO}:${VERSION} \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..2d96371 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,81 @@ +# Sizer-influx-grafana + +![Grafana][grafana-version] ![Influx][influx-version] + +This is a Docker image based on the awesome [docker-influxdb-grafana](https://github.com/philhawthorne/docker-influxdb-grafana) from [Phil Hawthorne](https://github.com/philhawthorne). + +## Key Different + +- Newer Grafana version +- Added provisioned app-sizer Dashboard and Datasources +- ChronoGraf is not included in this container + +The main purpose of this image is to be used to demo data from [App Sizer][app-sizer-page]. + +| Component | Version | +|-----------|---------| +| InfluxDB | 1.8.2 | +| Grafana | 9.0.0 | + +## Pre-configured Dashboard + +The Docker image includes a pre-configured dashboard in Grafana: + +

+ +

+ +To use the dashboard, you either to: +- **Default Configuration**: Uses database name `sizer` and measurement (table) named `app_size`. (They are default values configured in the App Sizer tool) +- **Custom Configuration**: + - For a different database name: Update the [Grafana Data Sources](https://grafana.com/docs/grafana/latest/datasources/) named InfluxDB. + - For a different measurement name: Update all [queries](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/) and [variables](https://grafana.com/docs/grafana/latest/dashboards/variables/) in the dashboard. + +To import the dashboard into an existing setup: +1. Use this [JSON file][json-dashboard-file]. +2. Ensure you add the proper Grafana datasource. +3. Update the measurement (table) name in the queries and variables if necessary + +## Quick Start + +To start the container with persistence, you can use our teammate's docker image exported to Docker Hub: + +```sh +docker run -d \ + --name sizer-influxdb-grafana \ + -p 3003:3003 \ + -p 8086:8086 \ + -v /path/for/influxdb:/var/lib/influxdb \ + -v /path/for/grafana:/var/lib/grafana \ + mikenguyen/sizer-influx-grafana:latest +``` + +## Mapped Ports + +| Host | Container | Service | +|------|-----------|---------| +| 3003 | 3003 | Grafana | +| 8086 | 8086 | InfluxDB| + +## Accessing Services + +### Grafana + +- URL: [http://localhost:3003](http://localhost:3003) +- Username: `root` +- Password: `root` + +### InfluxDB + +- Port: 8086 +- Username: `root` +- Password: `root` + + +[json-dashboard-file]: ../grafana/dashboard-to-import.json +[app-sizer-page]: ../docs/index.md +[grafana-version]: https://img.shields.io/badge/Grafana-9.0.0-brightgreen +[influx-version]: https://img.shields.io/badge/Influx-1.8.2-brightgreen + + + diff --git a/docker/bash/profile b/docker/bash/profile new file mode 100644 index 0000000..989fe39 --- /dev/null +++ b/docker/bash/profile @@ -0,0 +1,11 @@ +# ~/.profile: executed by Bourne-compatible login shells. + +if [ "$BASH" ]; then + if [ -f ~/.bashrc ]; then + . ~/.bashrc + fi +fi + +mesg n + +export HOME=/root diff --git a/docker/grafana/grafana.db b/docker/grafana/grafana.db new file mode 100644 index 0000000..5123787 Binary files /dev/null and b/docker/grafana/grafana.db differ diff --git a/docker/grafana/grafana.ini b/docker/grafana/grafana.ini new file mode 100644 index 0000000..05feb3c --- /dev/null +++ b/docker/grafana/grafana.ini @@ -0,0 +1,385 @@ +##################### Grafana Configuration Example ##################### +# +# Everything has defaults so you only need to uncomment things you want to +# change + +# possible values : production, development +; app_mode = production + +# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty +; instance_name = ${HOSTNAME} + +#################################### Paths #################################### +[paths] +# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used) +# +;data = /var/lib/grafana +# +# Directory where grafana can store logs +# +;logs = /var/log/grafana +# +# Directory where grafana will automatically scan and look for plugins +# +;plugins = /var/lib/grafana/plugins + +# +#################################### Server #################################### +[server] +# Protocol (http or https) +protocol = http + +# The ip address to bind to, empty will bind to all interfaces +;http_addr = + +# The http port to use +http_port = 3003 + +# The public facing domain name used to access grafana from a browser +;domain = localhost + +# Redirect to correct domain if host header does not match domain +# Prevents DNS rebinding attacks +;enforce_domain = false + +# The full public facing url you use in browser, used for redirects and emails +# If you use reverse proxy and sub path specify full url (with sub path) +root_url = http://localhost:3003 + +# Log web requests +;router_logging = false + +# the path relative working path +;static_root_path = public + +# enable gzip +;enable_gzip = false + +# https certs & key file +;cert_file = +;cert_key = + +#################################### Database #################################### +[database] +# You can configure the database connection by specifying type, host, name, user and password +# as seperate properties or as on string using the url propertie. + +# Either "mysql", "postgres" or "sqlite3", it's your choice +type = sqlite3 + +# Use either URL or the previous fields to configure the database +# Example: mysql://user:secret@host:port/database +;url = + +# For "postgres" only, either "disable", "require" or "verify-full" +;ssl_mode = disable + +# For "sqlite3" only, path relative to data_path setting +path = grafana.db + +#################################### Session #################################### +[session] +# Either "memory", "file", "redis", "mysql", "postgres", default is "file" +;provider = file + +# Provider config options +# memory: not have any config yet +# file: session dir path, is relative to grafana data_path +# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana` +# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` +# postgres: user=a password=b host=localhost port=5432 dbname=c sslmode=disable +;provider_config = sessions + +# Session cookie name +;cookie_name = grafana_sess + +# If you use session in https only, default is false +;cookie_secure = false + +# Session life time, default is 86400 +;session_life_time = 86400 + +#################################### Analytics #################################### +[analytics] +# Server reporting, sends usage counters to stats.grafana.org every 24 hours. +# No ip addresses are being tracked, only simple counters to track +# running instances, dashboard and error counts. It is very helpful to us. +# Change this option to false to disable reporting. +;reporting_enabled = true + +# Set to false to disable all checks to https://grafana.net +# for new vesions (grafana itself and plugins), check is used +# in some UI views to notify that grafana or plugin update exists +# This option does not cause any auto updates, nor send any information +# only a GET request to http://grafana.net to get latest versions +;check_for_updates = true + +# Google Analytics universal tracking code, only enabled if you specify an id here +;google_analytics_ua_id = + +#################################### Security #################################### +[security] +# default admin user, created on startup +admin_user = root + +# default admin password, can be changed before first start of grafana, or in profile settings +admin_password = root + +# used for signing +;secret_key = SW2YcwTIb9zpOOhoPsMm + +# Auto-login remember days +;login_remember_days = 7 +;cookie_username = grafana_user +;cookie_remember_name = grafana_remember + +# Enable iFrame embedding +allow_embedding = true + +# Set cookie SameSite to Lax so that embedding should work +cookie_samesite = Lax + +# disable gravatar profile images +;disable_gravatar = false + +# data source proxy whitelist (ip_or_domain:port separated by spaces) +;data_source_proxy_whitelist = + +[snapshots] +# snapshot sharing options +;external_enabled = true +;external_snapshot_url = https://snapshots-origin.raintank.io +;external_snapshot_name = Publish to snapshot.raintank.io + +# remove expired snapshot +;snapshot_remove_expired = true + +# remove snapshots after 90 days +;snapshot_TTL_days = 90 + +#################################### Users #################################### +[users] +# disable user signup / registration +;allow_sign_up = true + +# Allow non admin users to create organizations +;allow_org_create = true + +# Set to true to automatically assign new users to the default organization (id 1) +;auto_assign_org = true + +# Default role new users will be automatically assigned (if disabled above is set to true) +;auto_assign_org_role = Viewer + +# Background text for the user field on the login page +;login_hint = email or username + +# Default UI theme ("dark" or "light") +;default_theme = dark + +[auth] +# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false +;disable_login_form = false + +#################################### Anonymous Auth ########################## +[auth.anonymous] +# enable anonymous access +;enabled = false + +# specify organization name that should be used for unauthenticated users +;org_name = Main Org. + +# specify role for unauthenticated users +;org_role = Viewer + +#################################### Github Auth ########################## +[auth.github] +;enabled = false +;allow_sign_up = true +;client_id = some_id +;client_secret = some_secret +;scopes = user:email,read:org +;auth_url = https://github.com/login/oauth/authorize +;token_url = https://github.com/login/oauth/access_token +;api_url = https://api.github.com/user +;team_ids = +;allowed_organizations = + +#################################### Google Auth ########################## +[auth.google] +;enabled = false +;allow_sign_up = true +;client_id = some_client_id +;client_secret = some_client_secret +;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +;auth_url = https://accounts.google.com/o/oauth2/auth +;token_url = https://accounts.google.com/o/oauth2/token +;api_url = https://www.googleapis.com/oauth2/v1/userinfo +;allowed_domains = + +#################################### Generic OAuth ########################## +[auth.generic_oauth] +;enabled = false +;name = OAuth +;allow_sign_up = true +;client_id = some_id +;client_secret = some_secret +;scopes = user:email,read:org +;auth_url = https://foo.bar/login/oauth/authorize +;token_url = https://foo.bar/login/oauth/access_token +;api_url = https://foo.bar/user +;team_ids = +;allowed_organizations = + +#################################### Grafana.net Auth #################### +[auth.grafananet] +;enabled = false +;allow_sign_up = true +;client_id = some_id +;client_secret = some_secret +;scopes = user:email +;allowed_organizations = + +#################################### Auth Proxy ########################## +[auth.proxy] +;enabled = false +;header_name = X-WEBAUTH-USER +;header_property = username +;auto_sign_up = true +;ldap_sync_ttl = 60 +;whitelist = 192.168.1.1, 192.168.2.1 + +#################################### Basic Auth ########################## +[auth.basic] +;enabled = true + +#################################### Auth LDAP ########################## +[auth.ldap] +;enabled = false +;config_file = /etc/grafana/ldap.toml +;allow_sign_up = true + +#################################### SMTP / Emailing ########################## +[smtp] +;enabled = false +;host = localhost:25 +;user = +;password = +;cert_file = +;key_file = +;skip_verify = false +;from_address = admin@grafana.localhost + +[emails] +;welcome_email_on_sign_up = false + +#################################### Logging ########################## +[log] +# Either "console", "file", "syslog". Default is console and file +# Use space to separate multiple modes, e.g. "console file" +;mode = console file + +# Either "trace", "debug", "info", "warn", "error", "critical", default is "info" +;level = info + +# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug +;filters = + + +# For "console" mode only +[log.console] +;level = + +# log line format, valid options are text, console and json +;format = console + +# For "file" mode only +[log.file] +;level = + +# log line format, valid options are text, console and json +;format = text + +# This enables automated log rotate(switch of following options), default is true +;log_rotate = true + +# Max line number of single file, default is 1000000 +;max_lines = 1000000 + +# Max size shift of single file, default is 28 means 1 << 28, 256MB +;max_size_shift = 28 + +# Segment log daily, default is true +;daily_rotate = true + +# Expired days of log file(delete after max days), default is 7 +;max_days = 7 + +[log.syslog] +;level = + +# log line format, valid options are text, console and json +;format = text + +# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used. +;network = +;address = + +# Syslog facility. user, daemon and local0 through local7 are valid. +;facility = + +# Syslog tag. By default, the process' argv[0] is used. +;tag = + + +#################################### AMQP Event Publisher ########################## +[event_publisher] +;enabled = false +;rabbitmq_url = amqp://localhost/ +;exchange = grafana_events + +;#################################### Dashboard JSON files ########################## +[dashboards.json] +;enabled = false +;path = /var/lib/grafana/dashboards + +#################################### Alerting ###################################### +[alerting] +# Makes it possible to turn off alert rule execution. +;execute_alerts = true + +#################################### Internal Grafana Metrics ########################## +# Metrics available at HTTP API Url /api/metrics +[metrics] +# Disable / Enable internal metrics +;enabled = true + +# Publish interval +;interval_seconds = 10 + +# Send internal metrics to Graphite +[metrics.graphite] +# Enable by setting the address setting (ex localhost:2003) +;address = +;prefix = prod.grafana.%(instance_name)s. + +#################################### Internal Grafana Metrics ########################## +# Url used to to import dashboards directly from Grafana.net +[grafana_net] +;url = https://grafana.net + +#################################### External image storage ########################## +[external_image_storage] +# Used for uploading images to public servers so they can be included in slack/email messages. +# you can choose between (s3, webdav) +;provider = + +[external_image_storage.s3] +;bucket_url = +;access_key = +;secret_key = + +[external_image_storage.webdav] +;url = +;username = +;password = diff --git a/docker/influxdb/influxdb.conf b/docker/influxdb/influxdb.conf new file mode 100644 index 0000000..9ee5adb --- /dev/null +++ b/docker/influxdb/influxdb.conf @@ -0,0 +1,439 @@ +### Welcome to the InfluxDB configuration file. + +# The values in this file override the default values used by the system if +# a config option is not specified. The commented out lines are the the configuration +# field and the default value used. Uncommentting a line and changing the value +# will change the value used at runtime when the process is restarted. + +# Once every 24 hours InfluxDB will report usage data to usage.influxdata.com +# The data includes a random ID, os, arch, version, the number of series and other +# usage data. No data from user databases is ever transmitted. +# Change this option to true to disable reporting. +# reporting-disabled = false + +# we'll try to get the hostname automatically, but if it the os returns something +# that isn't resolvable by other servers in the cluster, use this option to +# manually set the hostname +# hostname = "localhost" + +### +### [meta] +### +### Controls the parameters for the Raft consensus group that stores metadata +### about the InfluxDB cluster. +### + +[meta] + # Where the metadata/raft database is stored + dir = "/var/lib/influxdb/meta" + + # Automatically create a default retention policy when creating a database. + # retention-autocreate = true + + # If log messages are printed for the meta service + # logging-enabled = true + +### +### [data] +### +### Controls where the actual shard data for InfluxDB lives and how it is +### flushed from the WAL. "dir" may need to be changed to a suitable place +### for your system, but the WAL settings are an advanced configuration. The +### defaults should work for most systems. +### + +[data] + # The directory where the TSM storage engine stores TSM files. + dir = "/var/lib/influxdb/data" + + # The directory where the TSM storage engine stores WAL files. + wal-dir = "/var/lib/influxdb/wal" + + # Trace logging provides more verbose output around the tsm engine. Turning + # this on can provide more useful output for debugging tsm engine issues. + # trace-logging-enabled = false + + # Whether queries should be logged before execution. Very useful for troubleshooting, but will + # log any sensitive data contained within a query. + # query-log-enabled = true + + # Settings for the TSM engine + + # CacheMaxMemorySize is the maximum size a shard's cache can + # reach before it starts rejecting writes. + # cache-max-memory-size = 1048576000 + + # CacheSnapshotMemorySize is the size at which the engine will + # snapshot the cache and write it to a TSM file, freeing up memory + # cache-snapshot-memory-size = 26214400 + + # CacheSnapshotWriteColdDuration is the length of time at + # which the engine will snapshot the cache and write it to + # a new TSM file if the shard hasn't received writes or deletes + # cache-snapshot-write-cold-duration = "10m" + + # CompactFullWriteColdDuration is the duration at which the engine + # will compact all TSM files in a shard if it hasn't received a + # write or delete + # compact-full-write-cold-duration = "4h" + + # The maximum series allowed per database before writes are dropped. This limit can prevent + # high cardinality issues at the database level. This limit can be disabled by setting it to + # 0. + # max-series-per-database = 1000000 + + # The maximum number of tag values per tag that are allowed before writes are dropped. This limit + # can prevent high cardinality tag values from being written to a measurement. This limit can be + # disabled by setting it to 0. + # max-values-per-tag = 100000 + +### +### [coordinator] +### +### Controls the clustering service configuration. +### + +[coordinator] + # The default time a write request will wait until a "timeout" error is returned to the caller. + # write-timeout = "10s" + + # The maximum number of concurrent queries allowed to be executing at one time. If a query is + # executed and exceeds this limit, an error is returned to the caller. This limit can be disabled + # by setting it to 0. + # max-concurrent-queries = 0 + + # The maximum time a query will is allowed to execute before being killed by the system. This limit + # can help prevent run away queries. Setting the value to 0 disables the limit. + # query-timeout = "0s" + + # The the time threshold when a query will be logged as a slow query. This limit can be set to help + # discover slow or resource intensive queries. Setting the value to 0 disables the slow query logging. + # log-queries-after = "0s" + + # The maximum number of points a SELECT can process. A value of 0 will make the maximum + # point count unlimited. + # max-select-point = 0 + + # The maximum number of series a SELECT can run. A value of 0 will make the maximum series + # count unlimited. + + # The maximum number of series a SELECT can run. A value of zero will make the maximum series + # count unlimited. + # max-select-series = 0 + + # The maxium number of group by time bucket a SELECt can create. A value of zero will max the maximum + # number of buckets unlimited. + # max-select-buckets = 0 + +### +### [retention] +### +### Controls the enforcement of retention policies for evicting old data. +### + +[retention] + # Determines whether retention policy enforcment enabled. + # enabled = true + + # The interval of time when retention policy enforcement checks run. + # check-interval = "30m" + +### +### [shard-precreation] +### +### Controls the precreation of shards, so they are available before data arrives. +### Only shards that, after creation, will have both a start- and end-time in the +### future, will ever be created. Shards are never precreated that would be wholly +### or partially in the past. + +[shard-precreation] + # Determines whether shard pre-creation service is enabled. + # enabled = true + + # The interval of time when the check to pre-create new shards runs. + # check-interval = "10m" + + # The default period ahead of the endtime of a shard group that its successor + # group is created. + # advance-period = "30m" + +### +### Controls the system self-monitoring, statistics and diagnostics. +### +### The internal database for monitoring data is created automatically if +### if it does not already exist. The target retention within this database +### is called 'monitor' and is also created with a retention period of 7 days +### and a replication factor of 1, if it does not exist. In all cases the +### this retention policy is configured as the default for the database. + +[monitor] + # Whether to record statistics internally. + # store-enabled = true + + # The destination database for recorded statistics + # store-database = "_internal" + + # The interval at which to record statistics + # store-interval = "10s" + +### +### [admin] +### +### Controls the availability of the built-in, web-based admin interface. If HTTPS is +### enabled for the admin interface, HTTPS must also be enabled on the [http] service. +### +### NOTE: This interface is deprecated as of 1.1.0 and will be removed in a future release. + +[admin] + # Determines whether the admin service is enabled. + enabled = true + + # The default bind address used by the admin service. + bind-address = ":8083" + + # Whether the admin service should use HTTPS. + # https-enabled = false + + # The SSL certificate used when HTTPS is enabled. + # https-certificate = "/etc/ssl/influxdb.pem" + +### +### [http] +### +### Controls how the HTTP endpoints are configured. These are the primary +### mechanism for getting data into and out of InfluxDB. +### + +[http] + # Determines whether HTTP endpoint is enabled. + # enabled = true + + # The bind address used by the HTTP service. + # bind-address = ":8086" + + # Determines whether HTTP authentication is enabled. + # auth-enabled = false + + # The default realm sent back when issuing a basic auth challenge. + # realm = "InfluxDB" + + # Determines whether HTTP request logging is enable.d + # log-enabled = true + + # Determines whether detailed write logging is enabled. + # write-tracing = false + + # Determines whether the pprof endpoint is enabled. This endpoint is used for + # troubleshooting and monitoring. + # pprof-enabled = true + + # Determines whether HTTPS is enabled. + # https-enabled = false + + # The SSL certificate to use when HTTPS is enabled. + # https-certificate = "/etc/ssl/influxdb.pem" + + # Use a separate private key location. + # https-private-key = "" + + # The JWT auth shared secret to validate requests using JSON web tokens. + # shared-sercret = "" + + # The default chunk size for result sets that should be chunked. + # max-row-limit = 10000 + + # The maximum number of HTTP connections that may be open at once. New connections that + # would exceed this limit are dropped. Setting this value to 0 disables the limit. + # max-connection-limit = 0 + + # Enable http service over unix domain socket + # unix-socket-enabled = false + + # The path of the unix domain socket. + # bind-socket = "/var/run/influxdb.sock" + +### +### [subscriber] +### +### Controls the subscriptions, which can be used to fork a copy of all data +### received by the InfluxDB host. +### + +[subscriber] + # Determines whether the subscriber service is enabled. + # enabled = true + + # The default timeout for HTTP writes to subscribers. + # http-timeout = "30s" + + # Allows insecure HTTPS connections to subscribers. This is useful when testing with self- + # signed certificates. + # insecure-skip-verify = false + + # The path to the PEM encoded CA certs file. If the empty string, the default system certs will be used + # ca-certs = "" + + # The number of writer goroutines processing the write channel. + # write-concurrency = 40 + + # The number of in-flight writes buffered in the write channel. + # write-buffer-size = 1000 + + +### +### [[graphite]] +### +### Controls one or many listeners for Graphite data. +### + +[[graphite]] + # Determines whether the graphite endpoint is enabled. + # enabled = false + # database = "graphite" + # retention-policy = "" + # bind-address = ":2003" + # protocol = "tcp" + # consistency-level = "one" + + # These next lines control how batching works. You should have this enabled + # otherwise you could get dropped metrics or poor performance. Batching + # will buffer points in memory if you have many coming in. + + # Flush if this many points get buffered + # batch-size = 5000 + + # number of batches that may be pending in memory + # batch-pending = 10 + + # Flush at least this often even if we haven't hit buffer limit + # batch-timeout = "1s" + + # UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max. + # udp-read-buffer = 0 + + ### This string joins multiple matching 'measurement' values providing more control over the final measurement name. + # separator = "." + + ### Default tags that will be added to all metrics. These can be overridden at the template level + ### or by tags extracted from metric + # tags = ["region=us-east", "zone=1c"] + + ### Each template line requires a template pattern. It can have an optional + ### filter before the template and separated by spaces. It can also have optional extra + ### tags following the template. Multiple tags should be separated by commas and no spaces + ### similar to the line protocol format. There can be only one default template. + # templates = [ + # "*.app env.service.resource.measurement", + # # Default template + # "server.*", + # ] + +### +### [collectd] +### +### Controls one or many listeners for collectd data. +### + +[[collectd]] + # enabled = false + # bind-address = ":25826" + # database = "collectd" + # retention-policy = "" + # + # The collectd service supports either scanning a directory for multiple types + # db files, or specifying a single db file. + # typesdb = "/usr/local/share/collectd" + # + # security-level = "none" + # auth-file = "/etc/collectd/auth_file" + + # These next lines control how batching works. You should have this enabled + # otherwise you could get dropped metrics or poor performance. Batching + # will buffer points in memory if you have many coming in. + + # Flush if this many points get buffered + # batch-size = 5000 + + # Number of batches that may be pending in memory + # batch-pending = 10 + + # Flush at least this often even if we haven't hit buffer limit + # batch-timeout = "10s" + + # UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max. + # read-buffer = 0 + +### +### [opentsdb] +### +### Controls one or many listeners for OpenTSDB data. +### + +[[opentsdb]] + # enabled = false + # bind-address = ":4242" + # database = "opentsdb" + # retention-policy = "" + # consistency-level = "one" + # tls-enabled = false + # certificate= "/etc/ssl/influxdb.pem" + + # Log an error for every malformed point. + # log-point-errors = true + + # These next lines control how batching works. You should have this enabled + # otherwise you could get dropped metrics or poor performance. Only points + # metrics received over the telnet protocol undergo batching. + + # Flush if this many points get buffered + # batch-size = 1000 + + # Number of batches that may be pending in memory + # batch-pending = 5 + + # Flush at least this often even if we haven't hit buffer limit + # batch-timeout = "1s" + +### +### [[udp]] +### +### Controls the listeners for InfluxDB line protocol data via UDP. +### + +[[udp]] + # enabled = false + # bind-address = ":8089" + # database = "udp" + # retention-policy = "" + + # These next lines control how batching works. You should have this enabled + # otherwise you could get dropped metrics or poor performance. Batching + # will buffer points in memory if you have many coming in. + + # Flush if this many points get buffered + # batch-size = 5000 + + # Number of batches that may be pending in memory + # batch-pending = 10 + + # Will flush at least this often even if we haven't hit buffer limit + # batch-timeout = "1s" + + # UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max. + # read-buffer = 0 + +### +### [continuous_queries] +### +### Controls how continuous queries are run within InfluxDB. +### + +[continuous_queries] + # Determiens whether the continuous query service is enabled. + # enabled = true + + # Controls whether queries are logged when executed by the CQ service. + # log-enabled = true + + # interval for how often continuous queries will be checked if they need to run + # run-interval = "1s" diff --git a/docker/run.sh b/docker/run.sh new file mode 100644 index 0000000..c4b074a --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,39 @@ +#!/bin/bash -e + +# +# MIT License +# +# Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE +# + +# We need to ensure this directory is writeable on start of the container +chmod 0777 /var/lib/grafana +# When Grafana runs for the first time, it copies the sample "App Download Size Breakdown" dashboard. +# The dashboard is copied here to ensure it also works with mounted volumes ("/var/lib/grafana/") using Docker's -v option. +# If grafana.db does not already exist in /var/lib/grafana, then the file is copied from /tmp/grafana to /var/lib/grafana. +if [ ! -f /var/lib/grafana/grafana.db ]; then + cp /tmp/grafana/grafana.db /var/lib/grafana/grafana.db +fi + +exec /usr/bin/supervisord \ No newline at end of file diff --git a/docker/supervisord/supervisord.conf b/docker/supervisord/supervisord.conf new file mode 100644 index 0000000..6f68494 --- /dev/null +++ b/docker/supervisord/supervisord.conf @@ -0,0 +1,11 @@ +[supervisord] +nodaemon = true + +[program:influxdb] +priority = 1 +command = /usr/bin/influxd -pidfile /var/run/influxdb/influxd.pid -config /etc/influxdb/influxdb.conf + +[program:grafana] +priority = 3 +command = /usr/sbin/grafana-server --homepath=/usr/share/grafana --pidfile=/var/run/grafana-server.pid --config=/etc/grafana/grafana.ini --packaging=deb cfg:default.paths.provisioning=/etc/grafana/provisioning cfg:default.paths.data=/var/lib/grafana cfg:default.paths.logs=/var/log/grafana cfg:default.paths.plugins=/var/lib/grafana/plugins cfg:default.paths.logs=/var/log/grafana + diff --git a/docker/system/99fixbadproxy b/docker/system/99fixbadproxy new file mode 100644 index 0000000..f2901b0 --- /dev/null +++ b/docker/system/99fixbadproxy @@ -0,0 +1,4 @@ +Acquire::http::Pipeline-Depth "0"; +Acquire::http::No-Cache=True; +Acquire::BrokenProxy=true; + diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..1b97558 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,131 @@ +# App Sizer CLI + +App Sizer provides a Command Line Interface (CLI) to cater to non-Gradle build systems, offering the same comprehensive features as the Gradle plugin. + + +## Getting Started + +1. Generate the command line binary file: + ``` + ./gradlew clt:shadowJar + ``` + +2. Create your config file following [this template](../cli-config-template.yml). + +3. Run the analysis using the command line tool: + ``` + java -jar clt-all.jar --config-file ./path/to/config/your-config-file.yml + ``` + +## Configuration + +The App Sizer CLI accepts a YAML file as configuration ([template](../cli-config-template.yml)). The file consists of three main blocks: + +```yaml +project-input: + # Configure the input for the project +apk-generation: + # APK Generation configuration +report: + # Output Configuration +``` + +### Project Input + +| Property | Description | +|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| libraries-directory | Path to all SDK & library binaries that your project depends on. In Gradle, you can save all dependencies in a folder by setting a new Gradle home path for your build with the `-g` option. Example: `./gradlew assembleRelease -g ./new-gradle`. Then all dependency binaries will be saved to `./new-gradle/caches/modules-2/files-2.1` | +| modules-directory | Path to all of your modules' AAR & Jar files. In Gradle, you can run `assembleDebug` in the project root folder to build all AAR/Jar files, then set the root folder for this property | +| modules-dir-is-project-root | Boolean value. Enable this flag if you set the root project as the modules-directory to optimize performance | +| r8-mapping-file | Path to the R8 mapping file if you enable R8 for your build | +| owner-mapping-file | Path to YAML file mapping project modules to team owners | +| version | Your app version | +| large-file-threshold | File size threshold (in bytes) for considering a file as large | +| project-name | Project name | + +And example of `owner-mapping-file`: + +```yaml +Platform: + - app +Team1: + - android-module-level1 + - kotlin-module +Team2: + - sample-group:android-module-level2 +``` + +### APK Generation + +| Property | Description | +|----------|-------------| +| bundle-tool | Path to the [bundletool](https://github.com/google/bundletool) JAR file | +| app-bundle-file | Path to the app bundle file (aab) | +| device-specs | List of [device specification](https://developer.android.com/tools/bundletool#generate_use_json) files for APK generation | +| key-signing | Key signing information | + +### Output Configuration + +| Property | Description | +|----------|-------------| +| output-directory | Directory to save markdown and JSON reports | +| custom-attributes | Map of additional attributes to include in every report row | +| influx-db-config | InfluxDB configuration (see below) | + +#### InfluxDB Configuration + +| Property | Description | +|----------|-------------| +| url | URL of the InfluxDB server | +| db-name | Name of the InfluxDB database | +| report-table-name | Measurement name for storing report data | +| username | InfluxDB username (optional) | +| password | InfluxDB password (optional) | +| retention-policy | InfluxDB retention policy configuration (optional) | + +## Full Configuration Example + +```yaml +project-input: + libraries-directory: "./build/gradle-cache/caches/modules-2/files-2.1" + modules-directory: "./" + modules-dir-is-project-root: true + r8-mapping-file: "./app/build/outputs/mapping/proDebug/mapping.txt" + owner-mapping-file: "./module-owner.yml" + version: "1.0.1" + large-file-threshold: 10 + project-name: "sample" +apk-generation: + bundle-tool: "./binary/bundletool-all-1.15.4.jar" + app-bundle-file: "./app/build/outputs/bundle/proDebug/sample-bundle-file-pro-debug.aab" + device-specs: + - "./app-size-config/device-1.json" + - "./app-size-config/device-2.json" + key-signing: + keystore-file: "./buildsystem/sample-release.keystore" + keystore-pw: "12345678" + key-alias: "key0" + key-pw: "12345678" +report: + output-directory: "./build/app-sizer" + custom-attributes: + pipelineId: "100" + influx-db-config: + db-name: "sizer" + url: "http://localhost:8086" + username: "root" + password: "root" + report-table-name: "app_size" + retention-policy: + name: "app_sizer" + duration: "360d" + shard-duration: "0m" + replication-factor: 2 + is-default: true +``` + + +## Resources + +- [Bundletool GitHub Repository](https://github.com/google/bundletool) +- [InfluxDB Documentation](https://www.influxdata.com/time-series-platform/) \ No newline at end of file diff --git a/docs/css/site.css b/docs/css/site.css new file mode 100644 index 0000000..24a3ef0 --- /dev/null +++ b/docs/css/site.css @@ -0,0 +1,6 @@ +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4 { + font-weight: 500; +} \ No newline at end of file diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..ea026d8 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,81 @@ +# Sizer-influx-grafana + +![Grafana][grafana-version] ![Influx][influx-version] + +This is a Docker image based on the awesome [docker-influxdb-grafana](https://github.com/philhawthorne/docker-influxdb-grafana) from [Phil Hawthorne](https://github.com/philhawthorne). + +## Key Different + +- Newer Grafana version +- Added provisioned app-sizer Dashboard and Datasources +- ChronoGraf is not included in this container + +The main purpose of this image is to be used to demo data from [App Sizer][app-sizer-page]. + +| Component | Version | +|-----------|---------| +| InfluxDB | 1.8.2 | +| Grafana | 9.0.0 | + +## Pre-configured Dashboard + +The Docker image includes a pre-configured dashboard in Grafana: + +

+ +

+ +To use the dashboard, you either to: +- **Default Configuration**: Uses database name `sizer` and measurement (table) named `app_size`. (They are default values configured in the App Sizer tool) +- **Custom Configuration**: + - For a different database name: Update the [Grafana Data Sources](https://grafana.com/docs/grafana/latest/datasources/) named InfluxDB. + - For a different measurement name: Update all [queries](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/) and [variables](https://grafana.com/docs/grafana/latest/dashboards/variables/) in the dashboard. + +To import the dashboard into an existing setup: +1. Use this [JSON file][json-dashboard-file]. +2. Ensure you add the proper Grafana datasource. +3. Update the measurement (table) name in the queries and variables if necessary + +## Quick Start + +To start the container with persistence, you can use our teammate's docker image exported to Docker Hub: + +```sh +docker run -d \ + --name sizer-influxdb-grafana \ + -p 3003:3003 \ + -p 8086:8086 \ + -v /path/for/influxdb:/var/lib/influxdb \ + -v /path/for/grafana:/var/lib/grafana \ + mikenguyen/sizer-influx-grafana:latest +``` + +## Mapped Ports + +| Host | Container | Service | +|------|-----------|---------| +| 3003 | 3003 | Grafana | +| 8086 | 8086 | InfluxDB| + +## Accessing Services + +### Grafana + +- URL: [http://localhost:3003](http://localhost:3003) +- Username: `root` +- Password: `root` + +### InfluxDB + +- Port: 8086 +- Username: `root` +- Password: `root` + + +[json-dashboard-file]: ../grafana/dashboard-to-import.json +[app-sizer-page]: ./index.md +[grafana-version]: https://img.shields.io/badge/Grafana-9.0.0-brightgreen +[influx-version]: https://img.shields.io/badge/Influx-1.8.2-brightgreen + + + diff --git a/docs/images/dashboard.gif b/docs/images/dashboard.gif new file mode 100644 index 0000000..b218512 Binary files /dev/null and b/docs/images/dashboard.gif differ diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000..c02732e Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/images/task-graph.png b/docs/images/task-graph.png new file mode 100644 index 0000000..bd8f08d Binary files /dev/null and b/docs/images/task-graph.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6bd6260 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,152 @@ +# App Sizer + +## Overview +App Sizer is a tool designed to analyze the download size of Android applications. By providing detailed insights into the composition of your app's binary, App Sizer helps developers identify areas for size reduction, ultimately improving user acquisition and retention rates. + +*The app download size in Android refers to the amount of data a user needs to download from an app store (typically Google Play Store) to install an application on their Android device* + +

+ +

+ +## Key Features +App Sizer offers comprehensive analysis including: +1. Total app download size +2. Detailed size breakdown +3. Size contribution by teams +4. Module-wise size contribution +5. Size contribution by libraries +6. List of large files + +Reports are generated based on the provided Android device specifications. Our [blogpost][blog-post] introduce the tool features + +## Quick Start + +App Sizer provides two flexible integration methods: + +* A Gradle plugin that seamlessly integrates with your Android Gradle project. +* A command-line tool to cater to non-Gradle build systems, offering the same comprehensive features. + + *Note: The command-line option was the original implementation and remains supported for broader compatibility.* + +### Gradle Plugin Integration +In root `build.gradle`: + +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "com.grab:app-sizer:SNAPSHOT" + } +} +``` +In the app module 's `build.gradle` +```groovy +apply plugin: "com.grab.app-sizer" + +// AppSizer configuration +appSizer { + // DSL +} +``` + +To run analysis, execute + +``` +./gradlew app:appSizeAnalysisRelease --no-configure-on-demand --no-configuration-cache +``` + +For plugin configuration options, see [Plugin Configuration][plugin_doc]. + +### Cli Tool Integration +To generate the command line binary file, execute +```text +./gradlew clt:shadowJar +``` + +To run analysis using the command line tool, execute +```text +java -jar clt-all.jar --config-file ./path/to/config/app-size-settings.yml +``` + +For command line configuration options, see [Commandline Configuration][cli_doc]. + +## Report Types + +App Sizer currently supports three types of reports: + +* InfluxDB database (1.x) - suitable for CI tracking and enabling the creation of customized dashboards. For InfluxDB and Grafana setup, see our [Docker Setup Guide][grafana-docker]. +* Markdown table for convenient local analysis. +* JSON data for compatibility with other platforms. + +*The Markdown & Json reports are saved as [option]-report.md in the configured output folder (default: app/build/sizer/reports)* + +For more detail on reports, see [Report Detail][report_doc] + +## How it works +App Sizer functions as a mapping tool to generate the report. It takes APK, AAR, and JAR files as inputs. +1. **Input parsing**: +- The tool parses the APK down to file and class levels. It calculates the contribution of each component to the total app download size. +- Similarly, App Sizer parses AAR and JAR files. +2. **Mapping and Report Generation**: +- The tool then maps the APK components to their corresponding elements in the AAR and JAR files. +- Based on this analysis and other metadata, App Sizer generates comprehensive reports detailing size contributions. + +## Limitations + +App Sizer approximates class download sizes due to Dex structure complexity, and may not accurately attribute sizes for inline functions or uncategorized files. Results should be interpreted as close estimates, best used for identifying trends and relative size comparisons rather than exact measurements. + +For more details on limitations, see the [Limitation][limitation_doc]. + +## Components +* [Gradle Plugin][gradle-plugin] +* [Command line tool][commandline-tool] +* [InfluxDb & Grafana Docker][grafana-docker] + +## Contributing + +If you find any issues or have suggestions for improvements, please open an issue or submit a pull request to the App Sizer repository. + +## License + +``` +MIT License + + +Copyright 2024 Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE +``` + +[report_doc]: ./report.md +[plugin_doc]: ./plugin.md +[cli_doc]: ./cli.md +[limitation_doc]:./limitation.md +[gradle-plugin]: ../gradle-plugin +[commandline-tool]: ../clt +[grafana-docker]: ../docker +[blog-post]: https://engineering.grab.com/project-bonsai + + + diff --git a/docs/limitation.md b/docs/limitation.md new file mode 100644 index 0000000..7525f48 --- /dev/null +++ b/docs/limitation.md @@ -0,0 +1,53 @@ +# Limitations + +App Sizer is a powerful tool, but it has some limitations that users should be aware of. This document outlines the key limitations and explains their impact on the analysis results. + +## Class Download Size Calculation + +Calculating the exact download size of a class from an APK is challenging. App Sizer uses an approximation method: + +1. We obtain a relative [size of the class definition][class-size] (termed 'raw size'). +2. We use the Dex file download size that the class belongs to. +3. We derive a relative value for the class's download size using the formula: + + ``` + class's download size = class raw size * (dex download size / all classes' raw size) + ``` + +This approach provides a reasonable estimation but may not be 100% accurate. Interestingly, similar tools in the community have independently developed comparable methods. + +## Files Grouped Under "Others" + +### resources.arsc File +The `resources.arsc` file is a special file in Android APKs containing precompiled resources (such as binary XML for strings, arrays, and other value types) in a binary format for efficient access. + +- App Sizer does not analyze this file individually. +- It's grouped under the "Others" category. +- For small Android projects, this can disproportionately impact the data, potentially creating the illusion of an inefficient analysis. + +### Uncategorized Files +* Any files that cannot be categorized as Java/Kotlin code, resources, native libraries, or assets are automatically distributed to the app module and grouped under the **"Others"** category. +* Any files/classes that cannot find an owner (does not belong to a module or library) are automatically distributed to the app module + +## Inline Functions and Classes + +The nature of [inline functions][inline-functions] and [inline value classes][inline-class] in Kotlin presents a unique challenge: + +- The size contributed by inline elements is calculated and distributed to where they are used, not where the inline methods/classes are created. +- Build systems or optimization tools like R8 might rewrite code for efficiency, including inlining methods, which can result in similar outcomes to inline functions. + +This behavior can make it difficult to accurately attribute size contributions to specific modules or libraries. + +## Impact on Analysis + +These limitations mean that App Sizer's results should be interpreted as close approximations rather than exact measurements. They are most useful for: + +- Identifying trends in app size growth +- Comparing relative size contributions of different components +- Spotting large, unexpected size increases + +Users should keep these limitations in mind when making decisions based on App Sizer's output, especially for small projects or when dealing with inline-heavy codebases. + +[class-size]: https://github.com/JesusFreke/smali/blob/master/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedClassDef.java#L505 +[inline-functions]: https://kotlinlang.org/docs/inline-functions.html +[inline-class]: https://kotlinlang.org/docs/inline-classes.html \ No newline at end of file diff --git a/docs/plugin.md b/docs/plugin.md new file mode 100644 index 0000000..3a43928 --- /dev/null +++ b/docs/plugin.md @@ -0,0 +1,215 @@ +# App Sizer Plugin +App Sizer provide the app sizer gradle plugin as the option to seamlessly integrates with your Android Gradle project. This option is recommended. + +## Getting Started + +1. Add the plugin to your root `build.gradle` + +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "com.grab:app-sizer:SNAPSHOT" + } +} +``` + +2. Apply the plugin in your app module's `build.gradle` + +```groovy +apply plugin: "com.grab.app-sizer" + +appSizer { + // Configuration goes here +} +``` + +3. Run the analysis + +```bash +./gradlew app:appSizeAnalysisRelease --no-configure-on-demand --no-configuration-cache +``` + +## Configuration +Use the registered `appSizer` extension block to the app module's `build.gradle` to configure App Sizer Plugin + +```groovy +appSizer { + enabled = true + projectInput { + // config the input for the plugin + } + metrics { + // config the output for the plugin + } +} +``` +* **enabled**: Given the App Sizer Plugin has not supported configuration on demand & configuration catching. We provide you an option to turned of the plugin just in case it impact your gradle configuration performance. + +### Project Input +Configure the input for the project: + +```groovy +appSizer { + projectInput { + largeFileThreshold = 10 + teamMappingFile = file("${rootProject.rootDir}/module-owner.yml") + enableMatchDebugVariant = true + variantFilter { variant -> + variant.setIgnore(variant.flavors.contains("ignore-flavor")) + } + apk { + // APK Generation + } + } + ... +} +``` + +| Property | Description | +|----------|----------------------------------------------------------------| +| `largeFileThreshold` | File size threshold (in bytes) for considering a file as large. | +| `teamMappingFile` | YAML file mapping project modules to team owners. | +| `enableMatchDebugVariant` | If true, uses debug AAR files to improve build performance. | +| `variantFilter` | Specifies which variants to exclude from analysis. | + +And example of `teamMappingFile`: + +```yaml +Platform: + - app +Team1: + - android-module-level1 + - kotlin-module +Team2: + - sample-group:android-module-level2 +``` + +### APK Generation + +Configure APK generation settings: + +```groovy +appSizer { + projectInput { + ... + apk { + deviceSpecs = [ + file("${rootProject.rootDir}/app-size-config/device-1.json"), + file("${rootProject.rootDir}/app-size-config/device-2.json") + ] + bundleToolFile = file("${rootProject.rootDir}/binary/bundletool-all-1.15.4.jar") + } + } + ... +} +``` + +| Property | Description | +|----------|----------------------------------------------------------------------------------------------------------------------------| +| `deviceSpecs` | List of [device specification](https://developer.android.com/tools/bundletool#generate_use_json) files for APK generation. | +| `bundleToolFile` | Path to the [bundletool](https://github.com/google/bundletool) JAR file. | + +### Output Configuration + +Configure the reporting output: + +```groovy +appSizer { + ... + metrics { + influxDB { + dbName = "sizer" + reportTableName = "app_size" + url = "http://localhost:8086" + username = "db-username" + password = "db-pw" + } + local { + outputDirectory = project.layout.buildDirectory.dir("app-sizer") + } + customAttributes.putAll( + ["pipeline_id": "1001"] + ) + } +} +``` + +| Property | Description | +|----------|-----------------------------------------------------------------------------------| +| `local.outputDirectory` | Directory to save markdown and JSON reports (default is `app/build/sizer/reports`)| +| `customAttributes` | Map of additional attributes to include in every report row. | + +#### InfluxDB Configuration + +| Property | Description | +|----------|-----------------------------------------------------| +| `dbName` | Name of the InfluxDB database. | +| `reportTableName` | Measurement name for storing report data. | +| `url` | URL of the InfluxDB server. | +| `username` | InfluxDB username (optional). | +| `password` | InfluxDB password (optional). | +| `retentionPolicy` | InfluxDB retention policy configuration (optional). | + +## Full Configuration Example + +```groovy +appSizer { + enabled = true + projectInput { + apk { + bundleToolFile = file("${rootProject.rootDir}/binary/bundletool-all-1.15.4.jar") + deviceSpecs = [ + file("${rootProject.rootDir}/app-size-config/device-1.json"), + file("${rootProject.rootDir}/app-size-config/device-2.json") + ] + } + variantFilter { variant -> + variant.setIgnore(variant.flavors.contains("gea")) + } + enableMatchDebugVariant = true + largeFileThreshold = 10 + teamMappingFile = file("${rootProject.rootDir}/module-owner.yml") + } + metrics { + influxDB { + dbName = "sizer" + reportTableName = "app_size" + url = "http://localhost:8086" + username = "root" + password = "root" + retentionPolicy { + name = "app_sizer" + duration = "360d" + shardDuration = "0m" + replicationFactor = 2 + setAsDefault = true + } + } + local { + outputDirectory = project.layout.buildDirectory.dir("app-sizer") + } + customAttributes.putAll( + ["pipeline_id": "1001"] + ) + } +} +``` + +## Task Graph + +

+ +

+ +## Troubleshooting + +- If you encounter issues with the `verifyResourceRelease` task, try enabling `enableMatchDebugVariant`. +- Ensure that the `bundletool` JAR file is correctly referenced in your configuration. + +## Resources + +- [Bundletool GitHub Repository](https://github.com/google/bundletool) +- [InfluxDB Documentation](https://www.influxdata.com/time-series-platform/) diff --git a/docs/report.md b/docs/report.md new file mode 100644 index 0000000..13c75fe --- /dev/null +++ b/docs/report.md @@ -0,0 +1,132 @@ +# Reports + +App Sizer supports three types of reports to cater to different use cases and environments: + +1. InfluxDB database (1.x) +2. Markdown tables +3. JSON data + +## InfluxDB Database + +InfluxDB (1.x) is recommended for CI tracking and creating customized dashboards. It's ideal for integrating the tool into your CI pipeline to track historical reports between releases. + +### Setup + +We provide a Docker image with InfluxDB (1.x) and Grafana pre-configured: + +```sh +docker run -d \ + --name sizer-influxdb-grafana \ + -p 3003:3003 \ + -p 3004:8083 \ + -p 8086:8086 \ + -v /path/for/influxdb:/var/lib/influxdb \ + -v /path/for/grafana:/var/lib/grafana \ + mikenguyen/sizer-influx-grafana:latest +``` + +For more details on the Docker setup, see our [Docker guide][grafana-docker]. + +### Dashboard + +A default **App Download Size Breakdown** dashboard is included in the Grafana docker instance. If you have an existing InfluxDB and Grafana setup, you can import our dashboard using this [JSON file](../grafana/dashboard-to-import.json). + +## Markdown Tables + +Markdown tables provide a convenient format for local analysis. The report is saved as `[option]-report.md` in the configured output folder (default: `app/build/sizer/reports`). + +### Example: Module-wise Size Contribution + +| Contributor | Owner | Size | +|-------------|-------|------| +| app | Platform | 90.078 KB | +| android-module-level2 | Team2 | 123.968 KB | +| android-module-level1 | Team1 | 124.042 KB | +| kotlin-module | Team2 | 248.326 KB | + +## JSON Report + +JSON reports offer compatibility with other platforms and tools. The report is saved as `[option]-metrics.json` in the configured output folder. + +### JSON Structure + +Here's a sample of the JSON structure: + +```json +[ + { + "name": "apk", + "fields": [ + { + "name": "size", + "value": "1789199", + "value_type": "integer" + }, + { + "name": "pipeline_id", + "value": "1001", + "value_type": "string" + } + ], + "tags": [ + { + "name": "contributor", + "value": "apk", + "value_type": "string" + }, + { + "name": "project", + "value": "sample", + "value_type": "string" + }, + { + "name": "app_version", + "value": "1.0.9", + "value_type": "string" + }, + { + "name": "build_type", + "value": "proDebug", + "value_type": "string" + }, + { + "name": "device_name", + "value": "device-1", + "value_type": "string" + } + ], + "timestamp": 1720248703061 + }, + // More measurements... +] +``` + +### JSON Fields Explanation + +Each object in the array represents a single database row and contains the following properties: + +1. `name`: The measurement name (e.g., "apk") +2. `fields`: An array of fields containing numerical or custom data +3. `tags`: Each measurement includes relevant tags such as the project name, app version, build type, and device name, allowing for detailed analysis and filtering of the data. +4. `timestamp`: Unix timestamp (in milliseconds) when the measurement was taken + +### Using the JSON Report + +The JSON format makes it easy to: + +1. Import the data into various tools/databases +2. Integrate with other CI/CD processes +3. Perform programmatic analysis of app size trends over time + +You can parse this JSON data using any standard JSON library in your preferred programming language to extract and analyze the information as needed for your project. + +## Customizing Reports + +You can customize the reports by modifying the configuration in your Gradle plugin or CLI tool setup. For more details, refer to the [Plugin Configuration][plugin_doc] or [CLI Configuration][cli_doc] guides. + +[grafana-docker]: ../docker +[grafana-dashboard]: ../grafana/dashboard-to-import.json +[plugin_doc]: ./plugin.md +[cli_doc]: ./cli.md + + diff --git a/docs/task_graph.md b/docs/task_graph.md new file mode 100644 index 0000000..eb8e12f --- /dev/null +++ b/docs/task_graph.md @@ -0,0 +1,10 @@ +```mermaid +flowchart TD + A(generateApkDebug) + B(generateArchiveDepDebug) + C(appSizeAnalysisDebug) + + + C --> A + C --> B +``` diff --git a/docs/video/dashboard.mov b/docs/video/dashboard.mov new file mode 100644 index 0000000..356d15c Binary files /dev/null and b/docs/video/dashboard.mov differ diff --git a/gradle-plugin/build.gradle b/gradle-plugin/build.gradle new file mode 100644 index 0000000..05b3c56 --- /dev/null +++ b/gradle-plugin/build.gradle @@ -0,0 +1,67 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Gradle plugin project to get you started. + * For more details take a look at the Writing Custom Plugins chapter in the Gradle + * User Manual available at https://docs.gradle.org/7.5.1/userguide/custom_plugins.html + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id "com.grab.sizer.kotlin" + id 'java-gradle-plugin' + alias(libs.plugins.kotlin.dsl) +} + +dependencies { + compileOnly libs.android.gradle.plugin + compileOnly libs.android.gradle.api + + implementation project(':app-sizer') + implementation libs.google.dagger + implementation libs.gson + kapt libs.dagger.compiler + + testImplementation libs.junit + testImplementation gradleTestKit() + testImplementation libs.android.gradle.plugin + testImplementation libs.android.gradle.api +} + +gradlePlugin { + plugins { + appSizerPlugin { + id = 'com.grab.app-sizer' + implementationClass = 'com.grab.plugin.sizer.AppSizerPlugin' + displayName = "app-sizer-plugin" + } + } +} + diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/AppSizePluginExtension.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/AppSizePluginExtension.kt new file mode 100644 index 0000000..cc6eb10 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/AppSizePluginExtension.kt @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer + +import com.grab.plugin.sizer.configuration.InputExtension +import com.grab.plugin.sizer.configuration.MetricExtension +import org.gradle.api.Action +import org.gradle.api.Project + + +open class AppSizePluginExtension(val project: Project) { + var enabled = true + + /** + * This is a workaround, by default, the ArchiveDep task haven't supported catching yet + * This flag to force the task become cacheable by default. It's not recommend to enable this flag + */ + var archiveDepTaskCacheable = false + var input = project.objects.newInstance(InputExtension::class.java, project.objects) + var metrics = project.objects.newInstance(MetricExtension::class.java, project) + + fun projectInput(action: Action) { + action.execute(input) + } + + fun projectInput(block: InputExtension.() -> Unit) { + block(input) + } + + fun metrics(block: MetricExtension.() -> Unit) { + block(metrics) + } + + fun metrics(action: Action) { + action.execute(metrics) + } + +} + + diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/AppSizerPlugin.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/AppSizerPlugin.kt new file mode 100644 index 0000000..143ebeb --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/AppSizerPlugin.kt @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer + +import org.gradle.api.Plugin +import org.gradle.api.Project + +private const val PLUGIN_EXTENSION = "appSizer" + +class AppSizerPlugin : Plugin { + override fun apply(project: Project) = + TaskManager( + project, + project.extensions.create(PLUGIN_EXTENSION, AppSizePluginExtension::class.java) + ).configTasks() +} diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/ProjectParams.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/ProjectParams.kt new file mode 100644 index 0000000..5a589a5 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/ProjectParams.kt @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer + +import com.grab.sizer.AnalyticsOption +import org.gradle.api.Project + +private const val DEVICE_NAME_PARAM = "deviceName" +private const val PIPELINE_ID_PARAM = "pipeline" +private const val OPTION_PARAM = "option" +private const val LIBRARY_NAME_PARAM = "library" +private const val DEVICE_SPEC_PARAM = "deviceSpec" + +internal interface ProjectParams { + fun option(): AnalyticsOption + fun libraryName(): String? +} + +internal fun Project.params(): ProjectParams = DefaultProjectParams(this) + +private class DefaultProjectParams(private val project: Project) : ProjectParams { + override fun option(): AnalyticsOption = AnalyticsOption.fromString(project.findProperty(OPTION_PARAM) as String?) + override fun libraryName(): String? = (project.findProperty(LIBRARY_NAME_PARAM) as String?) +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/TaskManager.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/TaskManager.kt new file mode 100644 index 0000000..ccecaf7 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/TaskManager.kt @@ -0,0 +1,168 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.api.BaseVariant +import com.android.build.gradle.internal.dsl.BuildType +import com.android.build.gradle.internal.dsl.ProductFlavor +import com.android.build.gradle.internal.tasks.factory.dependsOn +import com.grab.plugin.sizer.configuration.DefaultVariantFilter +import com.grab.plugin.sizer.dependencies.* +import com.grab.plugin.sizer.tasks.AppSizeAnalysisTask +import com.grab.plugin.sizer.tasks.GenerateApkTask +import com.grab.plugin.sizer.tasks.GenerateArchivesListTask +import com.grab.plugin.sizer.utils.isAndroidApplication +import com.grab.plugin.sizer.utils.isAndroidLibrary +import com.grab.plugin.sizer.utils.isJava +import com.grab.plugin.sizer.utils.isKotlinJvm +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.the + +/* + * This internal `TaskManager` class is used for configuring tasks for the Plugin + * + * The `TaskManager` class is responsible for setting up tasks based on plugin extensions. + * It evaluates and applies tasks to projects based on the configuration found in a provided `AppSizePluginExtension`. + */ +internal class TaskManager( + private val project: Project, + private val pluginExtension: AppSizePluginExtension +) { + fun configTasks() { + project.rootProject.gradle.projectsEvaluated { + if (pluginExtension.enabled && project.isAndroidApplication) { + configAppSizeTask(project) + } + } + } + + private fun configAppSizeTask(project: Project) { + with(project.the()) { + applicationVariants.forEach { variant -> + val variantFilter = DefaultVariantFilter(variant) + pluginExtension.input.variantFilter?.execute(variantFilter) + if (!variantFilter.ignored) { + val generateApkTask = GenerateApkTask.registerTask( + project, + pluginExtension, + variant + ) + + val generateArchivesListTask = GenerateArchivesListTask.registerTask( + project, + variant = variant, + flavorMatchingFallbacks = getProductFlavor(variant)?.matchingFallbacks ?: emptyList(), + buildTypeMatchingFallbacks = getOriginalBuildType(variant).matchingFallbacks, + enableMatchDebugVariant = pluginExtension.input.enableMatchDebugVariant, + archiveDepTaskCacheable = pluginExtension.archiveDepTaskCacheable + ) + + val appSizeAnalysisTask = AppSizeAnalysisTask.registerTask( + project, + variant, + pluginExtension, + generateApkTask, + generateArchivesListTask, + ) + registerAppSizeTaskDep(project, variant, this, appSizeAnalysisTask) + } + } + } + } + + private fun registerAppSizeTaskDep( + project: Project, + variant: BaseVariant, + appExtension: AppExtension, + depTask: TaskProvider + ) { + val dependenciesComponent = DaggerDependenciesComponent.factory().create( + project = project, + variantInput = variant.toVariantInput(), + flavorMatchingFallbacks = appExtension.getProductFlavor(variant)?.matchingFallbacks ?: emptyList(), + buildTypeMatchingFallbacks = appExtension.getOriginalBuildType(variant).matchingFallbacks, + enableMatchDebugVariant = pluginExtension.input.enableMatchDebugVariant + ) + val markAsChecked = mutableSetOf() + dfs(project, markAsChecked, dependenciesComponent, depTask) + } + + private fun dfs( + project: Project, + markAsChecked: MutableSet, + dependenciesComponent: DependenciesComponent, + depTask: TaskProvider + ) { + if (markAsChecked.contains(project.path)) return + markAsChecked.add(project.path) + handleSubProject(project, depTask, dependenciesComponent.variantExtractor()) + dependenciesComponent.configurationExtractor() + .runtimeConfigurations(project) + .flatMap { configuration -> + configuration.dependencies.withType(ProjectDependency::class.java) + }.forEach { + dfs(it.dependencyProject, markAsChecked, dependenciesComponent, depTask) + } + } + + private fun handleSubProject( + project: Project, + task: TaskProvider, + variantExtractor: VariantExtractor + ) { + when { + project.isAndroidLibrary -> { + val variant = variantExtractor.findMatchVariant(project) + if (variant is AndroidAppSizeVariant) { + task.dependsOn(variant.baseVariant.assembleProvider) + } + } + + project.isKotlinJvm -> { + task.dependsOn(project.tasks.named("jar")) + } + + project.isJava -> { + task.dependsOn(project.tasks.named("jar")) + } + } + + } +} + +internal fun AppExtension.getProductFlavor(variant: BaseVariant): ProductFlavor? = productFlavors.find { + it.name == variant.flavorName +} + +internal fun AppExtension.getOriginalBuildType(variant: BaseVariant): BuildType = buildTypes.first { + it.name == variant.buildType.name +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/ApkGeneratorExtension.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/ApkGeneratorExtension.kt new file mode 100644 index 0000000..3b4454f --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/ApkGeneratorExtension.kt @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.configuration + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import java.io.File +import javax.inject.Inject + +open class ApkGeneratorExtension @Inject constructor(objects: ObjectFactory) { + val bundleToolFile: RegularFileProperty = objects.fileProperty() + val deviceSpecs: ListProperty = objects.listProperty(File::class.java) +} + diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/InputExtension.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/InputExtension.kt new file mode 100644 index 0000000..7df0cc2 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/InputExtension.kt @@ -0,0 +1,77 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.configuration + +import com.android.build.gradle.api.BaseVariant +import com.android.builder.model.BuildType +import com.android.builder.model.ProductFlavor +import org.gradle.api.Action +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import javax.inject.Inject + +private const val DEFAULT_LARGE_FILE = 10240L // 10kb + +open class InputExtension @Inject constructor(objects: ObjectFactory) { + val apk: ApkGeneratorExtension = objects.newInstance(ApkGeneratorExtension::class.java, objects) + val teamMappingFile: RegularFileProperty = objects.fileProperty() + var variantFilter: Action? = null + var largeFileThreshold: Long = DEFAULT_LARGE_FILE + var enableMatchDebugVariant = false + + + fun variantFilter(action: Action) { + variantFilter = action + } + + fun apk(action: Action) { + action.execute(apk) + } + + fun apk(block: ApkGeneratorExtension.() -> Unit) { + block(apk) + } +} + +interface VariantFilter { + fun setIgnore(ignore: Boolean) + val buildType: BuildType + val flavors: List + val name: String +} + +internal class DefaultVariantFilter(variant: BaseVariant) : VariantFilter { + var ignored: Boolean = false + override fun setIgnore(ignore: Boolean) { + ignored = ignore + } + + override val buildType: BuildType = variant.buildType + override val flavors: List = variant.productFlavors + override val name: String = variant.name +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/MetricExtension.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/MetricExtension.kt new file mode 100644 index 0000000..367cbca --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/configuration/MetricExtension.kt @@ -0,0 +1,96 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.configuration + +import groovy.lang.Closure +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.property +import javax.inject.Inject + +open class MetricExtension @Inject constructor(project: Project) { + val influxDBExtension: InfluxDBExtension = + project.objects.newInstance(InfluxDBExtension::class.java, project.objects) + + val localExtension: LocalExtension = project.objects.newInstance(LocalExtension::class.java, project) + + val customAttributes: MapProperty = + project.objects.mapProperty(String::class.java, String::class.java) + + fun influxDB(closure: Closure<*>) { + closure.delegate = influxDBExtension + closure.call() + } + + fun influxDB(block: InfluxDBExtension.() -> Unit) { + block(influxDBExtension) + } + + fun local(block: LocalExtension.() -> Unit) { + block(localExtension) + } + + fun local(closure: Closure<*>) { + closure.delegate = localExtension + closure.call() + } +} + +open class RetentionPolicyExtension @Inject constructor(objects: ObjectFactory) { + val name: Property = objects.property() + val duration: Property = objects.property() + val shardDuration: Property = objects.property() + val replicationFactor: Property = objects.property() + val setAsDefault: Property = objects.property().convention(false) +} + +open class InfluxDBExtension @Inject constructor(private val objects: ObjectFactory) { + val dbName: Property = objects.property() + val url: Property = objects.property() + val username: Property = objects.property() + val password: Property = objects.property() + val reportTableName: Property = objects.property() + val retentionPolicy: RetentionPolicyExtension = objects.newInstance(RetentionPolicyExtension::class.java) + + fun retentionPolicy(closure: Closure<*>) { + closure.delegate = retentionPolicy + closure.call() + } + + fun retentionPolicy(block: RetentionPolicyExtension.() -> Unit) { + retentionPolicy.block() + } +} + + +open class LocalExtension @Inject constructor(project: Project) { + val outputDirectory: DirectoryProperty = project.objects.directoryProperty() +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependency.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependency.kt new file mode 100644 index 0000000..f002b87 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependency.kt @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + + +interface ArchiveDependency { + val name: String + val pathToArtifact: String +} + +data class ExternalDependency( + override val name: String, + override val pathToArtifact: String +) : ArchiveDependency { + override fun hashCode(): Int = name.hashCode() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ExternalDependency + + return name == other.name + } +} + +data class ModuleDependency(override val name: String, override val pathToArtifact: String) : ArchiveDependency { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ModuleDependency + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() +} + +data class JavaModuleDependency(override val name: String, override val pathToArtifact: String) : ArchiveDependency { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JavaModuleDependency + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() +} + +data class AppDependency(override val name: String, override val pathToArtifact: String) : ArchiveDependency { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AppDependency + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependencyManager.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependencyManager.kt new file mode 100644 index 0000000..37afe96 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependencyManager.kt @@ -0,0 +1,48 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import java.io.File + +class ArchiveDependencyManager { + private fun createGson(): Gson = GsonBuilder() + .registerTypeHierarchyAdapter(ArchiveDependency::class.java, ArchiveDependencyTypeAdapter()) + .create() + + fun writeToJsonFile(archiveDependencyStore: ArchiveDependencyStore, outputFile: File) { + outputFile.writeText(createGson().toJson(archiveDependencyStore)) + } + + fun readFromJsonFile(inputJsonFile: File): ArchiveDependencyStore { + val hashSetType = object : TypeToken>() {}.type + return createGson().fromJson(inputJsonFile.readText(), hashSetType) + } +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependencyTypeAdapter.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependencyTypeAdapter.kt new file mode 100644 index 0000000..3a7af26 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveDependencyTypeAdapter.kt @@ -0,0 +1,102 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import com.grab.plugin.sizer.tasks.* + +private const val NAME = "name" +private const val TYPE = "type" +private const val TYPE_MODULE = "module" +private const val TYPE_MODULE_JAVA = "java" +private const val TYPE_EXTERNAL = "external" +private const val TYPE_APP = "app" + +private const val PATH_TO_ARTIFACT = "pathToArtifact" +class ArchiveDependencyTypeAdapter : TypeAdapter() { + override fun write(writer: JsonWriter, dependency: ArchiveDependency) { + writer.beginObject() + writer.name(NAME) + writer.value(dependency.name) + writer.name(PATH_TO_ARTIFACT) + writer.value(dependency.pathToArtifact) + writer.name(TYPE) + when (dependency) { + is ModuleDependency -> writer.value(TYPE_MODULE) + is JavaModuleDependency -> writer.value(TYPE_MODULE_JAVA) + is ExternalDependency -> writer.value(TYPE_EXTERNAL) + is AppDependency -> writer.value(TYPE_APP) + } + writer.endObject() + } + + override fun read(reader: JsonReader): ArchiveDependency { + var name = "" + var path = "" + var type = "" + reader.beginObject() + var fieldname: String? = null + while (reader.hasNext()) { + val token = reader.peek() + if (token.equals(JsonToken.NAME)) { + //get the current token + fieldname = reader.nextName() + } + when (fieldname) { + NAME -> { + reader.peek() + name = reader.nextString() + } + + PATH_TO_ARTIFACT -> { + reader.peek() + path = reader.nextString() + } + + TYPE -> { + reader.peek() + type = reader.nextString() + } + } + } + reader.endObject() + return when (type) { + TYPE_APP -> AppDependency(name, path) + TYPE_MODULE -> ModuleDependency(name, path) + TYPE_MODULE_JAVA -> JavaModuleDependency(name, path) + TYPE_EXTERNAL -> ExternalDependency(name, path) + else -> { + throw IllegalArgumentException("The $type is not valid") + } + } + + } +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveExtractor.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveExtractor.kt new file mode 100644 index 0000000..5d0335a --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/ArchiveExtractor.kt @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.grab.plugin.sizer.utils.isAndroidApplication +import com.grab.plugin.sizer.utils.isAndroidLibrary +import com.grab.plugin.sizer.utils.isJava +import com.grab.plugin.sizer.utils.isKotlinJvm +import org.gradle.api.Project +import javax.inject.Inject + +interface ArchiveExtractor { + fun extract(project: Project): ArchiveDependency +} + +@DependenciesScope +internal class DefaultArchiveExtractor @Inject constructor( + private val variantExtractor: VariantExtractor +) : ArchiveExtractor { + override fun extract(project: Project): ArchiveDependency { + val matchVariant = variantExtractor.findMatchVariant(project) + when { + project.isAndroidApplication -> { + return AppDependency( + name = project.pathTrimColon, + pathToArtifact = matchVariant.binaryOutPut.path + ) + } + + project.isAndroidLibrary -> { + return ModuleDependency( + name = project.pathTrimColon, + pathToArtifact = matchVariant.binaryOutPut.path + ) + } + + project.isKotlinJvm -> { + return JavaModuleDependency( + name = project.pathTrimColon, + pathToArtifact = matchVariant.binaryOutPut.path + ) + } + + project.isJava -> { + return JavaModuleDependency( + name = project.pathTrimColon, + pathToArtifact = matchVariant.binaryOutPut.path + ) + } + + else -> { + throw IllegalArgumentException("The ${project.name} is not an Android/Kotlin/Java module") + } + } + } +} + +private val Project.pathTrimColon + get() = path.trim(':') \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DefaultConfigurationExtractor.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DefaultConfigurationExtractor.kt new file mode 100644 index 0000000..7bb9b92 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DefaultConfigurationExtractor.kt @@ -0,0 +1,50 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import javax.inject.Inject + +interface ConfigurationExtractor { + fun runtimeConfigurations(project: Project): Sequence +} + +@DependenciesScope +internal class DefaultConfigurationExtractor @Inject constructor( + private val variantExtractor: VariantExtractor +) : ConfigurationExtractor { + override fun runtimeConfigurations(project: Project): Sequence { + val variant = variantExtractor.findMatchVariant(project) + return project.configurations.asSequence() + .filter { + variant.runtimeConfiguration.hierarchy.contains(it) + } + } +} + diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DependenciesComponent.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DependenciesComponent.kt new file mode 100644 index 0000000..1d5ccaf --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DependenciesComponent.kt @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.grab.plugin.sizer.utils.PluginLogger +import com.grab.sizer.utils.Logger +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import org.gradle.api.Project +import javax.inject.Named +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.RUNTIME) +internal annotation class DependenciesScope + +@Component( + modules = [DependenciesModule::class] +) +@DependenciesScope +internal interface DependenciesComponent { + fun dependencyExtractor(): DependencyExtractor + fun configurationExtractor(): ConfigurationExtractor + fun variantExtractor(): VariantExtractor + + fun logger(): Logger + + @Component.Factory + interface Factory { + fun create( + @BindsInstance project: Project, + @BindsInstance variantInput: VariantInput, + @BindsInstance @Named(BUILD_FLAVOR) flavorMatchingFallbacks: List, + @BindsInstance @Named(BUILD_TYPE) buildTypeMatchingFallbacks: List, + @BindsInstance @Named(ENABLE_MATCH_DEBUG_VARIANT) enableMatchDebugVariant: Boolean + ): DependenciesComponent + } +} + +@Module +internal interface DependenciesModule { + @Binds + fun bindArchiveExtractor(extractor: DefaultArchiveExtractor): ArchiveExtractor + + @Binds + fun bindConfigurationExtractor(extractor: DefaultConfigurationExtractor): ConfigurationExtractor + + @Binds + fun bindDependencyExtractor(extractor: DefaultDependencyExtractor): DependencyExtractor + + @Binds + fun bindVariantExtractor(extractor: DefaultVariantExtractor): VariantExtractor + + @Binds + fun bindLogger(logger: PluginLogger): Logger +} diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DependencyExtractor.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DependencyExtractor.kt new file mode 100644 index 0000000..24ff36a --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/DependencyExtractor.kt @@ -0,0 +1,132 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.grab.sizer.utils.Logger +import com.grab.sizer.utils.log +import org.gradle.api.Project +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.artifacts.ResolveException +import org.gradle.api.artifacts.ResolvedArtifact +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.internal.artifacts.DefaultResolvedDependency +import org.gradle.internal.component.AmbiguousVariantSelectionException +import java.util.* +import javax.inject.Inject + +typealias ArchiveDependencyStore = HashSet + +interface DependencyExtractor { + fun extract(): ArchiveDependencyStore +} + + +private const val INTERNAL_DEP_VERSION = "unspecified" + +@DependenciesScope +class DefaultDependencyExtractor @Inject constructor( + private val appProject: Project, + private val configurationExtractor: ConfigurationExtractor, + private val archiveExtractor: ArchiveExtractor, + private val logger: Logger +) : DependencyExtractor { + override fun extract(): ArchiveDependencyStore { + return ArchiveDependencyStore().apply { + val checkedProjects = mutableSetOf() + val queue: Queue = LinkedList().apply { add(appProject) } + + while (queue.isNotEmpty()) { + val project = queue.poll() + val projectArchive = archiveExtractor.extract(project) + add(projectArchive) + fetchInternalDependency(project, this, checkedProjects, queue) + fetchExternalDependency(project, this) + } + } + } + + private fun fetchInternalDependency( + project: Project, + archiveDependencyStore: ArchiveDependencyStore, + checkedProjects: MutableSet, + queue: Queue + ) { + configurationExtractor.runtimeConfigurations(project) + .flatMap { configuration -> configuration.dependencies } + .filterIsInstance() + .map { it.dependencyProject } + .forEach { dependencyProject -> + archiveDependencyStore.add( + archiveExtractor.extract(dependencyProject) + ) + if (!checkedProjects.contains(dependencyProject.path)) { + queue.add(dependencyProject) + checkedProjects.add(dependencyProject.path) + } + } + } + + private fun fetchExternalDependency( + project: Project, + archiveDependencyStore: ArchiveDependencyStore + ) { + configurationExtractor.runtimeConfigurations(project) + .filter { it.isCanBeResolved } + .map { it.resolvedConfiguration } + .flatMap { + try { + it.firstLevelModuleDependencies + } catch (e: ResolveException) { + logger.log("Fetching firstLevelModuleDependencies having issue with $it for ${project.name}") + emptySet() + } + } + .filterIsInstance() + .forEach { resolvedDep -> + /** + * Haven't found a proper way to detect if the resolvedDep is a module or a library + * Here is a workaround, it will not work if the library group starting with the root project name + */ + if (!resolvedDep.moduleGroup.startsWith(project.rootProject.name) && resolvedDep.moduleVersion != INTERNAL_DEP_VERSION) { + try { + resolvedDep.allModuleArtifacts.forEach { artifact -> + archiveDependencyStore.add(artifact.toArchiveDependency()) + } + } catch (e: AmbiguousVariantSelectionException) { + logger.log("Fetching allModuleArtifacts having issue with ${resolvedDep.name}") + } + } + } + } +} + +private fun ResolvedArtifact.toArchiveDependency(): ArchiveDependency = ExternalDependency( + name = id.componentIdentifier.toString(), + pathToArtifact = file.path +) + diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/VariantExtractor.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/VariantExtractor.kt new file mode 100644 index 0000000..b25b7ec --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/dependencies/VariantExtractor.kt @@ -0,0 +1,309 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.api.BaseVariant +import com.grab.plugin.sizer.utils.isAndroidApplication +import com.grab.plugin.sizer.utils.isAndroidLibrary +import com.grab.plugin.sizer.utils.isJava +import com.grab.plugin.sizer.utils.isKotlinJvm +import org.gradle.api.DomainObjectSet +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.plugins.JavaPlugin +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.the +import java.io.File +import java.io.Serializable +import javax.inject.Inject +import javax.inject.Named + + +internal const val BUILD_TYPE = "BUILD_TYPE" +internal const val BUILD_FLAVOR = "BUILD_FLAVOR" +internal const val ENABLE_MATCH_DEBUG_VARIANT = "ENABLE_MATCH_DEBUG_VARIANT" +internal const val BUILD_TYPE_DEBUG = "debug" + +internal interface VariantExtractor { + /** + * This method finds a matching variant for a provided project. + * It supports Android Applications, Android Libraries, Java and Kotlin JVM projects. + * + * @param project The project for which to locate the matching variant. + * @return AppSizeVariant that is the extracted variant for the given project type. + * @throws IllegalArgumentException if the project type is not supported. + */ + fun findMatchVariant(project: Project): AppSizeVariant +} + +internal interface AppSizeVariant { + val binaryOutPut: File + val runtimeConfiguration: Configuration + val buildType: String + val buildFlavor: String +} + +data class VariantInput( + val name: String, + val flavorName: String, + val buildTypeName: String, + val versionName: String? +) : Serializable + +internal fun BaseVariant.toVariantInput() = VariantInput( + name = name, + flavorName = flavorName, + buildTypeName = buildType.name, + versionName = mergedFlavor.versionName, +) + + +/** + * DefaultVariantExtractor class is designed to extract matching variant and debug variant from various project types. + * This class supports Android Applications, Android Libraries, Java and Kotlin JVM projects. + * Based on the enableMatchDebugVariant flag, this implement of VariantExtractor will return the variant accordingly + * - enableMatchDebugVariant is true, the variant debug build type will be selected by default, and the flavor will be matched + * - enableMatchDebugVariant is false, the build type and flavor will be taken into account + * @property variantInput references the base variant info being used for matching. + * @property flavorMatchingFallbacks references list of build flavors to be used as fallbacks. + * @property buildTypeMatchingFallbacks references list of build types to be used as fallbacks. + * @property enableMatchDebugVariant specifies whether to match debug variant. + */ +@DependenciesScope +internal class DefaultVariantExtractor @Inject constructor( + private val variantInput: VariantInput, + @Named(BUILD_FLAVOR) + private val flavorMatchingFallbacks: List, + @Named(BUILD_TYPE) + private val buildTypeMatchingFallbacks: List, + @Named(ENABLE_MATCH_DEBUG_VARIANT) + private val enableMatchDebugVariant: Boolean, +) : VariantExtractor { + + override fun findMatchVariant(project: Project): AppSizeVariant{ + return when{ + enableMatchDebugVariant -> findMatchDebugVariant(project) + else -> defaultFindMatchVariant(project) + } + } + + /** + * This method finds a matching variant for a provided project. + * It supports Android Applications, Android Libraries, Java and Kotlin JVM projects. + * + * @param project The project for which to locate the matching variant. + * @return AppSizeVariant that is the extracted variant for the given project type. + * @throws IllegalArgumentException if the project type is not supported. + */ + private fun defaultFindMatchVariant(project: Project): AppSizeVariant { + return when { + project.isAndroidApplication -> AndroidAppSizeVariant( + project.extractVariant(project.the().applicationVariants) + ) + + project.isAndroidLibrary -> AndroidAppSizeVariant( + project.extractVariant(project.the().libraryVariants) + ) + + project.isJava || project.isKotlinJvm -> JarAppSizeVariant(project) + else -> { + throw IllegalArgumentException("${project.name} is not supported") + } + } + } + + /** + * This method finds the debug variant for a provided project. + * It supports Android Applications, Android Libraries, Java and Kotlin JVM projects. + * + * @param project The project for which to locate the debug variant. + * @return AppSizeVariant that is the debug variant for the given project type. + * @throws IllegalArgumentException if the project type is not supported. + */ + private fun findMatchDebugVariant(project: Project): AppSizeVariant { + return when { + project.isAndroidApplication -> AndroidAppSizeVariant( + findDebugVariant(project.the().applicationVariants) + ) + + project.isAndroidLibrary -> AndroidAppSizeVariant( + findDebugVariant(project.the().libraryVariants) + ) + + project.isJava || project.isKotlinJvm -> JarAppSizeVariant(project) + else -> { + throw IllegalArgumentException("${project.name} is not supported") + } + } + } + + /** + * This function finds the debug variant that matches the flavor of the base variant. + * + * @param variants DomainObjectSet of BaseVariants that should be searched. + * @return BaseVariant that is the debug variant matching the flavor of the base variant. + * @throws RuntimeException if no matching debug variant can be found. + */ + private fun findDebugVariant(variants: DomainObjectSet): BaseVariant { + // Filter out the debug variants from the provided set of variants. + val debugVariants = variants.filter { variant -> + variant.buildType.name == BUILD_TYPE_DEBUG + } + // Try finding a debug variant that matches the flavor of the base variant. + val matchFlavor = debugVariants.find { variant -> + variant.flavorName == variantInput.flavorName + } + + // If a match is found, return it. + if (matchFlavor != null) return matchFlavor + + // If there is only one debug variant, return it. + if (debugVariants.isNotEmpty()) { + if (debugVariants.size == 1) return debugVariants.first() + + // If there are multiple debug variants, use the presets in flavorMatchingFallbacks + // to determine the best match. + flavorMatchingFallbacks.forEach { fallback -> + debugVariants.forEach { variant -> + if (fallback == variant.flavorName) return variant + } + } + } + // If no match was found, throw an exception. + throw RuntimeException("Can not find the matching debug variant") + } + + + /** + * This function extracts a variant that matches the base variant's flavor and build type. + * + * @receiver Project The project from which to extract the variant. + * @return BaseVariant that is the variant matching the flavor and build type of the base variant. + * @throws RuntimeException if no matching variant can be found. + */ + private fun Project.extractVariant(variants: DomainObjectSet): BaseVariant { + + // Try to find a variant that fully matches the base variant + val fullMatch = variants.find { variant -> + variant.name == variantInput.name + } + + // If a full match is found, return it + if (fullMatch != null) return fullMatch + + // Filter variants that has the same flavor as base variant + val matchFlavorVariant = variants.filter { variant -> + variant.flavorName == variantInput.flavorName + } + + // If we found matching flavor + if (matchFlavorVariant.isNotEmpty()) { + // Find the build type that matches the base variant + matchFlavorVariant.forEach { + // match both, buildType & flavor + if (it.buildType.name == variantInput.buildTypeName) + return it + } + + // If no full match is found, match just by build type with our fallbacks + buildTypeMatchingFallbacks.forEach { fallback -> + matchFlavorVariant.forEach { variant -> + if (variant.buildType.name == fallback) return variant + } + } + } + + // If no variant with matching flavor is found, filter by build type + val matchBuildType = variants.filter { variant -> + variant.buildType.name == variantInput.buildTypeName + } + + // If found, return; if there are multiple matches, find the first match flavor by our fallbacks + if (matchBuildType.isNotEmpty()) { + if (matchBuildType.size == 1) return matchBuildType.first() + flavorMatchingFallbacks.forEach { fallback -> + matchBuildType.forEach { variant -> + if (fallback == variant.flavorName) return variant + } + } + } + + // When no flavor or build type match, return debug by default + val matchDefaultBuildType = variants.filter { variant -> + variant.buildType.name == BUILD_TYPE_DEBUG + } + + // If found, return; if there are multiple matches, find the first match flavor by our fallbacks + if (matchDefaultBuildType.isNotEmpty()) { + if (matchDefaultBuildType.size == 1) return matchDefaultBuildType.first() + flavorMatchingFallbacks.forEach { fallback -> + matchDefaultBuildType.forEach { variant -> + if (fallback == variant.flavorName) return variant + } + } + } + // When no match found, throw exception + throw RuntimeException("Can not find the matching variant for ${project.name}") + } +} + +internal class JarAppSizeVariant( + private val project: Project +) : AppSizeVariant { + override val binaryOutPut: File + get() { + val jarTask = project.tasks.findByName(JavaPlugin.JAR_TASK_NAME) as Jar + return jarTask.archiveFile.get().asFile + } + + override val runtimeConfiguration: Configuration by lazy { + project.configurations.first { + it.name.equals("RuntimeClasspath", true) + } + } + + override val buildType: String + get() = "" + override val buildFlavor: String + get() = "" +} + +internal class AndroidAppSizeVariant( + val baseVariant: BaseVariant +) : AppSizeVariant { + override val binaryOutPut: File + get() = baseVariant.outputs.first().outputFile + override val runtimeConfiguration: Configuration + get() = baseVariant.runtimeConfiguration + override val buildType: String + get() = baseVariant.buildType.name + override val buildFlavor: String + get() = baseVariant.flavorName +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/AppSizeAnalysisTask.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/AppSizeAnalysisTask.kt new file mode 100644 index 0000000..58de94b --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/AppSizeAnalysisTask.kt @@ -0,0 +1,196 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.tasks + + +import com.android.build.gradle.api.BaseVariant +import com.grab.plugin.sizer.AppSizePluginExtension +import com.grab.plugin.sizer.configuration.InfluxDBExtension +import com.grab.plugin.sizer.configuration.RetentionPolicyExtension +import com.grab.plugin.sizer.dependencies.ArchiveDependencyManager +import com.grab.plugin.sizer.dependencies.ArchiveDependencyStore +import com.grab.plugin.sizer.dependencies.VariantInput +import com.grab.plugin.sizer.dependencies.toVariantInput +import com.grab.plugin.sizer.params +import com.grab.plugin.sizer.utils.PluginInputProvider +import com.grab.plugin.sizer.utils.PluginLogger +import com.grab.plugin.sizer.utils.PluginOutputProvider +import com.grab.sizer.AnalyticsOption +import com.grab.sizer.AppSizer +import com.grab.sizer.report.ProjectInfo +import com.grab.sizer.report.db.DatabaseRetentionPolicy +import com.grab.sizer.report.db.InfluxDBConfig +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import java.io.File + + +internal abstract class AppSizeAnalysisTask : DefaultTask() { + + @get:Input + abstract val variantInput: Property + + @get:Input + abstract val customProperties: MapProperty + + @get:InputFile + abstract val archiveDepJsonFile: RegularFileProperty + + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val apkDirectories: ConfigurableFileCollection + + @get:Input + abstract val option: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @get:InputFile + @get:Optional + abstract val teamMappingFile: RegularFileProperty + + @get:InputFile + @get:Optional + abstract val r8MappingFile: RegularFileProperty + + @get:Input + @get:Optional + abstract val largeFileThreshold: Property + + @get:Input + @get:Optional + abstract val libName: Property + + @get:Input + @get:Optional + abstract val influxDBConfig: Property + + + @TaskAction + fun run() { + apkDirectories.forEach { apkDirectory -> + val projectInfo = ProjectInfo( + projectName = project.rootProject.name, + versionName = variantInput.get().versionName ?: "NA", + deviceName = apkDirectory.nameWithoutExtension, + buildType = variantInput.get().name + ) + val archiveDependencyStore = ArchiveDependencyManager().readFromJsonFile(archiveDepJsonFile.asFile.get()) + AppSizer( + inputProvider = createInputProvider(archiveDependencyStore, apkDirectory), + outputProvider = createOutputProvider(projectInfo), + libName = libName.orNull, + logger = PluginLogger(project), + ).process(option.get()) + } + + } + + private fun createInputProvider( + archiveDependencyStore: ArchiveDependencyStore, + apksDirectory: File, + ) = PluginInputProvider( + archiveDependencyStore = archiveDependencyStore, + r8MappingFile = r8MappingFile.orNull?.asFile, + apksDirectory = apksDirectory, + largeFileThreshold = largeFileThreshold.get(), + teamMappingFile = if (teamMappingFile.isPresent) teamMappingFile.asFile.get() else null + ) + + private fun createOutputProvider( + projectInfo: ProjectInfo + ): PluginOutputProvider = + PluginOutputProvider( + influxDBConfig = influxDBConfig.orNull, + projectInfo = projectInfo, + customProperties = customProperties.get(), + outputFolder = outputDirectory.asFile.get() + ) + + companion object { + fun registerTask( + project: Project, + variant: BaseVariant, + pluginExtension: AppSizePluginExtension, + generateApkTask: TaskProvider, + generateArchivesListTask: TaskProvider, + ): TaskProvider { + return project.tasks.register( + "appSizeAnalysis${variant.name.capitalize()}", AppSizeAnalysisTask::class.java + ) { + this.variantInput.set(variant.toVariantInput()) + this.apkDirectories.setFrom(generateApkTask.map { it.outputDirectories }) + this.archiveDepJsonFile.set(generateArchivesListTask.map { it.archiveDepFile.get() }) + this.libName.set(project.params().libraryName()) + this.option.set(project.params().option()) + if (pluginExtension.metrics.influxDBExtension.url.isPresent) { + this.influxDBConfig.set(pluginExtension.metrics.influxDBExtension.toInfluxDBConfig()) + } + this.customProperties.set(pluginExtension.metrics.customAttributes) + if (pluginExtension.metrics.localExtension.outputDirectory.isPresent) { + this.outputDirectory.set(pluginExtension.metrics.localExtension.outputDirectory) + } else { + this.outputDirectory.set(project.layout.buildDirectory.dir("sizer/reports/${variant.name}")) + } + + if(pluginExtension.input.teamMappingFile.isPresent){ + this.teamMappingFile.set(pluginExtension.input.teamMappingFile) + } + + this.largeFileThreshold.set(pluginExtension.input.largeFileThreshold) + if (variant.mappingFileProvider.isPresent && variant.buildType.isMinifyEnabled) { + this.r8MappingFile.set(variant.mappingFileProvider.get().files.first()) + } + } + } + } +} + +private fun InfluxDBExtension.toInfluxDBConfig(): InfluxDBConfig = InfluxDBConfig( + dbName = if (dbName.isPresent) dbName.get() else null, + url = url.get(), + username = username.orNull, + password = password.orNull, + reportTableName = if (reportTableName.isPresent) reportTableName.get() else null, + databaseRetentionPolicy = if (retentionPolicy.name.isPresent) retentionPolicy.toDatabaseRetentionPolicy() else null +) + +private fun RetentionPolicyExtension.toDatabaseRetentionPolicy(): DatabaseRetentionPolicy = DatabaseRetentionPolicy( + name = name.get(), + duration = duration.get(), + shardDuration = shardDuration.get(), + replicationFactor = replicationFactor.get(), + isDefault = setAsDefault.get() +) diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/GenerateApkTask.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/GenerateApkTask.kt new file mode 100644 index 0000000..8699a7a --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/GenerateApkTask.kt @@ -0,0 +1,215 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.tasks + +import com.android.build.gradle.api.ApplicationVariant +import com.android.build.gradle.internal.tasks.FinalizeBundleTask +import com.android.builder.model.SigningConfig +import com.grab.plugin.sizer.AppSizePluginExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.LogLevel +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import java.io.File +import java.util.* + +private const val DEFAULT_DEVICE_SPEC = """ + { + "supportedAbis": ["armeabi-v7a", "arm64-v8a"], + "supportedLocales": ["en", "es"], + "screenDensity": 480, + "sdkVersion": 30 +} +""" + +internal const val DEFAULT_DEVICE_NAME = "default_device" + +internal abstract class GenerateApkTask : DefaultTask() { + @get:InputFile + abstract val bundleToolFile: RegularFileProperty + + @get:Input + abstract val variantName: Property + + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val deviceSpecFiles: ConfigurableFileCollection + + @get:InputFile + abstract val appBundleFile: RegularFileProperty + + @get:Input + abstract val signingConfig: Property + + @get:OutputDirectories + abstract val outputDirectories: ListProperty + + init { + outputDirectories.convention( + // Add the provider to ensure the deviceSpecFiles values has set + project.provider { + deviceSpecFiles.map { specFile -> + project.layout.buildDirectory + .dir("sizer/apk/${variantName.get()}/${specFile.nameWithoutExtension}") + .get() + } + } + ) + } + + @TaskAction + fun generateApk() { + deviceSpecs.forEach { deviceSpecFile -> + File.createTempFile(deviceSpecFile.name, ".apks").also { tempFile -> + try { + generateApksFile(tempFile, deviceSpecFile.path) + val outputDir = outputDirectories.get() + .find { + it.asFile.nameWithoutExtension == deviceSpecFile.nameWithoutExtension + }?.asFile + ?: throw IllegalArgumentException("Output folders are not match for ${deviceSpecFile.nameWithoutExtension}") + if (!outputDir.exists()) { + outputDir.mkdirs() + } else { + outputDir.clearDirectory() + } + extractApksToDirectory(tempFile, deviceSpecFile.path, outputDir) + } finally { + tempFile.delete() + project.logger.log(LogLevel.INFO, "Temp files were deleted") + } + } + } + } + + private val deviceSpecs: Iterable + get() = if (deviceSpecFiles.isEmpty) { + setOf( + File.createTempFile(DEFAULT_DEVICE_NAME, ".json") + .apply { + writeBytes( + DEFAULT_DEVICE_SPEC.toByteArray() + ) + } + ) + } else { + deviceSpecFiles + } + + private fun extractApksToDirectory(apksTempFile: File, deviceSpec: String, outputDirectory: File) { + project.exec { + commandLine( + "java", + "-jar", + bundleToolFile.asFile.get().path, + "extract-apks", + "--apks=${apksTempFile.path}", + "--output-dir=${outputDirectory.path}", + "--device-spec=${deviceSpec}", + ) + } + project.logger.log(LogLevel.QUIET, "The Apks for $deviceSpec were extracted successfully") + } + + private fun generateApksFile(apksTempFile: File, deviceSpec: String) { + val realSigningConfig = signingConfig.get() + + project.exec { + commandLine( + "java", + "-jar", + bundleToolFile.asFile.get().path, + "build-apks", + "--bundle=${appBundleFile.asFile.get().path}", + "--output=${apksTempFile.path}", + "--ks=${realSigningConfig.storeFile}", + "--ks-pass=pass:${realSigningConfig.storePassword}", + "--key-pass=pass:${realSigningConfig.keyPassword}", + "--ks-key-alias=${realSigningConfig.keyAlias}", + "--device-spec=${deviceSpec}", + "--overwrite" + ) + } + project.logger.log(LogLevel.QUIET, "The app.apks generated successfully") + } + + private fun File.clearDirectory() { + if (!exists()) return + if (!isDirectory) throw RuntimeException("The ${this.path} file is not a directory") + walk().forEach { apk -> + apk.delete() + } + } + + companion object { + fun registerTask( + project: Project, + extension: AppSizePluginExtension, + variant: ApplicationVariant + ): TaskProvider { + val bundleTask = project.tasks.named("sign${variant.name.capitalize()}Bundle") + val task = project.tasks.register("generateApk${variant.name.capitalize()}", GenerateApkTask::class.java) { + deviceSpecFiles.setFrom(extension.input.apk.deviceSpecs) + bundleToolFile.set(extension.input.apk.bundleToolFile) + appBundleFile.set( + bundleTask.map { + (it as FinalizeBundleTask).finalBundleFile.get() + } + ) + signingConfig.set(variant.signingConfig.toInternalSigningConfig()) + variantName.set(variant.name) + } + return task + } + } +} + +internal fun String.capitalize(): String = replaceFirstChar { + if (it.isLowerCase()) it.titlecase( + Locale.getDefault() + ) else it.toString() +} + +private fun SigningConfig.toInternalSigningConfig(): InternalSigningConfig = InternalSigningConfig( + storeFile = storeFile?.path ?: "", + storePassword = storePassword ?: "", + keyAlias = keyAlias ?: "", + keyPassword = keyPassword ?: "" +) + +internal data class InternalSigningConfig( + val storeFile: String, + val storePassword: String, + val keyAlias: String, + val keyPassword: String +) : java.io.Serializable \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/GenerateArchivesListTask.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/GenerateArchivesListTask.kt new file mode 100644 index 0000000..3f1aa5d --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/tasks/GenerateArchivesListTask.kt @@ -0,0 +1,157 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.tasks + + +import com.android.build.gradle.api.BaseVariant +import com.grab.plugin.sizer.dependencies.* +import com.grab.sizer.utils.log +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider + +/** + * This task is used to generate the list of the [com.grab.plugin.sizer.dependencies.ArchiveDependency] to a json file + * The file will be consumed by the [AppSizeAnalysisTask] as the input for the list of aar/jar files + * This task is currently non-cacheable + */ +internal abstract class GenerateArchivesListTask : DefaultTask() { + @get:Input + abstract val variantInput: Property + + @get:Input + abstract val flavorMatchingFallbacks: ListProperty + + @get:Input + abstract val buildTypeMatchingFallbacks: ListProperty + + @get:Input + abstract val enableMatchDebugVariant: Property + + /** + * This is a workaround, by default, this task haven't support catching yet + * This flag to force the task become cacheable by default. It's not recommend to enable this flag + */ + @get:Input + abstract val archiveDepTaskCacheable: Property + + + @get:OutputFile + abstract val archiveDepFile: RegularFileProperty + + init { + /** + * Todo: Update this task to make it cacheable + * If there is any dependencies updated, the task cache should be invalidated + */ + outputs.upToDateWhen { archiveDepTaskCacheable.get() } // Mark this task as non-cacheable task + + archiveDepFile.convention { + project.layout.buildDirectory.file("sizer/dep/${variantInput.get().name}/dependencies.json").get().asFile + } + } + + @TaskAction + fun run() { + if(enableMatchDebugVariant.get()){ + /** + * Extracts and manages project dependencies, separating modules from external libraries. + * + * This code performs the following steps: + * 1. Extracts module dependencies: + * - Uses createDependenciesComponent(true) to enable matching debug variants. + * - This is a workaround for cases where modules cannot be compiled in release build type. + * - When enabled, it fetches module AAR/JAR files from the debug variant. + * 2. Extracts library dependencies: + * - Uses createDependenciesComponent(false) to fetch libraries from the input variant. + * 3. Combines and processes dependencies: + * - Filters out external dependencies from modules. + * - Filters to include only external dependencies for libraries. + * + * This approach ensures proper handling of both module and external library dependencies, + * accommodating potential build type incompatibilities. + */ + val modules = createDependenciesComponent(true) + .dependencyExtractor() + .extract() + .filter { it !is ExternalDependency } + val libraries = createDependenciesComponent(false) + .dependencyExtractor() + .extract() + .filterIsInstance() + + ArchiveDependencyManager().writeToJsonFile( + (modules + libraries).toHashSet(), + archiveDepFile.get().asFile + ) + }else{ + createDependenciesComponent(false).run { + ArchiveDependencyManager().writeToJsonFile( + dependencyExtractor().extract(), + archiveDepFile.get().asFile + ) + } + } + + } + + private fun createDependenciesComponent(enableMatchDebugVariant : Boolean): DependenciesComponent = DaggerDependenciesComponent.factory().create( + project, + variantInput.get(), + flavorMatchingFallbacks.get(), + buildTypeMatchingFallbacks.get(), + enableMatchDebugVariant + ) + + companion object { + fun registerTask( + project: Project, + variant: BaseVariant, + flavorMatchingFallbacks: List, + buildTypeMatchingFallbacks: List, + enableMatchDebugVariant: Boolean, + archiveDepTaskCacheable : Boolean + ): TaskProvider { + return project.tasks.register( + "generateArchiveDep${variant.name.capitalize()}", GenerateArchivesListTask::class.java + ) { + this.variantInput.set(variant.toVariantInput()) + this.buildTypeMatchingFallbacks.set(buildTypeMatchingFallbacks) + this.flavorMatchingFallbacks.set(flavorMatchingFallbacks) + this.enableMatchDebugVariant.set(enableMatchDebugVariant) + this.archiveDepTaskCacheable.set(archiveDepTaskCacheable) + } + } + } +} \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/Logger.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/Logger.kt new file mode 100644 index 0000000..1be1ba4 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/Logger.kt @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.utils + +import com.grab.plugin.sizer.dependencies.DependenciesScope +import com.grab.sizer.utils.Logger +import org.gradle.api.Project +import org.gradle.api.logging.LogLevel +import javax.inject.Inject + +@DependenciesScope +class PluginLogger @Inject constructor(private val project: Project) : Logger { + override fun log(tag: String, message: String) = project.logger.log(LogLevel.QUIET, "$tag: $message") + + override fun logDebug(tag: String, message: String) = project.logger.log(LogLevel.DEBUG, "$tag: $message") + override fun log(tag: String, e: Exception) = project.logger.log(LogLevel.DEBUG, tag, e) +} diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/PluginInputProvider.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/PluginInputProvider.kt new file mode 100644 index 0000000..8f05bfa --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/PluginInputProvider.kt @@ -0,0 +1,102 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.utils + +import com.grab.plugin.sizer.dependencies.* +import com.grab.sizer.utils.InputProvider +import com.grab.sizer.utils.SizerInputFile +import java.io.File + +private const val EXT_AAR = "aar" +private const val EXT_JAR = "jar" + +class PluginInputProvider( + private val archiveDependencyStore: ArchiveDependencyStore, + private val apksDirectory: File, + private val largeFileThreshold: Long, + private val teamMappingFile: File? = null, + private val r8MappingFile: File? = null, +) : InputProvider { + override fun provideModuleAar(): Sequence = + archiveDependencyStore.getModuleDependency() + .map { + SizerInputFile( + tag = it.name, + file = File(it.pathToArtifact) + ) + } + + override fun provideModuleJar(): Sequence = + archiveDependencyStore.getJavaModuleDependencies() + .map { + SizerInputFile( + tag = it.name, + file = File(it.pathToArtifact) + ) + } + + override fun provideLibraryJar(): Sequence = archiveDependencyStore.getExternalDependencies() + .map { + SizerInputFile( + tag = it.name, + file = File(it.pathToArtifact) + ) + } + .filter { it.file.extension.equals(EXT_JAR, true) } + + override fun provideLibraryAar(): Sequence = archiveDependencyStore.getExternalDependencies() + .map { + SizerInputFile( + tag = it.name, + file = File(it.pathToArtifact) + ) + } + .filter { it.file.extension.equals(EXT_AAR, true) } + + override fun provideApkFiles(): Sequence { + return apksDirectory.listFiles()?.asSequence() ?: emptySequence() + } + + override fun provideR8MappingFile(): File? = r8MappingFile + + override fun provideTeamMappingFile(): File? = teamMappingFile + + override fun provideLargeFileThreshold(): Long = largeFileThreshold +} + + +fun ArchiveDependencyStore.getExternalDependencies(): Sequence = + asSequence().filterIsInstance(ExternalDependency::class.java) + +fun ArchiveDependencyStore.getJavaModuleDependencies(): Sequence = + asSequence().filterIsInstance(JavaModuleDependency::class.java) + +fun ArchiveDependencyStore.getModuleDependency(): Sequence = + asSequence().filterIsInstance(ModuleDependency::class.java) + +fun ArchiveDependencyStore.getApp(): AppDependency = asSequence().filterIsInstance().first() \ No newline at end of file diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/PluginOutputProvider.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/PluginOutputProvider.kt new file mode 100644 index 0000000..7e99940 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/PluginOutputProvider.kt @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.utils + +import com.grab.sizer.report.CustomProperties +import com.grab.sizer.report.ProjectInfo +import com.grab.sizer.report.db.InfluxDBConfig +import com.grab.sizer.utils.OutputProvider +import java.io.File + +class PluginOutputProvider( + private val influxDBConfig: InfluxDBConfig?, + private val customProperties: CustomProperties, + private val outputFolder: File, + private val projectInfo: ProjectInfo, +) : OutputProvider { + override fun provideInfluxDbConfig(): InfluxDBConfig? = influxDBConfig + override fun provideOutPutDirectory(): File = outputFolder + override fun provideCustomProperties(): CustomProperties = customProperties + override fun provideProjectInfo(): ProjectInfo = projectInfo +} diff --git a/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/ProjectExtensions.kt b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/ProjectExtensions.kt new file mode 100644 index 0000000..07c7b2c --- /dev/null +++ b/gradle-plugin/src/main/kotlin/com/grab/plugin/sizer/utils/ProjectExtensions.kt @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.utils + +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPlugin + +internal const val ANDROID_APPLICATION_PLUGIN = "com.android.application" +internal const val ANDROID_LIBRARY_PLUGIN = "com.android.library" +internal const val JAVA_LIB_PLUGIN = "java-library" +internal const val KOTLIN_LIB_PLUGIN = "org.jetbrains.kotlin.jvm" + +val Project.isAndroidLibrary get() = plugins.hasPlugin(ANDROID_LIBRARY_PLUGIN) +val Project.isAndroidApplication get() = plugins.hasPlugin(ANDROID_APPLICATION_PLUGIN) +val Project.isKotlinJvm get() = plugins.hasPlugin(KOTLIN_LIB_PLUGIN) +val Project.isJava get() = plugins.hasPlugin(JAVA_LIB_PLUGIN) || plugins.hasPlugin(JavaPlugin::class.java) \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/AppSizerPluginTest.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/AppSizerPluginTest.kt new file mode 100644 index 0000000..c01bf33 --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/AppSizerPluginTest.kt @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer + +import org.gradle.api.Project +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class AppSizerPluginTest { + + private lateinit var project: Project + + @Before + fun setup() { + project = ProjectBuilder.builder() + .build() + project.pluginManager.apply("com.android.application") + project.pluginManager.apply("com.grab.app-sizer") + } + + @Test + fun `plugin applies correctly`() { + assertTrue(project.plugins.hasPlugin(AppSizerPlugin::class.java)) + } + + @Test + fun `plugin creates extension`() { + val extension = project.extensions.findByName("appSizer") + assertNotNull(extension) + assertTrue(extension is AppSizePluginExtension) + } + // Todo [Add more cases] +} \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/DefaultDependencyExtractorTest.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/DefaultDependencyExtractorTest.kt new file mode 100644 index 0000000..2a97d59 --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/DefaultDependencyExtractorTest.kt @@ -0,0 +1,189 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.grab.plugin.sizer.fake.FakeConfiguration +import com.grab.plugin.sizer.fake.MockLogger +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class DefaultDependencyExtractorTest { + private lateinit var testProject: TestProject + private lateinit var mockLogger: MockLogger + private lateinit var archiveExtractor: ArchiveExtractor + + @Before + fun setup() { + testProject = TestProject() + mockLogger = MockLogger() + archiveExtractor = createMockArchiveExtractor() + } + + @Test + fun `extract method successfully extracts internal dependencies`() { + val configurationExtractor = testProject.createConfigurationExtractor() + val extractor = DefaultDependencyExtractor(testProject.appModule, configurationExtractor, archiveExtractor, mockLogger) + val result = extractor.extract() + + assertTrue(result.any { it is ModuleDependency && it.name == testProject.module1.name }) + assertTrue(result.any { it is ModuleDependency && it.name == testProject.module2.name }) + } + + @Test + fun `extract method successfully extracts external dependencies`() { + val configurationExtractor = testProject.createConfigurationExtractor() + val extractor = DefaultDependencyExtractor(testProject.appModule, configurationExtractor, archiveExtractor, mockLogger) + val result = extractor.extract() + + testProject.externalLibs.forEach { lib -> + assertTrue(result.any { it is ExternalDependency && it.name.contains(lib.split(":")[1]) }) + } + } + + @Test + fun `extract method handles non-resolvable configurations`() { + val nonResolvableConfig = testProject.createNonResolvableConfiguration(testProject.appModule) + val configExtractor = MockConfigurationExtractor( + mapOf(testProject.appModule to listOf(nonResolvableConfig)) + ) + val extractor = DefaultDependencyExtractor(testProject.appModule, configExtractor, archiveExtractor, mockLogger) + val result = extractor.extract() + + assertEquals(0, result.filterIsInstance().size) + } + + @Test + fun `extract method handles AmbiguousVariantSelectionException`() { + val ambiguousConfig = testProject.createAmbiguousConfiguration(testProject.appModule) + val configExtractor = MockConfigurationExtractor( + mapOf(testProject.appModule to listOf(ambiguousConfig)) + ) + val extractor = DefaultDependencyExtractor(testProject.appModule, configExtractor, archiveExtractor, mockLogger) + val result = extractor.extract() + + assertEquals(1, result.size) // App module + assertTrue(mockLogger.loggedMessages.any { it.contains("Fetching allModuleArtifacts having issue with") }) + } + + @Test + fun `extract method handles dependencies with unspecified version`() { + val mixedConfig = testProject.createVersionedModuleConfiguration(testProject.appModule) + val configExtractor = MockConfigurationExtractor( + mapOf(testProject.appModule to listOf(mixedConfig)) + ) + + val extractor = DefaultDependencyExtractor(testProject.appModule, configExtractor, archiveExtractor, mockLogger) + val result = extractor.extract() + assertEquals(2, result.filterIsInstance().size) // app + module1 + assertEquals(1, result.filterIsInstance().size) + } + + private fun createMockArchiveExtractor(): ArchiveExtractor { + return object : ArchiveExtractor { + override fun extract(project: Project): ArchiveDependency { + return ModuleDependency(project.name, "/path/to/${project.name}.aar") + } + } + } +} + + +private class TestProject { + val rootProject = ProjectBuilder.builder().withName("root").build() + + val appModule = ProjectBuilder.builder().withName("app").withParent(rootProject).build() + val module1 = ProjectBuilder.builder().withName("sub1").withParent(rootProject).build() + val module2 = ProjectBuilder.builder().withName("sub2").withParent(rootProject).build() + + val externalLibs = listOf( + "com.example:lib1:2.0.0", + "com.example:lib2:2.0.0", + "com.example:lib3:2.0.0", + "com.example:lib4:2.0.0", + "com.example:lib5:2.0.0" + ) + + fun createAppDependencies() = createDependencies(appModule, module1, module2, externalLibs[0], externalLibs[1]) + fun createModule1Dependencies() = createDependencies(module1, module2, externalLibs[2]) + fun createModule2Dependencies() = createDependencies(module2, externalLibs[3], externalLibs[4]) + + fun createAppConfiguration() = FakeConfiguration(createAppDependencies()) + fun createModule1Configuration() = FakeConfiguration(createModule1Dependencies()) + fun createModule2Configuration() = FakeConfiguration(createModule2Dependencies()) + + fun createConfigurationExtractor() = MockConfigurationExtractor( + mapOf( + appModule to listOf(createAppConfiguration()), + module1 to listOf(createModule1Configuration()), + module2 to listOf(createModule2Configuration()) + ) + ) + + fun createDependency(project: Project, notation: Any): Dependency = when (notation) { + is Project -> project.dependencies.project(mapOf("path" to notation.path)) + is String -> project.dependencies.create(notation) + else -> throw IllegalArgumentException("Unsupported dependency type: ${notation::class}") + } + + private fun createDependencies(project: Project, vararg dependencies: Any): List = + dependencies.map { createDependency(project, it) } + + fun createExternalDependency(project: Project, notation: String): Dependency = + project.dependencies.create(notation) + + fun createProjectDependency(project: Project, dependencyProject: Project): ProjectDependency = + project.dependencies.project(mapOf("path" to dependencyProject.path)) as ProjectDependency + + fun createNonResolvableConfiguration(project: Project): Configuration = + FakeConfiguration(createDependencies(project, externalLibs[0], externalLibs[1]), isResolvable = false) + + fun createAmbiguousConfiguration(project: Project): Configuration = + FakeConfiguration(createDependencies(project, externalLibs[0], externalLibs[1]), throwAmbiguousException = true) + + fun createVersionedModuleConfiguration(project: Project): Configuration { + module1.version = "1.0.0" + val versionedModule = createProjectDependency(project, module1) + val externalLib = createExternalDependency(project, "com.example:lib:0.0.1") + return FakeConfiguration(listOf(versionedModule, externalLib)) + } +} + + +class MockConfigurationExtractor(private val configurations: Map>) : + ConfigurationExtractor { + override fun runtimeConfigurations(project: Project): Sequence { + return configurations[project]?.asSequence() ?: emptySequence() + } +} \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/DefaultVariantExtractorTest.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/DefaultVariantExtractorTest.kt new file mode 100644 index 0000000..2a0606d --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/DefaultVariantExtractorTest.kt @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + + +package com.grab.plugin.sizer.dependencies + +import com.grab.plugin.sizer.utils.assertThrows +import org.gradle.api.Project +import org.gradle.api.internal.project.DefaultProject +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class DefaultVariantExtractorTest { + + private lateinit var rootProject: Project + private lateinit var variantInput: VariantInput + private lateinit var extractor: DefaultVariantExtractor + + @Before + fun setup() { + rootProject = TestProjectCreator.createRootProject() + variantInput = VariantInput("flavor1Debug", "flavor1", "debug", "1.0") + extractor = DefaultVariantExtractor( + variantInput, + listOf("flavor1", "flavor2"), + listOf("debug", "release"), + false + ) + } + + @Test + fun `findMatchVariant for Android library`() { + val project = TestProjectCreator.createAndroidLibraryProject(rootProject, "library") + val result = extractor.findMatchVariant(project) + assertAndroidVariant(result, "debug", "flavor1") + } + + @Test + fun `findMatchVariant for Android application`() { + val project = TestProjectCreator.createAndroidAppProject(rootProject, "app") + val result = extractor.findMatchVariant(project) + assertAndroidVariant(result, "debug", "flavor1") + } + + @Test + fun `findMatchVariant for Java project`() { + val project = TestProjectCreator.createJavaProject(rootProject, "java-project") + val result = extractor.findMatchVariant(project) + Assert.assertTrue(result is JarAppSizeVariant) + } + + + @Test + fun `findMatchVariant for unsupported project type`() { + val project = ProjectBuilder.builder().withParent(rootProject).build() + project.doEvaluate() + + assertThrows("${project.name} is not supported") { + extractor.findMatchVariant(project) + } + } + + @Test + fun `findMatchVariant with enableMatchDebugVariant true for library`() { + val project = TestProjectCreator.createAndroidLibraryProject(rootProject, "lib") + val variantInput = VariantInput("flavor1Release", "flavor1", "release", "1.0") + val extractorWithDebugEnabled = + DefaultVariantExtractor(variantInput, listOf("flavor1", "flavor2"), listOf("debug", "release"), true) + val result = extractorWithDebugEnabled.findMatchVariant(project) + assertAndroidVariant(result, "debug", "flavor1") + } + + @Test + fun `extractVariant returns full match when available for library`() { + val project = + TestProjectCreator.createAndroidLibraryProject(rootProject, "lib", flavors = listOf("flavor1", "flavor2")) + val variantInput = VariantInput("flavor1Release", "flavor1", "release", "1.0") + val extractor = createExtractor(variantInput) + val result = extractor.findMatchVariant(project) + + assertAndroidVariant(result, "release", "flavor1") + } + + @Test + fun `extractVariant matches flavor and build type separately when no full match for library`() { + val project = + TestProjectCreator.createAndroidLibraryProject(rootProject, "lib", flavors = listOf("flavor1", "flavor2")) + val variantInput = VariantInput("flavor2Release", "flavor2", "release", "1.0") + val extractor = createExtractor(variantInput) + + val result = extractor.findMatchVariant(project) + + assertAndroidVariant(result, "release", "flavor2") + } + + @Test + fun `extractVariant uses build type fallback when flavor matches but build type doesn't for library`() { + val project = + TestProjectCreator.createAndroidLibraryProject(rootProject, "lib", flavors = listOf("flavor1", "flavor2")) + val variantInput = VariantInput("flavor1CustomBuildType", "flavor1", "customBuildType", "1.0") + val extractor = createExtractor(variantInput, buildTypeFallbacks = listOf("release", "debug")) + + val result = extractor.findMatchVariant(project) + + assertAndroidVariant(result, "release", "flavor1") + } + + @Test + fun `extractVariant uses flavor fallback when build type matches but flavor doesn't for library`() { + val project = + TestProjectCreator.createAndroidLibraryProject(rootProject, "lib", flavors = listOf("flavor1", "flavor2")) + val variantInput = VariantInput("customFlavorDebug", "customFlavor", "debug", "1.0") + val extractor = createExtractor(variantInput, flavorFallbacks = listOf("flavor2", "flavor1")) + + val result = extractor.findMatchVariant(project) + + assertAndroidVariant(result, "debug", "flavor2") + } + + @Test + fun `extractVariant falls back to debug when no match found for library`() { + val project = + TestProjectCreator.createAndroidLibraryProject(rootProject, "lib", flavors = listOf("flavor1", "flavor2")) + val variantInput = VariantInput("customFlavorCustomBuildType", "customFlavor", "customBuildType", "1.0") + val extractor = createExtractor(variantInput) + + val result = extractor.findMatchVariant(project) + + assertAndroidVariant(result, "debug", "flavor1") + } + + private fun createExtractor( + variantInput: VariantInput, + flavorFallbacks: List = listOf("flavor1", "flavor2"), + buildTypeFallbacks: List = listOf("debug", "release") + ): DefaultVariantExtractor { + return DefaultVariantExtractor( + variantInput, + flavorFallbacks, + buildTypeFallbacks, + false + ) + } + + private fun assertAndroidVariant(result: AppSizeVariant, expectedBuildType: String, expectedFlavor: String) { + Assert.assertTrue(result is AndroidAppSizeVariant) + Assert.assertEquals(expectedBuildType, result.buildType) + Assert.assertEquals(expectedFlavor, result.buildFlavor) + } +} \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/TestProjectCreator.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/TestProjectCreator.kt new file mode 100644 index 0000000..b762b86 --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/dependencies/TestProjectCreator.kt @@ -0,0 +1,134 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.dependencies + +import com.android.build.gradle.AppExtension +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.LibraryPlugin +import org.gradle.api.Project +import org.gradle.api.internal.project.DefaultProject +import org.gradle.api.plugins.JavaPlugin +import org.gradle.testfixtures.ProjectBuilder + +object TestProjectCreator { + + fun createRootProject(): Project { + return ProjectBuilder.builder().build() + } + + fun createAndroidLibraryProject(rootProject: Project, name: String, flavors: List = listOf("flavor1", "flavor2")): Project { + val project = ProjectBuilder.builder().withName(name).withParent(rootProject).build() + project.pluginManager.apply(LibraryPlugin::class.java) + + val android = project.extensions.getByType(LibraryExtension::class.java) + android.compileSdkVersion(30) + android.namespace = "com.example.${project.name}" + android.defaultConfig { + minSdk = 21 + targetSdk = 30 + } + android.buildTypes { + getByName("debug") { + isMinifyEnabled = false + } + getByName("release") { + isMinifyEnabled = true + } + } + if (flavors.isNotEmpty()) { + android.flavorDimensions("version") + android.productFlavors { + flavors.forEach { flavor -> + create(flavor) { + dimension = "version" + } + } + } + } + + project.doEvaluate() + return project + } + + fun createAndroidAppProject(rootProject: Project, name: String): Project { + val project = ProjectBuilder.builder().withName(name).withParent(rootProject).build() + project.pluginManager.apply(AppPlugin::class.java) + + val android = project.extensions.getByType(AppExtension::class.java) + configureAndroidAppExtension(android, project.name) + + project.doEvaluate() + return project + } + + fun createJavaProject(rootProject: Project, name: String): Project { + val project = ProjectBuilder.builder().withName(name).withParent(rootProject).build() + project.pluginManager.apply(JavaPlugin::class.java) + project.doEvaluate() + return project + } + + private fun configureAndroidAppExtension(android: AppExtension, projectName: String) { + android.compileSdkVersion(30) + android.namespace = "com.example.$projectName" + android.defaultConfig { + applicationId = "com.example.$projectName" + minSdkVersion(21) + targetSdkVersion(30) + versionCode = 1 + versionName = "1.0" + } + + android.buildTypes { + getByName("debug") { + isMinifyEnabled = false + } + getByName("release") { + isMinifyEnabled = true + } + } + android.flavorDimensions("version") + android.productFlavors { + create("flavor1") { + dimension = "version" + } + create("flavor2") { + dimension = "version" + } + } + } +} + +/** + * Forces an evaluation of the project thereby running all configurations + */ +fun Project.doEvaluate() { + getTasksByName("tasks", false) + (this as DefaultProject).evaluate() +} \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeConfiguration.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeConfiguration.kt new file mode 100644 index 0000000..8cf750d --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeConfiguration.kt @@ -0,0 +1,317 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.fake + +import groovy.lang.Closure +import org.gradle.api.Action +import org.gradle.api.artifacts.* +import org.gradle.api.attributes.AttributeContainer +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileSystemLocation +import org.gradle.api.file.FileTree +import org.gradle.api.provider.Provider +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.TaskDependency +import java.io.File + +class FakeConfiguration( + private val dependencies: List, + private val isResolvable: Boolean = true, + private val throwAmbiguousException: Boolean = false +) : Configuration { + private val projectDependencies = dependencies.filterIsInstance() + override fun getName(): String = "runtimeClasspath" + override fun isCanBeResolved(): Boolean = isResolvable + + override fun getResolvedConfiguration() = FakeResolvedConfiguration(dependencies, throwAmbiguousException) + override fun getDependencies(): DependencySet = FakeDependencySet(dependencies = projectDependencies.toSet()) + + override fun iterator(): MutableIterator { + TODO("Not yet implemented") + } + + override fun addToAntBuilder(builder: Any, nodeName: String, type: FileCollection.AntType) { + TODO("Not yet implemented") + } + + override fun addToAntBuilder(builder: Any, nodeName: String): Any { + TODO("Not yet implemented") + } + + override fun getBuildDependencies(): TaskDependency { + TODO("Not yet implemented") + } + + override fun getSingleFile(): File { + TODO("Not yet implemented") + } + + override fun getFiles(): MutableSet { + TODO("Not yet implemented") + } + + override fun contains(file: File): Boolean { + TODO("Not yet implemented") + } + + override fun getAsPath(): String { + TODO("Not yet implemented") + } + + override fun plus(collection: FileCollection): FileCollection { + TODO("Not yet implemented") + } + + override fun minus(collection: FileCollection): FileCollection { + TODO("Not yet implemented") + } + + override fun filter(filterClosure: Closure<*>): FileCollection { + TODO("Not yet implemented") + } + + override fun filter(filterSpec: Spec): FileCollection { + TODO("Not yet implemented") + } + + override fun isEmpty(): Boolean { + TODO("Not yet implemented") + } + + override fun getAsFileTree(): FileTree { + TODO("Not yet implemented") + } + + override fun getElements(): Provider> { + TODO("Not yet implemented") + } + + override fun getAttributes(): AttributeContainer { + TODO("Not yet implemented") + } + + override fun attributes(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun getResolutionStrategy(): ResolutionStrategy { + TODO("Not yet implemented") + } + + override fun resolutionStrategy(closure: Closure<*>): Configuration { + TODO("Not yet implemented") + } + + override fun resolutionStrategy(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun getState(): Configuration.State { + TODO("Not yet implemented") + } + + override fun isVisible(): Boolean { + TODO("Not yet implemented") + } + + override fun setVisible(visible: Boolean): Configuration { + TODO("Not yet implemented") + } + + override fun getExtendsFrom(): MutableSet { + TODO("Not yet implemented") + } + + override fun setExtendsFrom(superConfigs: MutableIterable): Configuration { + TODO("Not yet implemented") + } + + override fun extendsFrom(vararg superConfigs: Configuration?): Configuration { + TODO("Not yet implemented") + } + + override fun isTransitive(): Boolean { + TODO("Not yet implemented") + } + + override fun setTransitive(t: Boolean): Configuration { + TODO("Not yet implemented") + } + + override fun getDescription(): String? { + TODO("Not yet implemented") + } + + override fun setDescription(description: String?): Configuration { + TODO("Not yet implemented") + } + + override fun getHierarchy(): MutableSet { + TODO("Not yet implemented") + } + + override fun resolve(): MutableSet { + TODO("Not yet implemented") + } + + override fun files(dependencySpecClosure: Closure<*>): MutableSet { + TODO("Not yet implemented") + } + + override fun files(dependencySpec: Spec): MutableSet { + TODO("Not yet implemented") + } + + override fun files(vararg dependencies: Dependency?): MutableSet { + TODO("Not yet implemented") + } + + override fun fileCollection(dependencySpec: Spec): FileCollection { + TODO("Not yet implemented") + } + + override fun fileCollection(dependencySpecClosure: Closure<*>): FileCollection { + TODO("Not yet implemented") + } + + override fun fileCollection(vararg dependencies: Dependency?): FileCollection { + TODO("Not yet implemented") + } + + override fun getUploadTaskName(): String { + TODO("Not yet implemented") + } + + override fun getTaskDependencyFromProjectDependency(useDependedOn: Boolean, taskName: String): TaskDependency { + TODO("Not yet implemented") + } + + override fun getAllDependencies(): DependencySet { + TODO("Not yet implemented") + } + + override fun getDependencyConstraints(): DependencyConstraintSet { + TODO("Not yet implemented") + } + + override fun getAllDependencyConstraints(): DependencyConstraintSet { + TODO("Not yet implemented") + } + + override fun getArtifacts(): PublishArtifactSet { + TODO("Not yet implemented") + } + + override fun getAllArtifacts(): PublishArtifactSet { + TODO("Not yet implemented") + } + + override fun getExcludeRules(): MutableSet { + TODO("Not yet implemented") + } + + override fun exclude(excludeProperties: MutableMap): Configuration { + TODO("Not yet implemented") + } + + override fun defaultDependencies(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun withDependencies(action: Action): Configuration { + TODO("Not yet implemented") + } + + override fun getAll(): MutableSet { + TODO("Not yet implemented") + } + + override fun getIncoming(): ResolvableDependencies { + TODO("Not yet implemented") + } + + override fun getOutgoing(): ConfigurationPublications { + TODO("Not yet implemented") + } + + override fun outgoing(action: Action) { + TODO("Not yet implemented") + } + + override fun copy(): Configuration { + TODO("Not yet implemented") + } + + override fun copy(dependencySpec: Spec): Configuration { + TODO("Not yet implemented") + } + + override fun copy(dependencySpec: Closure<*>): Configuration { + TODO("Not yet implemented") + } + + override fun copyRecursive(): Configuration { + TODO("Not yet implemented") + } + + override fun copyRecursive(dependencySpec: Spec): Configuration { + TODO("Not yet implemented") + } + + override fun copyRecursive(dependencySpec: Closure<*>): Configuration { + TODO("Not yet implemented") + } + + override fun setCanBeConsumed(allowed: Boolean) { + TODO("Not yet implemented") + } + + override fun isCanBeConsumed(): Boolean { + TODO("Not yet implemented") + } + + override fun setCanBeResolved(allowed: Boolean) { + TODO("Not yet implemented") + } + + override fun setCanBeDeclared(allowed: Boolean) { + TODO("Not yet implemented") + } + + override fun isCanBeDeclared(): Boolean { + TODO("Not yet implemented") + } + + override fun shouldResolveConsistentlyWith(versionsSource: Configuration): Configuration { + TODO("Not yet implemented") + } + + override fun disableConsistentResolution(): Configuration { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeDependencySet.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeDependencySet.kt new file mode 100644 index 0000000..ff2cf60 --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeDependencySet.kt @@ -0,0 +1,158 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.fake + +import groovy.lang.Closure +import org.gradle.api.Action +import org.gradle.api.DomainObjectCollection +import org.gradle.api.DomainObjectSet +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.DependencySet +import org.gradle.api.provider.Provider +import org.gradle.api.specs.Spec +import org.gradle.api.tasks.TaskDependency + +class FakeDependencySet(val dependencies : Set) : DependencySet { + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val innerIterator = dependencies.iterator() + + override fun hasNext(): Boolean = innerIterator.hasNext() + + override fun next(): Dependency = innerIterator.next() + + override fun remove() { + throw UnsupportedOperationException("Remove operation is not supported for this iterator") + } + } + } + + override fun contains(element: Dependency?): Boolean { + TODO("Not yet implemented") + } + + override fun add(element: Dependency?): Boolean { + TODO("Not yet implemented") + } + + override fun addAll(elements: Collection): Boolean { + TODO("Not yet implemented") + } + + override fun clear() { + TODO("Not yet implemented") + } + + + override fun remove(element: Dependency?): Boolean { + TODO("Not yet implemented") + } + + override fun removeAll(elements: Collection): Boolean { + TODO("Not yet implemented") + } + + override fun retainAll(elements: Collection): Boolean { + TODO("Not yet implemented") + } + + override fun containsAll(elements: Collection): Boolean { + TODO("Not yet implemented") + } + + override fun isEmpty(): Boolean { + TODO("Not yet implemented") + } + + override fun addLater(provider: Provider) { + TODO("Not yet implemented") + } + + override fun addAllLater(provider: Provider>) { + TODO("Not yet implemented") + } + + override fun withType(type: Class): DomainObjectSet { + TODO("Not yet implemented") + } + + override fun withType(type: Class, configureAction: Action): DomainObjectCollection { + TODO("Not yet implemented") + } + + override fun withType(type: Class, configureClosure: Closure<*>): DomainObjectCollection { + TODO("Not yet implemented") + } + + override fun matching(spec: Spec): DomainObjectSet { + TODO("Not yet implemented") + } + + override fun matching(spec: Closure<*>): DomainObjectSet { + TODO("Not yet implemented") + } + + override fun whenObjectAdded(action: Action): Action { + TODO("Not yet implemented") + } + + override fun whenObjectAdded(action: Closure<*>) { + TODO("Not yet implemented") + } + + override fun whenObjectRemoved(action: Action): Action { + TODO("Not yet implemented") + } + + override fun whenObjectRemoved(action: Closure<*>) { + TODO("Not yet implemented") + } + + override fun all(action: Action) { + TODO("Not yet implemented") + } + + override fun all(action: Closure<*>) { + TODO("Not yet implemented") + } + + override fun configureEach(action: Action) { + TODO("Not yet implemented") + } + + override fun findAll(spec: Closure<*>): MutableSet { + TODO("Not yet implemented") + } + + override fun getBuildDependencies(): TaskDependency { + TODO("Not yet implemented") + } + + override val size: Int + get() = TODO("Not yet implemented") +} \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeResolvedConfiguration.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeResolvedConfiguration.kt new file mode 100644 index 0000000..e4a806e --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/FakeResolvedConfiguration.kt @@ -0,0 +1,172 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.fake + +import org.gradle.api.artifacts.* +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier +import org.gradle.api.artifacts.component.ComponentIdentifier +import org.gradle.api.specs.Spec +import org.gradle.internal.component.AmbiguousVariantSelectionException +import java.io.File + +class FakeResolvedConfiguration( + private val dependencies: List, + private val throwAmbiguousException: Boolean = false +) : ResolvedConfiguration { + override fun getFirstLevelModuleDependencies(): Set { + return dependencies.map { dep -> + FakeResolvedDependency( + throwAmbiguousException = throwAmbiguousException, + dependency = dep + ) + }.toSet() + } + + override fun hasError(): Boolean { + TODO("Not yet implemented") + } + + override fun getLenientConfiguration(): LenientConfiguration { + TODO("Not yet implemented") + } + + override fun rethrowFailure() { + TODO("Not yet implemented") + } + + override fun getFiles(): MutableSet { + TODO("Not yet implemented") + } + + override fun getFiles(dependencySpec: Spec): MutableSet { + TODO("Not yet implemented") + } + + override fun getFirstLevelModuleDependencies(dependencySpec: Spec): MutableSet { + TODO("Not yet implemented") + } + + override fun getResolvedArtifacts(): MutableSet { + TODO("Not yet implemented") + } +} + +class FakeResolvedArtifact(private val dependency: Dependency) : ResolvedArtifact { + override fun getFile(): File { + return File("/path/to/${dependency.name}.aar") + } + + override fun getModuleVersion(): ResolvedModuleVersion { + TODO("Not yet implemented") + } + + override fun getName(): String { + TODO("Not yet implemented") + } + + override fun getType(): String { + TODO("Not yet implemented") + } + + override fun getExtension(): String? { + TODO("Not yet implemented") + } + + override fun getClassifier(): String? { + TODO("Not yet implemented") + } + + override fun getId(): ComponentArtifactIdentifier { + return object : ComponentArtifactIdentifier { + override fun getComponentIdentifier(): ComponentIdentifier { + return object :ComponentIdentifier{ + override fun getDisplayName(): String = dependency.name + override fun toString(): String = dependency.name + } + } + + override fun getDisplayName(): String = dependency.name + } + } +} + +class FakeResolvedDependency( + private val throwAmbiguousException: Boolean, + private val dependency: Dependency +) : ResolvedDependency { + override fun getModuleVersion(): String = dependency.version ?: "unspecified" + override fun getAllModuleArtifacts(): Set { + if (throwAmbiguousException) { + throw FakeAmbiguousVariantSelectionException() + } + + return setOf(FakeResolvedArtifact(dependency)) + } + + override fun getModuleGroup(): String = dependency.group ?: "" + + override fun getName(): String = dependency.name + + override fun getModuleName(): String { + TODO("Not yet implemented") + } + + override fun getConfiguration(): String { + TODO("Not yet implemented") + } + + override fun getModule(): ResolvedModuleVersion { + TODO("Not yet implemented") + } + + override fun getChildren(): MutableSet { + TODO("Not yet implemented") + } + + override fun getParents(): MutableSet { + TODO("Not yet implemented") + } + + override fun getModuleArtifacts(): MutableSet { + TODO("Not yet implemented") + } + + override fun getParentArtifacts(parent: ResolvedDependency): MutableSet { + TODO("Not yet implemented") + } + + override fun getArtifacts(parent: ResolvedDependency): MutableSet { + TODO("Not yet implemented") + } + + override fun getAllArtifacts(parent: ResolvedDependency): MutableSet { + TODO("Not yet implemented") + } +} + +class FakeAmbiguousVariantSelectionException() : AmbiguousVariantSelectionException("Fake Exception") \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/MockLogger.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/MockLogger.kt new file mode 100644 index 0000000..baf71fa --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/fake/MockLogger.kt @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.fake + +import com.grab.sizer.utils.Logger + +class MockLogger : Logger { + val loggedMessages = mutableListOf() + + override fun log(tag: String, message: String) { + loggedMessages.add("$tag: $message") + } + + override fun log(tag: String, e: Exception) { + loggedMessages.add("$tag: ${e.message}") + } + + override fun logDebug(tag: String, message: String) { + loggedMessages.add("DEBUG - $tag: $message") + } +} \ No newline at end of file diff --git a/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/utils/Utils.kt b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/utils/Utils.kt new file mode 100644 index 0000000..931f828 --- /dev/null +++ b/gradle-plugin/src/test/kotlin/com/grab/plugin/sizer/utils/Utils.kt @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +package com.grab.plugin.sizer.utils + +import org.junit.Assert + +internal inline fun assertThrows(expectedMessage: String, block: () -> Unit) { + try { + block() + Assert.fail("Expected ${T::class.simpleName} to be thrown") + } catch (e: Throwable) { + Assert.assertTrue(e is T) + Assert.assertEquals(expectedMessage, e.message) + } +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8e69fc6 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,28 @@ +# +# MIT License +# +# Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE +# + +kotlin.code.style=official diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..19a3db8 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,47 @@ +[versions] +gradle-plugin-publish = "0.14.0" +android-gradle-plugin = "8.5.1" +kotlin = "1.9.22" +kotlin-dsl = "4.3.1" +dagger = "2.47" +dexlib2 = "2.5.2" +clikt = "4.4.0" +bundletool = "1.14.1" +apkanalyzer = "31.5.1" +common = "31.5.1" +jackson = "2.15.0" +guava = "33.2.1-jre" +gson = "2.10.1" +influxdbClient = "2.24" +shadow-jar = "7.0.0" + +# Tests +junit = "4.13.2" + +[libraries] +android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" } +android-gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "android-gradle-plugin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +google-dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } +dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } +org-smali-dexlib2 = { module = "org.smali:dexlib2", version.ref = "dexlib2" } +clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } +android-tools-bundletool = { module = "com.android.tools.build:bundletool", version.ref = "bundletool" } +android-tools-apkanalyzer = { module = "com.android.tools.apkparser:apkanalyzer", version.ref = "apkanalyzer" } +android-tools-common = { module = "com.android.tools:common", version.ref = "common" } +jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } +jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +org-influxdb-client = { module = "org.influxdb:influxdb-java", version.ref = "influxdbClient" } + +# Tests +junit = { module = "junit:junit", version.ref = "junit" } + +[plugins] +kotlin-dsl = { id = "org.gradle.kotlin.kotlin-dsl", version.ref = "kotlin-dsl" } +gradle-plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "gradle-plugin-publish" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +johnrengelman-shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow-jar" } \ No newline at end of file diff --git a/gradle/publish-root-config.gradle b/gradle/publish-root-config.gradle new file mode 100644 index 0000000..aad0273 --- /dev/null +++ b/gradle/publish-root-config.gradle @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Grabtaxi Holdings PTE LTD (GRAB) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: "io.github.gradle-nexus.publish-plugin" + +ext["ossrhUsername"] = "" +ext["ossrhPassword"] = "" +ext["sonatypeStagingProfileId"] = "" +ext["signing.keyId"] = "" +ext["signing.password"] = "" +ext["signing.key"] = "" + +def localProperties = rootProject.file("local.properties") +if (localProperties.exists()) { + def p = new Properties() + new FileInputStream(localProperties).withCloseable { is -> p.load(is) } + p.each { name, value -> ext[name] = value } +} else { + ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME") + ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD") + ext["sonatypeStagingProfileId"] = System.getenv("SONATYPE_STAGING_PROFILE_ID") + ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID") + ext["signing.password"] = System.getenv("SIGNING_PASSWORD") + ext["signing.key"] = System.getenv("SIGNING_KEY") +} + +nexusPublishing { + repositories { + sonatype { + stagingProfileId = sonatypeStagingProfileId + username = ossrhUsername + password = ossrhPassword + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + } + } +} \ No newline at end of file diff --git a/gradle/publishing.gradle b/gradle/publishing.gradle new file mode 100644 index 0000000..aea9f02 --- /dev/null +++ b/gradle/publishing.gradle @@ -0,0 +1,107 @@ +/* + * Copyright 2022 Grabtaxi Holdings PTE LTD (GRAB) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +apply plugin: "maven-publish" +apply plugin: "signing" +apply plugin: "org.jetbrains.dokka" + +task sourcesJar(type: Jar) { + group = "publishing" + archiveClassifier.set("sources") + from sourceSets.main.java.srcDirs + from sourceSets.main.kotlin.srcDirs +} + +task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { + group = "publishing" + archiveClassifier.set("javadoc") + from dokkaJavadoc.outputDirectory +} + +artifacts { + archives javadocJar + archives sourcesJar +} + +afterEvaluate { + publishing { + publications { + // Gradle plugin marker if it exists + named("grazelPluginPluginMarkerMaven") { publication -> + configurePom(publication) + } + + release(MavenPublication) { publication -> + groupId project.findProperty("groupId") + artifactId project.name + version project.findProperty("versionName") + + from components.java + + artifact sourcesJar + artifact javadocJar + + configurePom(publication) + } + } + } +} + +private void configurePom(MavenPublication publication) { + publication.pom { + name = project.name + description = project.description + url = website + licenses { + license { + name = "The MIT License" + url = "https://github.com/grab/App-Sizer/blob/master/LICENSE" + } + } + developers { + developer { + id = "MinhNguyen-nvm" + name = "Minh Nguyen" + email = "minhnguyen.gtvt@gmail.com" + } + developer { + id = "arunkumar9t2" + name = "Arunkumar" + email = "hi@arunkumar.dev" + } + developer { + id = "minkuan88" + name = "Min Kuan Lim" + email = "minkuan88@hotmail.com" + } + } + scm { + connection = "git@github.com:grab/App-Sizer.git" + developerConnection = "git@github.com:grab/App-Sizer.git" + url = website + } + } +} + +afterEvaluate { + signing { + useInMemoryPgpKeys( + rootProject.ext["signing.keyId"].toString(), + rootProject.ext["signing.key"].toString(), + rootProject.ext["signing.password"].toString(), + ) + sign publishing.publications + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a1da8bb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,34 @@ +# +# MIT License +# +# Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE +# + +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..c1faa90 --- /dev/null +++ b/gradlew @@ -0,0 +1,233 @@ +#!/bin/sh + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/grafana/dashboard-to-import.json b/grafana/dashboard-to-import.json new file mode 100644 index 0000000..4fcbde0 --- /dev/null +++ b/grafana/dashboard-to-import.json @@ -0,0 +1,590 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "iteration": 1718528153248, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "description": "This graph illustrates the trend of app download sizes, grouped by their respective app versions. You have the option to apply a filter based on the Reference Device.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 6, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 9, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "alias": "Hover value", + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "hide": false, + "query": "SELECT \"size\" FROM \"app_size\" WHERE (\"type\" = 'apk_basic' and \"contributor\"= 'apk' and \"device_name\" =~ /^$reference_device$/ ) AND $timeFilter ", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series" + }, + { + "alias": "App Download Size for $tag_app_version", + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "hide": false, + "query": "SELECT \"size\" FROM \"app_size\" WHERE (\"type\" = 'apk_basic' and \"contributor\"= 'apk' and \"device_name\" =~ /^$reference_device$/ ) AND $timeFilter GROUP BY \"app_version\"", + "rawQuery": true, + "refId": "B", + "resultFormat": "time_series" + } + ], + "title": "App Download Size Trending", + "type": "timeseries" + }, + { + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "description": "A graph representation of the breakdown of the App Download Size by individual components. You have the option to filter by App Version, Reference Device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_contributor", + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "query": "SELECT last(\"size\") FROM \"app_size\" WHERE (\"type\" = 'apk' AND \"app_version\" =~ /^$app_version$/ AND \"device_name\" =~ /^$reference_device$/ AND \"contributor\" != 'apk' AND $timeFilter ) GROUP BY \"contributor\"", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series" + } + ], + "title": "App Download Size Breakdown by Components", + "type": "piechart" + }, + { + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "description": "A roster of teams, each accompanied by the corresponding size of their contribution to the total app download size. \nYou could filter by the App Version and the Reference Device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 4, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_contributor", + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "query": "SELECT last(\"size\") FROM \"app_size\" WHERE (\"type\"='team' and \"app_version\" =~ /^$app_version$/ and \"device_name\" =~ /^$reference_device$/ AND $timeFilter ) GROUP BY \"contributor\"", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series" + } + ], + "title": "App Download Size Breakdown by Teams", + "type": "piechart" + }, + { + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "description": "A list of module names associated with the selected 'Team Name' filter, accompanied by a graph illustrating the contribution of each module to the app download size.\nYou have the option to filter by App Version, Reference Device and Team Name", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 6, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_contributor", + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "query": "SELECT last(\"size\") FROM \"app_size\" WHERE (\"type\" = 'module' AND \"app_version\" =~ /^$app_version$/ and \"device_name\" =~ /^$reference_device$/ and \"owner\" =~ /^$tf_name$/ ) GROUP BY \"contributor\"", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series" + } + ], + "title": "Team Codebase Breakdown (Select the team name to see the detail)", + "type": "piechart" + }, + { + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "description": "A compilation of libraries, each denoted with the size it contributes to the overall App Download Size. You have the option to filter by App Version and Reference Device.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 7, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "alias": "$tag_contributor", + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "query": "SELECT last(\"size\") FROM \"app_size\" WHERE (\"type\" = 'library' AND \"app_version\" =~ /^$app_version$/ AND \"device_name\" =~ /^$reference_device$/ AND $timeFilter ) GROUP BY \"contributor\"\n", + "rawQuery": true, + "refId": "A", + "resultFormat": "time_series" + } + ], + "title": "App Download Size Breakdown by Libraries", + "type": "piechart" + }, + { + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "description": "A list of files exceeding the size threshold as set in the app-sizer configuration. You can apply filters based on App Version and Reference Device.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "displayMode": "auto", + "inspect": false + }, + "displayName": "${__field.labels}", + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 8, + "options": { + "footer": { + "enablePagination": false, + "fields": [ + "Size" + ], + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true + }, + "pluginVersion": "9.0.0", + "targets": [ + { + "alias": "", + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "query": "SELECT last(\"size\") as Size , \"contributor\" as \"File Name\", \"owner\" as Owner FROM \"app_size\" WHERE (\"type\" = 'large_file' AND \"app_version\" =~ /^$app_version$/ AND \"device_name\" =~ /^$reference_device$/ AND $timeFilter ) group by \"contributor\"\n", + "rawQuery": true, + "refId": "A", + "resultFormat": "table" + } + ], + "title": "Large Files", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "Size", + "Owner", + "File Name" + ] + } + } + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "1.0.2", + "value": "1.0.2" + }, + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "definition": "select DISTINCT(\"app_version\") from (select * from \"app_size\") ", + "hide": 0, + "includeAll": false, + "label": "App Version", + "multi": false, + "name": "app_version", + "options": [], + "query": "select DISTINCT(\"app_version\") from (select * from \"app_size\") ", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "device-1", + "value": "device-1" + }, + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "definition": "select DISTINCT(\"device_name\") from (select * from \"app_size\") ", + "hide": 0, + "includeAll": false, + "label": "Reference Device", + "multi": false, + "name": "reference_device", + "options": [], + "query": "select DISTINCT(\"device_name\") from (select * from \"app_size\") ", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "Platform", + "value": "Platform" + }, + "datasource": { + "type": "influxdb", + "uid": "m2unCvxIk" + }, + "definition": "select DISTINCT(\"owner\") from (select * from \"app_size\") where \"owner\" != 'NA'", + "hide": 0, + "includeAll": false, + "label": "Team name", + "multi": false, + "name": "tf_name", + "options": [], + "query": "select DISTINCT(\"owner\") from (select * from \"app_size\") where \"owner\" != 'NA'", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "App Download Size Breakdown", + "uid": "FBZO0xxSk", + "version": 51, + "weekStart": "" +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..ed4e8f6 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,48 @@ +site_name: App Sizer +site_description: "App Sizer is a tool designed to analyze the download size of Android applications" +site_author: "Grab, Inc" +site_url: "https://github.com/grab/AppSizer" +remote_branch: gh-pages + +repo_name: "grab/AppSizer" +repo_url: "https://github.com/grab/AppSizer" + +copyright: "Copyright 2024 Grabtaxi Holdings PTE LTE (GRAB)" + +theme: + name: "material" + palette: + primary: "cyan" + font: + text: "Roboto" + code: "Roboto Mono" + features: + - navigation.indexes + +nav: + - Home Page: index.md + - Configuration: + - Gradle Plugin: plugin.md + - Commandline Tool: cli.md + - Grafana & InfluxDb Docker: docker.md + - Reports: report.md + - Limitation: limitation.md + +plugins: + - search + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.magiclink + - pymdownx.tabbed + - pymdownx.superfences + - pymdownx.highlight + - codehilite: + guess_lang: false + - toc: + permalink: true + +# Customization +extra_css: + - "css/site.css" \ No newline at end of file diff --git a/sample/README.md b/sample/README.md new file mode 100644 index 0000000..96160aa --- /dev/null +++ b/sample/README.md @@ -0,0 +1,94 @@ +# App Sizer Sample Project + +This sample project demonstrates the integration and usage of the App Sizer tool, which analyzes the download size of Android applications. By providing detailed insights into the composition of your app's binary, App Sizer helps developers identify areas for size reduction, ultimately improving user acquisition and retention rates. + +## Project Structure + +``` +sample/ +├── app/ +├── android-module-level1/ +├── android-module-level2/ +├── kotlin-module/ +``` + +## Using App Sizer + +This sample project demonstrates two ways to use App Sizer: via the Gradle plugin and via the CLI tool. + +### Gradle Plugin Integration + +The App Sizer tool is configured in the `build.gradle` file of the `app` module. Look for the `appSizer` block: + +```groovy +appSizer { + projectInput { + // Configuration options + } + metrics { + // Output configuration + } +} +``` + +To run the App Sizer analysis using the Gradle plugin: + +1. Open a terminal in the sample project directory. +2. Execute the following command: + ``` + ./gradlew app:appSizeAnalysisProRelease --no-configure-on-demand + ``` + +Refer to the [App Sizer Plugin documentation](../docs/plugin.md) for detailed configuration options. + +### CLI Tool Integration + +This sample project also includes a configuration for the App Sizer CLI tool. + +1. The configuration file for the CLI tool is located at: + ``` + ./app-size-config/app-size-settings.yml + ``` + You can modify this file to adjust the CLI tool's settings. Refer to the [App Sizer CLI documentation](../docs/cli.md) for detailed configuration options. + +2. To execute the CLI analysis, run the following command from the project root: + ``` + sh exec-clt.sh + ``` + +This script will build & run the App Sizer CLI tool using the configuration specified in `app-size-settings.yml`. + + +## Understanding the Results + +After running the analysis, you can find the results in: + +For the Gradle plugin: +- InfluxDB (if configured) +- Markdown report: `app/build/sizer/reports/[option]-report.md` +- JSON report: `app/build/sizer/reports/[option]-metrics.json` + +For the CLI tool: +- Markdown report: `[root-project]/build/app-sizer/[option]-report.md` +- JSON report: `[root-project]/build/app-sizer/[option]-metrics.json` + +## Module Ownership + +The `module-owner.yml` file in the project root defines the ownership of different modules. This is used by App Sizer to attribute size contributions to different teams or components. +```yaml +Platform: + - app +Team1: + - android-module-level1 +Team2: + - android-module-level2 + - kotlin-module + +``` + +## Additional Resources + +- [App Sizer Documentation](../docs/index.md) +- [Configuring the Gradle Plugin](../docs/plugin.md) +- [Understanding the Reports](../docs/report.md) +- [App Sizer Limitations](../docs/limitation.md) \ No newline at end of file diff --git a/sample/android-module-level1/build.gradle b/sample/android-module-level1/build.gradle new file mode 100644 index 0000000..b2e7e5e --- /dev/null +++ b/sample/android-module-level1/build.gradle @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +plugins { + id 'com.sample.android.library' +} + +dependencies { + implementation project(":sample-group:android-module-level2") +} + +android { + namespace = "com.grab.android.sample.level1" +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/AndroidManifest.xml b/sample/android-module-level1/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9155354 --- /dev/null +++ b/sample/android-module-level1/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass100.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass100.kt new file mode 100644 index 0000000..bdb9edd --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass100.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass100 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return "aa" + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass51.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass51.kt new file mode 100644 index 0000000..3abc8c4 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass51.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass51 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass52().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass52.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass52.kt new file mode 100644 index 0000000..a198916 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass52.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass52 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass53().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass53.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass53.kt new file mode 100644 index 0000000..5c09846 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass53.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass53 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass54().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass54.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass54.kt new file mode 100644 index 0000000..fa004ea --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass54.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass54 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass55().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass55.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass55.kt new file mode 100644 index 0000000..fa627cb --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass55.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass55 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass56().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass56.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass56.kt new file mode 100644 index 0000000..193f36e --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass56.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass56 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass57().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass57.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass57.kt new file mode 100644 index 0000000..8d65b36 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass57.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass57 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass58().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass58.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass58.kt new file mode 100644 index 0000000..2a5e6b1 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass58.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass58 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass59().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass59.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass59.kt new file mode 100644 index 0000000..5b2535d --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass59.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass59 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass60().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass60.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass60.kt new file mode 100644 index 0000000..8288d31 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass60.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass60 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass61().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass61.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass61.kt new file mode 100644 index 0000000..0591364 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass61.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass61 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass62().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass62.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass62.kt new file mode 100644 index 0000000..16074f8 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass62.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass62 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass63().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass63.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass63.kt new file mode 100644 index 0000000..a81e0f5 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass63.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass63 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass64().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass64.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass64.kt new file mode 100644 index 0000000..92da1a6 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass64.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass64 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass65().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass65.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass65.kt new file mode 100644 index 0000000..2f91364 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass65.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass65 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass66().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass66.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass66.kt new file mode 100644 index 0000000..f02a7e5 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass66.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass66 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass67().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass67.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass67.kt new file mode 100644 index 0000000..e36ecba --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass67.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass67 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass68().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass68.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass68.kt new file mode 100644 index 0000000..db42b6b --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass68.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass68 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass69().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass69.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass69.kt new file mode 100644 index 0000000..82ba16d --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass69.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass69 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass70().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass70.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass70.kt new file mode 100644 index 0000000..2c61eb5 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass70.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass70 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass71().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass71.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass71.kt new file mode 100644 index 0000000..6174996 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass71.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass71 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass72().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass72.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass72.kt new file mode 100644 index 0000000..11ce6cd --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass72.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass72 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass73().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass73.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass73.kt new file mode 100644 index 0000000..ef852f0 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass73.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass73 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass74().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass74.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass74.kt new file mode 100644 index 0000000..2ac8f5f --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass74.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass74 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass75().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass75.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass75.kt new file mode 100644 index 0000000..a427c36 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass75.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass75 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass76().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass76.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass76.kt new file mode 100644 index 0000000..8527228 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass76.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass76 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass77().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass77.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass77.kt new file mode 100644 index 0000000..7fe6b56 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass77.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass77 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass78().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass78.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass78.kt new file mode 100644 index 0000000..d1ed9c5 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass78.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass78 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass79().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass79.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass79.kt new file mode 100644 index 0000000..e1afad4 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass79.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass79 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass80().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass80.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass80.kt new file mode 100644 index 0000000..74f6328 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass80.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass80 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass81().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass81.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass81.kt new file mode 100644 index 0000000..9212a76 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass81.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass81 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass82().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass82.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass82.kt new file mode 100644 index 0000000..e468d65 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass82.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass82 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass83().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass83.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass83.kt new file mode 100644 index 0000000..6b8f6be --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass83.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass83 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass84().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass84.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass84.kt new file mode 100644 index 0000000..9a6179f --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass84.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass84 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass85().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass85.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass85.kt new file mode 100644 index 0000000..3ad9989 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass85.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass85 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass86().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass86.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass86.kt new file mode 100644 index 0000000..4234022 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass86.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass86 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass87().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass87.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass87.kt new file mode 100644 index 0000000..c45ab24 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass87.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass87 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass88().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass88.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass88.kt new file mode 100644 index 0000000..ec1de0c --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass88.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass88 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass89().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass89.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass89.kt new file mode 100644 index 0000000..14fe550 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass89.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass89 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass90().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass90.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass90.kt new file mode 100644 index 0000000..838e0e7 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass90.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass90 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass91().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass91.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass91.kt new file mode 100644 index 0000000..12f4e35 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass91.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass91 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass92().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass92.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass92.kt new file mode 100644 index 0000000..1cf8d84 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass92.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass92 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass93().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass93.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass93.kt new file mode 100644 index 0000000..8941711 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass93.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass93 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass94().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass94.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass94.kt new file mode 100644 index 0000000..146e3a6 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass94.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass94 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass95().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass95.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass95.kt new file mode 100644 index 0000000..6c0ffff --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass95.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass95 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass96().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass96.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass96.kt new file mode 100644 index 0000000..d571f2e --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass96.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass96 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass97().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass97.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass97.kt new file mode 100644 index 0000000..5db4321 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass97.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass97 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass98().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass98.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass98.kt new file mode 100644 index 0000000..41d1d0b --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass98.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass98 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass99().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass99.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass99.kt new file mode 100644 index 0000000..975e2f5 --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/dummy/DummyClass99.kt @@ -0,0 +1,72 @@ +package com.grab.sample.dummy + +class DummyClass99 { + + fun method1(): String { + method2() + method3() + method4() + method5() + method6() + method7() + method8() + method9() + method10() + return DummyClass100().method1() + } + + fun method2(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method3(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method4(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method5(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method6(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method7(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method8(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method9(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + + fun method10(): String { + val str1 = (1..5).map { ('a'..'z').random() }.joinToString("") + val str2 = (1..5).map { ('a'..'z').random() }.joinToString("") + return str1 + str2 + } + +} \ No newline at end of file diff --git a/sample/android-module-level1/src/main/java/com/grab/android/sample/level1/Level1Activity.kt b/sample/android-module-level1/src/main/java/com/grab/android/sample/level1/Level1Activity.kt new file mode 100644 index 0000000..2b21c9f --- /dev/null +++ b/sample/android-module-level1/src/main/java/com/grab/android/sample/level1/Level1Activity.kt @@ -0,0 +1,15 @@ +package com.grab.android.sample.level1 + +import android.app.Activity +import android.os.Bundle +import android.os.PersistableBundle +import com.grab.sample.dummy.DummyClass1 +import com.grab.sample.dummy.DummyClass51 + +class Level1Activity : Activity() { + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + DummyClass1().method1() + DummyClass51().method1() + } +} \ No newline at end of file diff --git a/sample/app-size-config/app-size-settings.yml b/sample/app-size-config/app-size-settings.yml new file mode 100644 index 0000000..cfcc311 --- /dev/null +++ b/sample/app-size-config/app-size-settings.yml @@ -0,0 +1,37 @@ +project-input: + libraries-directory: "./build/gradle-cache/caches/modules-2/files-2.1" + modules-directory: "./" + project-root-dir: "./" + r8-mapping-file: "./app/build/outputs/mapping/proRelease/mapping.txt" + owner-mapping-file: "./module-owner.yml" + version: "1.0.1" + large-file-threshold: 10 + project-name: "sample" +apk-generation: + bundle-tool: "./binary/bundletool-all-1.15.4.jar" + app-bundle-file: "./app/build/outputs/bundle/proRelease/sample-bundle-file-pro-release.aab" + device-specs: + - "./app-size-config/device-1.json" + - "./app-size-config/device-2.json" + key-signing: + keystore-file: "./buildsystem/sample-release.keystore" + keystore-pw: "12345678" + key-alias: "key0" + key-pw: "12345678" +report: + output-directory: "./build/app-sizer" + custom-attributes: + pipelineId: "100" + influx-db-config: + db-name: "sizer" + url: "http://localhost:8086" + username: "root" + password: "root" + report-table-name: "app_size" + retention-policy: + name: "app_sizer" + duration: "360d" + shard-duration: "0m" + replication-factor: 2 + is-default: true + diff --git a/sample/app-size-config/device-1.json b/sample/app-size-config/device-1.json new file mode 100644 index 0000000..182a93c --- /dev/null +++ b/sample/app-size-config/device-1.json @@ -0,0 +1,6 @@ +{ + "supportedAbis": ["armeabi-v7a", "armeabi"], + "supportedLocales": ["en", "fr"], + "screenDensity": 480, + "sdkVersion": 23 +} \ No newline at end of file diff --git a/sample/app-size-config/device-2.json b/sample/app-size-config/device-2.json new file mode 100644 index 0000000..6ce607d --- /dev/null +++ b/sample/app-size-config/device-2.json @@ -0,0 +1,12 @@ +{ + "supportedAbis": [ + "arm64-v8a", + "armeabi-v7a", + "armeabi" + ], + "supportedLocales": [ + "en" + ], + "screenDensity": 320, + "sdkVersion": 27 +} diff --git a/sample/app/build.gradle b/sample/app/build.gradle new file mode 100644 index 0000000..547d1b7 --- /dev/null +++ b/sample/app/build.gradle @@ -0,0 +1,143 @@ +/* + * MIT License + * + * Copyright (c) 2024. Grabtaxi Holdings Pte Ltd (GRAB), All rights reserved. + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +plugins { + id 'com.sample.android.application' +} + +android { + namespace 'com.grab.android.sample' + defaultConfig { + applicationId "com.grab.android.sample" + archivesBaseName = "sample-bundle-file" + versionCode 1 + versionName "0.0.1" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + buildFeatures { + viewBinding true + } + + signingConfigs { + release { + // You need to specify these in your local.properties file or + // as environment variables + storeFile file("$rootProject.projectDir/buildsystem/sample-release.keystore") + storePassword '12345678' + keyAlias 'key0' + keyPassword '12345678' + } + } + + buildTypes { + release { + signingConfig signingConfigs.release + } + } + + + flavorDimensions += "version" + productFlavors { + // flavor for production + pro { + dimension = "version" + applicationIdSuffix = ".pro" + versionNameSuffix = "-pro" + } + // flavor for early access + gea { + dimension "service" + applicationIdSuffix = ".gea" + versionNameSuffix = "-gea" + } + } +} + + +dependencies { + implementation project(":android-module-level1") + implementation project(":kotlin-module") + implementation libs.androidx.appcompat + implementation libs.androidx.navigation.ktx + implementation libs.androidx.navigation.fragment.ktx + testImplementation libs.junit +} + +apply plugin: "com.grab.app-sizer" + +appSizer { + enabled = true + projectInput { + apk { + bundleToolFile = file("${rootProject.rootDir}/binary/bundletool-all-1.15.4.jar") + deviceSpecs = [ + file("${rootProject.rootDir}/app-size-config/device-1.json"), + file("${rootProject.rootDir}/app-size-config/device-2.json") + ] + } + variantFilter { variant -> + variant.setIgnore(variant.flavors.contains("gea")) + } + enableMatchDebugVariant = true + largeFileThreshold = 10 + teamMappingFile = file("${rootProject.rootDir}/module-owner.yml") + } + metrics { + /** Enable this block if you have an InfluxDb locally **/ + /** + influxDB { + dbName = "sizer" + reportTableName = "app_size" + url = "http://localhost:8086" + username = "root" + password = "root" + retentionPolicy { + name = "app_sizer" + duration = "360d" + shardDuration = "0m" + replicationFactor = 2 + setAsDefault = true + } + } + **/ + customAttributes.putAll( + ["pipeline_id": "1001"] + ) + } +} \ No newline at end of file diff --git a/sample/app/proguard-rules.pro b/sample/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/sample/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/sample/app/src/main/AndroidManifest.xml b/sample/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6374219 --- /dev/null +++ b/sample/app/src/main/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/app/src/main/java/com/grab/android/sample/FirstFragment.kt b/sample/app/src/main/java/com/grab/android/sample/FirstFragment.kt new file mode 100644 index 0000000..62e5b1a --- /dev/null +++ b/sample/app/src/main/java/com/grab/android/sample/FirstFragment.kt @@ -0,0 +1,45 @@ +package com.grab.android.sample + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.grab.android.sample.databinding.FragmentFirstBinding + +/** + * A simple [Fragment] subclass as the default destination in the navigation. + */ +class FirstFragment : Fragment() { + + private var _binding: FragmentFirstBinding? = null + + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonFirst.setOnClickListener { + findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/sample/app/src/main/java/com/grab/android/sample/MainActivity.kt b/sample/app/src/main/java/com/grab/android/sample/MainActivity.kt new file mode 100644 index 0000000..7ddc79e --- /dev/null +++ b/sample/app/src/main/java/com/grab/android/sample/MainActivity.kt @@ -0,0 +1,61 @@ +package com.grab.android.sample + +import android.os.Bundle +import com.google.android.material.snackbar.Snackbar +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import android.view.Menu +import android.view.MenuItem +import com.grab.android.sample.databinding.ActivityMainBinding +import com.grab.sample.dummy.KDummyClass1 + +class MainActivity : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navController = findNavController(R.id.nav_host_fragment_content_main) + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + + binding.fab.setOnClickListener { view -> + Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) + .setAction("Action", null).show() + } + + KDummyClass1().method1() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + return when (item.itemId) { + R.id.action_settings -> true + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_content_main) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/sample/app/src/main/java/com/grab/android/sample/SecondFragment.kt b/sample/app/src/main/java/com/grab/android/sample/SecondFragment.kt new file mode 100644 index 0000000..53be679 --- /dev/null +++ b/sample/app/src/main/java/com/grab/android/sample/SecondFragment.kt @@ -0,0 +1,44 @@ +package com.grab.android.sample + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.grab.android.sample.databinding.FragmentSecondBinding + +/** + * A simple [Fragment] subclass as the second destination in the navigation. + */ +class SecondFragment : Fragment() { + + private var _binding: FragmentSecondBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentSecondBinding.inflate(inflater, container, false) + return binding.root + + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonSecond.setOnClickListener { + findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/sample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..30d5477 --- /dev/null +++ b/sample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/app/src/main/res/drawable/ic_launcher_background.xml b/sample/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..645112f --- /dev/null +++ b/sample/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/app/src/main/res/layout/activity_main.xml b/sample/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..de5f092 --- /dev/null +++ b/sample/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/app/src/main/res/layout/content_main.xml b/sample/app/src/main/res/layout/content_main.xml new file mode 100644 index 0000000..5474693 --- /dev/null +++ b/sample/app/src/main/res/layout/content_main.xml @@ -0,0 +1,46 @@ + + + + + + + \ No newline at end of file diff --git a/sample/app/src/main/res/layout/fragment_first.xml b/sample/app/src/main/res/layout/fragment_first.xml new file mode 100644 index 0000000..dcf5fd9 --- /dev/null +++ b/sample/app/src/main/res/layout/fragment_first.xml @@ -0,0 +1,55 @@ + + + + + + + +