├── .github ├── dependabot.yml └── workflows │ ├── ci_test_and_publish.yml │ └── sample_app.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── RELEASING.md ├── affectedmoduledetector ├── .gitignore ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── dropbox │ │ └── affectedmoduledetector │ │ ├── AffectedModuleConfiguration.kt │ │ ├── AffectedModuleDetector.kt │ │ ├── AffectedModuleDetectorPlugin.kt │ │ ├── AffectedModuleTaskType.kt │ │ ├── AffectedTestConfiguration.kt │ │ ├── DependencyTracker.kt │ │ ├── FileLogger.kt │ │ ├── GitClient.kt │ │ ├── InternalTaskType.kt │ │ ├── ProjectGraph.kt │ │ ├── commitshaproviders │ │ ├── CommitShaProvider.kt │ │ ├── ForkCommit.kt │ │ ├── PreviousCommit.kt │ │ ├── SpecifiedBranchCommit.kt │ │ ├── SpecifiedBranchCommitMergeBase.kt │ │ └── SpecifiedRawCommitSha.kt │ │ └── util │ │ ├── File.kt │ │ └── OsQuirks.kt │ └── test │ └── kotlin │ └── com │ └── dropbox │ └── affectedmoduledetector │ ├── AffectedModuleConfigurationTest.kt │ ├── AffectedModuleDetectorImplTest.kt │ ├── AffectedModuleDetectorIntegrationTest.kt │ ├── AffectedModuleDetectorPluginTest.kt │ ├── AffectedTestConfigurationTest.kt │ ├── AttachLogsTestRule.kt │ ├── InternalTaskTypeTest.kt │ ├── ProjectGraphTest.kt │ ├── commitshaproviders │ ├── ForkCommitTest.kt │ ├── PreviousCommitTest.kt │ ├── SpecifiedBranchCommitMergeBaseTest.kt │ └── SpecifiedBranchCommitTest.kt │ ├── mocks │ ├── MockCommandRunner.kt │ └── MockCommitShaProvider.kt │ └── rules │ └── SetupAndroidProject.kt ├── build.gradle ├── dependency_graph.png ├── gradle.properties ├── gradle ├── jacoco.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .gitignore ├── build.gradle ├── buildSrc │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── kotlin │ │ └── com │ │ │ └── dropbox │ │ │ └── sample │ │ │ ├── AffectedTestsPlugin.kt │ │ │ ├── Dependencies.kt │ │ │ └── tasks │ │ │ └── AffectedTasksPlugin.kt │ │ └── resources │ │ └── META-INF │ │ └── gradle-plugins │ │ ├── com.dropbox.affectedtasksplugin.properties │ │ └── com.dropbox.sample.AffectedTestPlugin.properties ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample-app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── dropbox │ │ │ └── detector │ │ │ └── sample │ │ │ └── ExampleAndroidTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── dropbox │ │ │ │ └── detector │ │ │ │ └── sample │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── dropbox │ │ └── detector │ │ └── sample │ │ └── ExampleUnitTest.kt ├── sample-core │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── detekt-baseline.xml │ ├── proguard-rules.pro │ └── src │ │ ├── main │ │ └── AndroidManifest.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── dropbox │ │ └── detector │ │ └── sample_core │ │ └── ExampleUnitTest.kt ├── sample-jvm-module │ ├── .gitignore │ ├── build.gradle.kts │ ├── detekt-baseline.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── dropbox │ │ └── sample_jvm_module │ │ └── MyClass.kt ├── sample-util │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── detekt-baseline.xml │ ├── proguard-rules.pro │ └── src │ │ ├── main │ │ └── AndroidManifest.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── dropbox │ │ └── detector │ │ └── sample_util │ │ └── ExampleUnitTest.kt └── settings.gradle ├── settings.gradle └── specified_branch_difference.png /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/ci_test_and_publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build the project with Gradle, run integration tests, and release. 2 | # Because secrets are not available on external forks, this job is expected to fail 3 | # on external pull requests. 4 | 5 | name: Build project & run tests 6 | 7 | on: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-latest 16 | if: github.repository == 'dropbox/AffectedModuleDetector' && github.ref == 'refs/heads/main' 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Set up our JDK environment 23 | uses: actions/setup-java@v3 24 | with: 25 | distribution: 'zulu' 26 | java-version: '17' 27 | 28 | - name: Upload Artifacts 29 | run: ./gradlew publishAllPublicationsToMavenCentralRepository --no-daemon --no-parallel 30 | env: 31 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} 32 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} 33 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 34 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 35 | 36 | - name: Retrieve version 37 | run: | 38 | echo "VERSION_NAME=$(cat gradle.properties | grep -w "VERSION_NAME" | cut -d'=' -f2)" >> $GITHUB_ENV 39 | - name: Publish release 40 | run: ./gradlew closeAndReleaseRepository --no-daemon --no-parallel 41 | if: "!endsWith(env.VERSION_NAME, '-SNAPSHOT')" 42 | env: 43 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} 44 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} 45 | test: 46 | runs-on: ubuntu-latest 47 | timeout-minutes: 30 48 | 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | api-level: 53 | - 31 54 | 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v2 58 | 59 | - name: Set up our JDK environment 60 | uses: actions/setup-java@v3 61 | with: 62 | distribution: 'zulu' 63 | java-version: '17' 64 | - name: Run tests 65 | uses: reactivecircus/android-emulator-runner@v2 66 | with: 67 | api-level: 31 68 | profile: Nexus 6 69 | arch: x86_64 70 | force-avd-creation: false 71 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 72 | disable-animations: true 73 | script: ./gradlew assemble testCoverage 74 | env: 75 | API_LEVEL: ${{ matrix.api-level }} 76 | - name: Upload code coverage 77 | run: bash <(curl -s https://codecov.io/bash) 78 | -------------------------------------------------------------------------------- /.github/workflows/sample_app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build the sample app and ensure that all functionality works. 2 | 3 | name: Build sample app 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | sample-app: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 30 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Set up our JDK environment 20 | uses: actions/setup-java@v3 21 | with: 22 | distribution: 'zulu' 23 | java-version: '17' 24 | - name: Build sample 25 | run: | 26 | ./gradlew :affectedmoduledetector:publishToMavenLocal 27 | cd sample 28 | ./gradlew build 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | buildSrc/build 18 | buildSrc/.idea/ 19 | affected_module_detector.log -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Store is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Store to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open Source Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people’s personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone’s consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Consequences of Unacceptable Behavior 47 | 48 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 49 | 50 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 51 | 52 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 53 | 54 | ## 6. Reporting Guidelines 55 | 56 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. . 57 | 58 | 59 | 60 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 61 | 62 | ## 7. Addressing Grievances 63 | 64 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify The New York Times with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 65 | 66 | 67 | 68 | ## 8. Scope 69 | 70 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. 71 | 72 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 73 | 74 | ## 9. Contact info 75 | 76 | 77 | 78 | ## 10. License and attribution 79 | 80 | This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 81 | 82 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 83 | 84 | Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016-2017 The New York Times Company 190 | 191 | Copyright (c) 2019 Dropbox, Inc. 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Affected Module Detector 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.dropbox.affectedmoduledetector/affectedmoduledetector/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.dropbox.affectedmoduledetector/affectedmoduledetector/) 4 | 5 | [![Build Status](https://travis-ci.org/dropbox/AffectedModuleDetector.svg?branch=main)](https://travis-ci.org/dropbox/AffectedModuleDetector) 6 | 7 | [![codecov](https://codecov.io/gh/dropbox/AffectedModuleDetector/branch/main/graph/badge.svg)](https://codecov.io/gh/dropbox/AffectedModuleDetector) 8 | 9 | A Gradle Plugin to determine which modules were affected by a set of files in a commit. One use case for this plugin is for developers who would like to only run tests in modules which have changed in a given commit. 10 | 11 | ## Overview 12 | 13 | The AffectedModuleDetector will look at the last commit and determine which files have changed, it will then build a dependency graph of all the modules in the project. The detector exposes a set of APIs which can be used to determine whether a module was considered affected. 14 | 15 | ### Git 16 | 17 | The module detector assumes that it is being applied to a project stored in git and a git client is present on the system. It will query the last commit on the current branch to determine the list of files changed. 18 | 19 | ### Dependency Tracker 20 | 21 | The tracker will evaluate the project and find all modules and their dependencies for all configurations. 22 | 23 | ### Affected Module Detector 24 | 25 | The detector allows for three options for affected modules: 26 | - **Changed Projects**: These are projects which had files changed within them – enabled with `-Paffected_module_detector.changedProjects`) 27 | - **Dependent Projects**: These are projects which are dependent on projects which had changes within them – enabled with `-Paffected_module_detector.dependentProjects`) 28 | - **All Affected Projects**: This is the union of Changed Projects and Dependent Projects (this is the default configuration) 29 | 30 | These options can be useful depending on how many tests your project has and where in the integration cycle you would like to run them. For example, Changed Projects may be a good options when initially sending a Pull Requests, and All Affected Projects may be useful to use when a developer merges their pull request. 31 | 32 | The detector exposes APIs which will be helpful for your plugin to use. In particular, it exposes: 33 | - AffectedModuleDetector.configureTaskGuard - This will apply an `onlyIf` guard on your task and can be called either during configuration or execution 34 | - AffectedModuleDetector.isProjectAffected - This will return a boolean if the project has been affected. It can only be called after the project has been configured. 35 | 36 | 37 | In the example below, we're showing a hypothetical project graph and what projects would be considered affected if the All Affected Projects option was used and a change was made in the `:networking` module. 38 | 39 | 40 | ## Installation 41 | 42 | ```groovy 43 | // settings.gradle(.kts) 44 | pluginManagement { 45 | repositories { 46 | mavenCentral() 47 | gradlePluginPortal() 48 | } 49 | } 50 | 51 | // root build.gradle(.kts) 52 | plugins { 53 | id("com.dropbox.affectedmoduledetector") version "" 54 | } 55 | ``` 56 | 57 | Note that the plugin is currently published to Maven Central, so you need to add it to the repositories list in the `pluginsManagement` block. 58 | 59 | Alternatively, it can be consumed via manual buildscript dependency + plugin application. 60 | 61 | Apply the project to the root `build.gradle`: 62 | ```groovy 63 | buildscript { 64 | repositories { 65 | mavenCentral() 66 | } 67 | dependencies { 68 | classpath "com.dropbox.affectedmoduledetector:affectedmoduledetector:" 69 | } 70 | } 71 | //rootproject 72 | apply plugin: "com.dropbox.affectedmoduledetector" 73 | ``` 74 | 75 | If you want to develop a plugin using the APIs, add this to your `buildSrc`'s `dependencies` list: 76 | ``` 77 | implementation("com.dropbox.affectedmoduledetector:affectedmoduledetector:") 78 | ``` 79 | 80 | ## Configuration 81 | 82 | You can specify the configuration block for the detector in the root project: 83 | ```groovy 84 | affectedModuleDetector { 85 | baseDir = "${project.rootDir}" 86 | pathsAffectingAllModules = [ 87 | "buildSrc/" 88 | ] 89 | logFilename = "output.log" 90 | logFolder = "${project.rootDir}/output" 91 | compareFrom = "PreviousCommit" //default is PreviousCommit 92 | excludedModules = [ 93 | "sample-util", ":(app|library):.+" 94 | ] 95 | ignoredFiles = [ 96 | ".*\\.md", ".*\\.txt", ".*README" 97 | ] 98 | buildAllWhenNoProjectsChanged = true // default is true 99 | includeUncommitted = true 100 | top = "HEAD" 101 | customTasks = [ 102 | new AffectedModuleConfiguration.CustomTask( 103 | "runDetektByImpact", 104 | "detekt", 105 | "Run static analysis tool without auto-correction by Impact analysis" 106 | ) 107 | ] 108 | } 109 | ``` 110 | 111 | - `baseDir`: The root directory for all of the `pathsAffectingAllModules`. Used to validate the paths exist. 112 | - `pathsAffectingAllModules`: Paths to files or folders which if changed will trigger all modules to be considered affected 113 | - `logFilename`: A filename for the output detector to use 114 | - `logFolder`: A folder to output the log file in 115 | - `specifiedBranch`: A branch to specify changes against. Must be used in combination with configuration `compareFrom = "SpecifiedBranchCommit"` 116 | - `specifiedRawCommitSha`: A raw commit SHA to specify changes against. Must be used in combination with configuration `compareFrom = "SpecifiedRawCommitSha"` 117 | - `ignoredFiles`: A set of files that will be filtered out of the list of changed files retrieved by git. 118 | - `buildAllWhenNoProjectsChanged`: If true, the plugin will build all projects when no projects are considered affected. 119 | - `compareFrom`: A commit to compare the branch changes against. Can be either: 120 | - PreviousCommit: compare against the previous commit 121 | - ForkCommit: compare against the commit the branch was forked from 122 | - SpecifiedBranchCommit: compare against the last commit of `$specifiedBranch` using `git rev-parse` approach. 123 | - SpecifiedBranchCommitMergeBase: compare against the nearest ancestors with `$specifiedBranch` using `git merge base` approach. 124 | - SpecifiedRawCommitSha: compare against the provided raw commit SHA. 125 | 126 | **Note:** specify the branch to compare changes against using the `specifiedBranch` configuration before the `compareFrom` configuration 127 | - `excludedModules`: A list of modules that will be excluded from the build process, can be the name of a module, or a regex against the module gradle path 128 | - `includeUncommitted`: If uncommitted files should be considered affected 129 | - `top`: The top of the git log to use. Must be used in combination with configuration `includeUncommitted = false` 130 | - `customTasks`: set of [CustomTask](https://github.com/dropbox/AffectedModuleDetector/blob/main/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt) 131 | 132 | By default, the Detector will look for `assembleAndroidDebugTest`, `connectedAndroidDebugTest`, and `testDebug`. Modules can specify a configuration block to specify which variant tests to run: 133 | ```groovy 134 | affectedTestConfiguration { 135 | assembleAndroidTestTask = "assembleAndroidReleaseTest" 136 | runAndroidTestTask = "connectedAndroidReleaseTest" 137 | jvmTestTask = "testRelease" 138 | } 139 | ``` 140 | 141 | The plugin will create a few top level tasks that will assemble or run tests for only affected modules: 142 | * `./gradlew runAffectedUnitTests` - runs jvm tests 143 | * `./gradlew runAffectedAndroidTests` - runs connected tests 144 | * `./gradlew assembleAffectedAndroidTests` - assembles but does not run on device tests, useful when working with device labs 145 | 146 | ## SpecifiedBranchCommit vs SpecifiedBranchCommitMergeBase 147 | 148 | - SpecifiedBranchCommit using `git rev-parse` command for getting sha. 149 | - SpecifiedBranchCommitMergeBase using `git merge base` command for getting sha. 150 | 151 | What does it mean? 152 | When we run any AMD command we compare the current branch with the specified parent branch. Consider an example when, during the development of our feature, 153 | another developer merged his changes (9 files) into our common remote parent branch - "origin/dev". 154 | 155 | Please, look at picture: 156 | ![specified_branch_difference.png](specified_branch_difference.png) 157 | 158 | Suppose we have changed 6 files in our "feature" branch. 159 | 160 | 1. Behaviour of SpecifiedBranchCommit: 161 | AMD will show the result that 15 files were affected. Because our branch is not updated (pull) and AMD will see our 6 files and 9 files that were merged by another developer. 162 | 2. Behaviour of SpecifiedBranchCommitMergeBase: 163 | AMD will show the result that 6 files were affected. And this is the correct behavior. 164 | 165 | Hence, depending on your CI settings you have to configure AMD appropriately. 166 | 167 | ## SpecifiedRawCommitSha 168 | 169 | If you want AMD plugin to skip performing the git operations under-the-hood (like `git rev-parse` and `git merge base`), you can provide the raw commit SHA you wish to compare against. 170 | One of the main reasons you might want to follow this approach is, maybe your environment leverages [Git mirroring](https://docs.github.com/en/repositories/creating-and-managing-repositories/duplicating-a-repository) for speed optimizations, for example CI/CD environments. 171 | Mirroring _can_ lead to inaccurate common ancestor commits as the duplicated repository _may be_ out of sync with the true remote repository. 172 | 173 | ## Sample Usage 174 | 175 | Running the plugin generated tasks is quite simple. By default, if `affected_module_detector.enable` is not set, 176 | the generated tasks will run on all the modules. However, the plugin offers three different modes of operation so that it 177 | only executes the given task on a subset of projects. 178 | 179 | #### Running All Affected Projects (Changed Projects + Dependent Projects) 180 | 181 | To run all the projects affected by a change, run one of the tasks while enabling the module detector. 182 | 183 | ``` 184 | ./gradlew runAffectedUnitTests -Paffected_module_detector.enable 185 | ``` 186 | 187 | #### Running All Changed Projects 188 | 189 | To run all the projects that changed, run one of the tasks (while enabling the module detector) and with `-Paffected_module_detector.changedProjects` 190 | 191 | ``` 192 | ./gradlew runAffectedUnitTests -Paffected_module_detector.enable -Paffected_module_detector.changedProjects 193 | ``` 194 | 195 | #### Running All Dependent Projects 196 | 197 | To run all the dependent projects of projects that changed, run one of the tasks (while enabling the module detector) and with `-Paffected_module_detector.dependentProjects` 198 | 199 | ``` 200 | ./gradlew runAffectedUnitTests -Paffected_module_detector.enable -Paffected_module_detector.dependentProjects 201 | ``` 202 | 203 | ## Using the Sample project 204 | 205 | To run this on the sample app: 206 | 207 | 1. Publish the plugin to local maven: 208 | ``` 209 | ./gradlew :affectedmoduledetector:publishToMavenLocal 210 | ``` 211 | 212 | 2. Try running the following commands: 213 | ``` 214 | cd sample 215 | ./gradlew runAffectedUnitTests -Paffected_module_detector.enable 216 | ``` 217 | 218 | You should see zero tests run. Make a change within one of the modules and commit it. Rerunning the command should execute tests in that module and its dependent modules. 219 | 220 | ## Custom tasks 221 | 222 | If you want to add a custom gradle command to execute with impact analysis 223 | you must declare [AffectedModuleConfiguration.CustomTask](https://github.com/dropbox/AffectedModuleDetector/blob/main/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt) 224 | which is implementing the [AffectedModuleTaskType](https://github.com/dropbox/AffectedModuleDetector/blob/main/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleTaskType.kt) interface in the `build.gradle` configuration of your project: 225 | 226 | ```groovy 227 | // ... 228 | 229 | affectedModuleDetector { 230 | // ... 231 | customTasks = [ 232 | new AffectedModuleConfiguration.CustomTask( 233 | "runDetektByImpact", 234 | "detekt", 235 | "Run static analysis tool without auto-correction by Impact analysis" 236 | ) 237 | ] 238 | // ... 239 | } 240 | ``` 241 | 242 | **NOTE:** Please, test all your custom commands. 243 | If your custom task doesn't work correctly after testing, it might be that your task is quite complex 244 | and to work correctly it must use more gradle api's. 245 | Hence, you must create `buildSrc` module and write a custom plugin manually like [AffectedModuleDetectorPlugin](https://github.com/dropbox/AffectedModuleDetector/blob/main/affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPlugin.kt) 246 | 247 | ## Notes 248 | 249 | Special thanks to the AndroidX team for originally developing this project at https://android.googlesource.com/platform/frameworks/support/+/androidx-main/buildSrc/src/main/kotlin/androidx/build/dependencyTracker 250 | 251 | ## License 252 | 253 | Copyright (c) 2021 Dropbox, Inc. 254 | 255 | Licensed under the Apache License, Version 2.0 (the "License"); 256 | you may not use this file except in compliance with the License. 257 | You may obtain a copy of the License at 258 | 259 | http://www.apache.org/licenses/LICENSE-2.0 260 | 261 | Unless required by applicable law or agreed to in writing, software 262 | distributed under the License is distributed on an "AS IS" BASIS, 263 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 264 | See the License for the specific language governing permissions and 265 | limitations under the License. 266 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ======== 3 | 4 | 1. Change the version in the `gradle.properties` file to a non-SNAPSHOT verson. 5 | 2. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 6 | 3. `git tag -a vX.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version) 7 | * Run `git tag` to verify it. 8 | 4. `git push && git push --tags` 9 | * This should be pushed to your fork. 10 | 5. Create a PR with this commit and merge it. 11 | 6. Confirm the new artifacts are present in https://repo1.maven.org/maven2/com/dropbox/affectedmoduledetector/affectedmoduledetector/ 12 | * If the plugin fails to publish the release, you'll need to log into Sonatype and drop any repositories under 'Staging Repositories' and re-run the CI job to publish. 13 | 7. Go to https://github.com/dropbox/AffectedModuleDetector/releases and `Draft a new release` from the tag you just created 14 | 7. Update the top level `gradle.properties` to the next SNAPSHOT version. 15 | 8. `git commit -am "Prepare next development version."` 16 | 9. Create a PR with this commit and merge it. 17 | -------------------------------------------------------------------------------- /affectedmoduledetector/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /affectedmoduledetector/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'kotlin' 4 | id 'java-gradle-plugin' 5 | id "com.vanniktech.maven.publish" 6 | id "org.jlleitschuh.gradle.ktlint" 7 | } 8 | 9 | apply from: rootProject.file("gradle/jacoco.gradle") 10 | 11 | java { 12 | sourceCompatibility = JavaVersion.VERSION_11 13 | targetCompatibility = JavaVersion.VERSION_11 14 | } 15 | 16 | kotlin { 17 | jvmToolchain(11) 18 | } 19 | 20 | jacoco { 21 | toolVersion = "0.8.10" 22 | } 23 | 24 | gradlePlugin { 25 | plugins { 26 | affectedModuleDetectorPlugin { 27 | id = GROUP 28 | implementationClass = "com.dropbox.affectedmoduledetector.AffectedModuleDetectorPlugin" 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 35 | testImplementation("junit:junit:4.13.2") 36 | testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") 37 | testImplementation("com.google.truth:truth:1.4.2") 38 | } 39 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import com.dropbox.affectedmoduledetector.util.toOsSpecificPath 4 | import java.io.File 5 | import java.io.Serializable 6 | 7 | class AffectedModuleConfiguration : Serializable { 8 | 9 | /** 10 | * Implementation of [AffectedModuleTaskType] for easy adding of custom gradle task to 11 | * AffectedModuleDetector. You can declare a new instance of it in build.gradle. 12 | * 13 | * @see AffectedModuleTaskType - interface 14 | * @see customTasks - configuration field 15 | */ 16 | data class CustomTask( 17 | override val commandByImpact: String, 18 | override val originalGradleCommand: String, 19 | override val taskDescription: String 20 | ) : AffectedModuleTaskType 21 | 22 | /** 23 | * If you want to add a custom task for impact analysis you must set the list 24 | * of [AffectedModuleTaskType] implementations. 25 | * 26 | * Example: 27 | * `build.gradle 28 | * 29 | * affectedModuleDetector { 30 | * ... 31 | * customTasks = [ // <- list of custom gradle invokes 32 | * new AffectedModuleConfiguration.CustomTask( 33 | * "runSomeCustomTaskByImpact", 34 | * "someTaskForExample", 35 | * "Task description." 36 | * ) 37 | * ] 38 | * ... 39 | * } 40 | * ` 41 | * 42 | * @see AffectedModuleTaskType - interface 43 | * @see CustomTask - Implementation class 44 | * @see AffectedModuleDetectorPlugin - gradle plugin 45 | */ 46 | var customTasks = emptySet() 47 | 48 | /** 49 | * Folder to place the log in 50 | */ 51 | var logFolder: String? = null 52 | 53 | /** 54 | * Name for the log file 55 | */ 56 | var logFilename: String = "affected_module_detector.log" 57 | 58 | /** 59 | * Base directory to use for [pathsAffectingAllModules] 60 | */ 61 | var baseDir: String? = null 62 | 63 | /** 64 | * Files or folders which if changed will trigger all projects to be considered affected 65 | */ 66 | var pathsAffectingAllModules = setOf() 67 | set(value) { 68 | requireNotNull(baseDir) { 69 | "baseDir must be set to use pathsAffectingAllModules" 70 | } 71 | // Protect against users specifying the wrong path separator for their OS. 72 | field = value.map { it.toOsSpecificPath() }.toSet() 73 | } 74 | get() { 75 | field.forEach { path -> 76 | require(File(baseDir, path).exists()) { 77 | "Could not find expected path in pathsAffectingAllModules: $path" 78 | } 79 | } 80 | return field 81 | } 82 | 83 | var specifiedBranch: String? = null 84 | 85 | var specifiedRawCommitSha: String? = null 86 | 87 | var compareFrom: String = "PreviousCommit" 88 | set(value) { 89 | val commitShaProviders = listOf( 90 | "PreviousCommit", 91 | "ForkCommit", 92 | "SpecifiedBranchCommit", 93 | "SpecifiedBranchCommitMergeBase", 94 | "SpecifiedRawCommitSha" 95 | ) 96 | require(commitShaProviders.contains(value)) { 97 | "The property configuration compareFrom must be one of the following: ${commitShaProviders.joinToString(", ")}" 98 | } 99 | if (value == "SpecifiedBranchCommit" || value == "SpecifiedBranchCommitMergeBase") { 100 | requireNotNull(specifiedBranch) { 101 | "Specify a branch using the configuration specifiedBranch" 102 | } 103 | } 104 | if (value == "SpecifiedRawCommitSha") { 105 | requireNotNull(specifiedRawCommitSha) { 106 | "Provide a Commit SHA for the specifiedRawCommitSha property when using SpecifiedRawCommitSha comparison strategy." 107 | } 108 | } 109 | field = value 110 | } 111 | 112 | /** 113 | * A set of modules that will not be considered in the build process, even if changes are made in them. 114 | */ 115 | var excludedModules = emptySet() 116 | 117 | /** 118 | * A set of files that will be filtered out of the list of changed files retrieved by git. 119 | */ 120 | var ignoredFiles = emptySet() 121 | 122 | /** 123 | * If uncommitted files should be considered affected 124 | */ 125 | var includeUncommitted: Boolean = true 126 | 127 | /** 128 | * If we should build all projects when no projects have changed 129 | */ 130 | var buildAllWhenNoProjectsChanged: Boolean = true 131 | 132 | /** 133 | * The top of the git log to use, only used when [includeUncommitted] is false 134 | */ 135 | var top: String = "HEAD" 136 | set(value) { 137 | require(!includeUncommitted) { 138 | "Set includeUncommitted to false to set a custom top" 139 | } 140 | field = value 141 | } 142 | 143 | companion object { 144 | 145 | const val name = "affectedModuleDetector" 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPlugin.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Dropbox, Inc. All rights reserved. 3 | */ 4 | 5 | package com.dropbox.affectedmoduledetector 6 | 7 | import org.gradle.api.Plugin 8 | import org.gradle.api.Project 9 | import org.gradle.api.Task 10 | import org.gradle.api.tasks.testing.Test 11 | import org.gradle.util.GradleVersion 12 | import org.jetbrains.annotations.VisibleForTesting 13 | 14 | /** 15 | * This plugin creates and registers all affected test tasks. 16 | * Advantage is speed in not needing to skip modules at a large scale. 17 | * 18 | * Registers 3 tasks: 19 | * - `gradlew runAffectedUnitTests` - runs jvm tests 20 | * - `gradlew runAffectedAndroidTests` - runs connected tests 21 | * - `gradlew assembleAffectedAndroidTests` - assembles but does not run on device tests, 22 | * useful when working with device labs. 23 | * 24 | * Configure using affected module detector block after applying the plugin: 25 | * 26 | * affectedModuleDetector { 27 | * baseDir = "${project.rootDir}" 28 | * pathsAffectingAllModules = [ 29 | * "buildSrc/" 30 | * ] 31 | * logFolder = "${project.rootDir}". 32 | * } 33 | * 34 | * To enable affected module detection, you need to pass [com.dropbox.affectedmoduledetector.AffectedModuleDetector.Companion.ENABLE_ARG] 35 | * into the build as a command line parameter. 36 | * 37 | * See [AffectedModuleDetector] for additional flags. 38 | */ 39 | class AffectedModuleDetectorPlugin : Plugin { 40 | 41 | override fun apply(project: Project) { 42 | require( 43 | value = project.isRoot, 44 | lazyMessage = { 45 | "Must be applied to root project, but was found on ${project.path} instead." 46 | } 47 | ) 48 | 49 | registerSubprojectConfiguration(project) 50 | registerMainConfiguration(project) 51 | registerCustomTasks(project) 52 | registerTestTasks(project) 53 | 54 | project.gradle.projectsEvaluated { 55 | AffectedModuleDetector.configure(project) 56 | filterAndroidTests(project) 57 | filterJvmTests(project) 58 | filterCustomTasks(project) 59 | } 60 | } 61 | 62 | private fun registerMainConfiguration(project: Project) { 63 | project.extensions.add( 64 | AffectedModuleConfiguration.name, 65 | AffectedModuleConfiguration() 66 | ) 67 | } 68 | 69 | private fun registerSubprojectConfiguration(project: Project) { 70 | project.subprojects { subproject -> 71 | subproject.extensions.add( 72 | AffectedTestConfiguration.name, 73 | AffectedTestConfiguration() 74 | ) 75 | } 76 | } 77 | 78 | private fun registerCustomTasks(rootProject: Project) { 79 | val mainConfiguration = requireConfiguration(rootProject) 80 | 81 | rootProject.afterEvaluate { 82 | registerCustomTasks(rootProject, mainConfiguration.customTasks) 83 | } 84 | } 85 | 86 | @VisibleForTesting 87 | internal fun registerCustomTasks( 88 | rootProject: Project, 89 | customTasks: Set 90 | ) { 91 | customTasks.forEach { taskType -> 92 | rootProject.tasks.register(taskType.commandByImpact) { task -> 93 | task.group = CUSTOM_TASK_GROUP_NAME 94 | task.description = taskType.taskDescription 95 | disableConfigCache(task) 96 | 97 | rootProject.subprojects { project -> 98 | pluginIds.forEach { pluginId -> 99 | withPlugin(pluginId, task, taskType, project) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | @VisibleForTesting 107 | internal fun registerTestTasks(rootProject: Project) { 108 | registerInternalTask( 109 | rootProject = rootProject, 110 | taskType = InternalTaskType.ANDROID_JVM_TEST, 111 | groupName = TEST_TASK_GROUP_NAME 112 | ) 113 | 114 | registerInternalTask( 115 | rootProject = rootProject, 116 | taskType = InternalTaskType.ANDROID_TEST, 117 | groupName = TEST_TASK_GROUP_NAME 118 | ) 119 | 120 | registerInternalTask( 121 | rootProject = rootProject, 122 | taskType = InternalTaskType.ASSEMBLE_ANDROID_TEST, 123 | groupName = TEST_TASK_GROUP_NAME 124 | ) 125 | } 126 | 127 | @VisibleForTesting 128 | internal fun registerInternalTask( 129 | rootProject: Project, 130 | taskType: AffectedModuleTaskType, 131 | groupName: String 132 | ) { 133 | rootProject.tasks.register(taskType.commandByImpact) { task -> 134 | task.group = groupName 135 | task.description = taskType.taskDescription 136 | disableConfigCache(task) 137 | 138 | rootProject.subprojects { project -> 139 | pluginIds.forEach { pluginId -> 140 | if (pluginId == PLUGIN_JAVA_LIBRARY || pluginId == PLUGIN_KOTLIN) { 141 | if (taskType == InternalTaskType.ANDROID_JVM_TEST) { 142 | withPlugin(pluginId, task, InternalTaskType.JVM_TEST, project) 143 | } 144 | } else { 145 | withPlugin(pluginId, task, taskType, project) 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | @Suppress("UnstableApiUsage") 153 | private fun disableConfigCache(task: Task) { 154 | if (GradleVersion.current() >= GradleVersion.version("7.4")) { 155 | task.notCompatibleWithConfigurationCache("AMD requires knowledge of what has changed in the file system so we can not cache those values (https://github.com/dropbox/AffectedModuleDetector/issues/150)") 156 | } 157 | } 158 | 159 | private fun withPlugin( 160 | pluginId: String, 161 | task: Task, 162 | testType: AffectedModuleTaskType, 163 | project: Project 164 | ) { 165 | val config = requireConfiguration(project) 166 | 167 | fun isExcludedModule(configuration: AffectedModuleConfiguration, path: String): Boolean { 168 | return configuration.excludedModules.find { path.startsWith(":$it") } != null 169 | } 170 | 171 | project.pluginManager.withPlugin(pluginId) { 172 | getAffectedPath(testType, project)?.let { path -> 173 | val pathOrNull = project.tasks.findByPath(path) 174 | val onlyIf = when { 175 | pathOrNull == null -> false 176 | !AffectedModuleDetector.isProjectEnabled(pathOrNull.project) -> true 177 | else -> AffectedModuleDetector.isProjectAffected(pathOrNull.project) 178 | } 179 | 180 | if (onlyIf && AffectedModuleDetector.isProjectProvided(project) && !isExcludedModule(config, path)) { 181 | task.dependsOn(path) 182 | } 183 | pathOrNull?.onlyIf { onlyIf } 184 | } 185 | } 186 | } 187 | 188 | private fun getAffectedPath( 189 | taskType: AffectedModuleTaskType, 190 | project: Project 191 | ): String? { 192 | val tasks = requireNotNull( 193 | value = project.extensions.findByName(AffectedTestConfiguration.name), 194 | lazyMessage = { "Unable to find ${AffectedTestConfiguration.name} in $project" } 195 | ) as AffectedTestConfiguration 196 | 197 | return when (taskType) { 198 | InternalTaskType.ANDROID_TEST -> { 199 | getPathAndTask(project, tasks.runAndroidTestTask) 200 | } 201 | 202 | InternalTaskType.ASSEMBLE_ANDROID_TEST -> { 203 | getPathAndTask(project, tasks.assembleAndroidTestTask) 204 | } 205 | 206 | InternalTaskType.ANDROID_JVM_TEST -> { 207 | getPathAndTask(project, tasks.jvmTestTask) 208 | } 209 | 210 | InternalTaskType.JVM_TEST -> { 211 | if (tasks.jvmTestTask != AffectedTestConfiguration.DEFAULT_JVM_TEST_TASK) { 212 | getPathAndTask(project, tasks.jvmTestTask) 213 | } else { 214 | getPathAndTask(project, taskType.originalGradleCommand) 215 | } 216 | } 217 | 218 | else -> { 219 | getPathAndTask(project, taskType.originalGradleCommand) 220 | } 221 | } 222 | } 223 | 224 | private fun getPathAndTask(project: Project, task: String?): String? { 225 | return if (task.isNullOrBlank()) null else "${project.path}:$task" 226 | } 227 | 228 | private fun filterAndroidTests(project: Project) { 229 | val tracker = DependencyTracker(project, null) 230 | project.tasks.configureEach { task -> 231 | if (task.name.contains(ANDROID_TEST_PATTERN)) { 232 | tracker.findAllDependents(project.projectPath).forEach { dependentProject -> 233 | project.rootProject.findProject(dependentProject.path)?.tasks?.forEach { dependentTask -> 234 | AffectedModuleDetector.configureTaskGuard(dependentTask) 235 | } 236 | } 237 | } 238 | } 239 | } 240 | 241 | private fun filterCustomTasks(project: Project) { 242 | project.tasks.configureEach { task -> 243 | if (task.group == CUSTOM_TASK_GROUP_NAME) { 244 | AffectedModuleDetector.configureTaskGuard(task) 245 | } 246 | } 247 | } 248 | 249 | // Only allow unit tests to run if the AffectedModuleDetector says to include them 250 | private fun filterJvmTests(project: Project) { 251 | project.tasks.withType(Test::class.java).configureEach { task -> 252 | AffectedModuleDetector.configureTaskGuard(task) 253 | } 254 | } 255 | 256 | private fun requireConfiguration(project: Project): AffectedModuleConfiguration { 257 | return requireNotNull( 258 | value = project.rootProject.extensions.findByName(AffectedModuleConfiguration.name), 259 | lazyMessage = { "Unable to find ${AffectedModuleConfiguration.name} in ${project.rootProject}" } 260 | ) as AffectedModuleConfiguration 261 | } 262 | 263 | companion object { 264 | 265 | @VisibleForTesting 266 | internal const val TEST_TASK_GROUP_NAME = "Affected Module Detector" 267 | 268 | @VisibleForTesting 269 | internal const val CUSTOM_TASK_GROUP_NAME = "Affected Module Detector custom tasks" 270 | 271 | private const val PLUGIN_ANDROID_APPLICATION = "com.android.application" 272 | private const val PLUGIN_ANDROID_LIBRARY = "com.android.library" 273 | private const val PLUGIN_JAVA_LIBRARY = "java" 274 | private const val PLUGIN_KOTLIN = "kotlin" 275 | 276 | private const val ANDROID_TEST_PATTERN = "AndroidTest" 277 | 278 | private val pluginIds = listOf( 279 | PLUGIN_ANDROID_APPLICATION, 280 | PLUGIN_ANDROID_LIBRARY, 281 | PLUGIN_JAVA_LIBRARY, 282 | PLUGIN_KOTLIN 283 | ) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleTaskType.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * For creating a custom task which will be run only if module was affected 7 | * just override fields in your data structure which implements this interface. 8 | * 9 | * Your data structure must override all this variable 10 | */ 11 | interface AffectedModuleTaskType : Serializable { 12 | 13 | /** 14 | * Console command `./gradlew [commandByImpact]` which will run the original 15 | * command `./gradlew originalCommand` on modules affected by diff changes. 16 | */ 17 | val commandByImpact: String 18 | 19 | /** 20 | * The original console command `./gradlew [originalGradleCommand]` that does something. 21 | * Example: 22 | * - :connectedDebugAndroidTest 23 | * - :assembleDebugAndroidTest 24 | * - :detekt 25 | */ 26 | val originalGradleCommand: String 27 | 28 | /** 29 | * Description of new gradle task 30 | */ 31 | val taskDescription: String 32 | } 33 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/AffectedTestConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | /** 4 | * Used to configure which variant to run for affected tasks by adding following block to modules 5 | * affectedTestConfiguration{ 6 | * assembleAndroidTestTask = "assembleDevDebugAndroidTest" 7 | * } 8 | */ 9 | open class AffectedTestConfiguration { 10 | 11 | var assembleAndroidTestTask: String? = DEFAULT_ASSEMBLE_ANDROID_TEST_TASK 12 | var runAndroidTestTask: String? = DEFAULT_ANDROID_TEST_TASK 13 | var jvmTestTask: String? = DEFAULT_JVM_TEST_TASK 14 | 15 | companion object { 16 | const val name = "affectedTestConfiguration" 17 | 18 | internal const val DEFAULT_JVM_TEST_TASK = "testDebugUnitTest" 19 | internal const val DEFAULT_NON_ANDROID_JVM_TEST_TASK = "test" 20 | internal const val DEFAULT_ASSEMBLE_ANDROID_TEST_TASK = "assembleDebugAndroidTest" 21 | internal const val DEFAULT_ANDROID_TEST_TASK = "connectedDebugAndroidTest" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/DependencyTracker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (c) 2020, Dropbox, Inc. All rights reserved. 19 | */ 20 | package com.dropbox.affectedmoduledetector 21 | 22 | import org.gradle.api.Project 23 | import org.gradle.api.artifacts.ProjectDependency 24 | import org.gradle.api.logging.Logger 25 | import java.io.Serializable 26 | 27 | /** 28 | * Utility class that traverses all project dependencies and discover which modules depend on each 29 | * other. This is mainly used by [AffectedModuleDetector] to find out which projects should be run. 30 | */ 31 | class DependencyTracker(rootProject: Project, logger: Logger?) : Serializable { 32 | val dependentList: Map> 33 | 34 | init { 35 | val result = mutableMapOf>() 36 | val stringBuilder = StringBuilder() 37 | rootProject.subprojects.forEach { project -> 38 | project.configurations.forEach { config -> 39 | config.dependencies.filterIsInstance().forEach { 40 | stringBuilder.append( 41 | "there is a dependency from ${project.path} (${config.name}) to " + 42 | it.dependencyProject.path + 43 | "\n" 44 | ) 45 | result.getOrPut(it.dependencyProject.projectPath) { mutableSetOf() }.add(project.projectPath) 46 | } 47 | } 48 | } 49 | logger?.info(stringBuilder.toString()) 50 | dependentList = result 51 | } 52 | 53 | fun findAllDependents(projectPath: ProjectPath): Set { 54 | val result = mutableSetOf() 55 | fun addAllDependents(projectPath: ProjectPath) { 56 | if (result.add(projectPath)) { 57 | dependentList[projectPath]?.forEach(::addAllDependents) 58 | } 59 | } 60 | addAllDependents(projectPath) 61 | // the projectPath isn't a dependent of itself 62 | return result.minus(projectPath) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/FileLogger.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.dropbox.affectedmoduledetector 18 | 19 | import org.gradle.api.logging.LogLevel 20 | import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLogger 21 | import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext 22 | import org.gradle.internal.time.Clock 23 | import java.io.File 24 | import java.io.Serializable 25 | 26 | /** Gradle logger that logs to a file */ 27 | class FileLogger(val file: File) : Serializable { 28 | @Transient var impl: OutputEventListenerBackedLogger? = null 29 | 30 | fun toLogger(): OutputEventListenerBackedLogger { 31 | if (impl == null) { 32 | impl = 33 | OutputEventListenerBackedLogger( 34 | "amd", 35 | OutputEventListenerBackedLoggerContext(Clock { System.currentTimeMillis() }) 36 | .also { 37 | it.level = LogLevel.DEBUG 38 | it.setOutputEventListener { file.appendText(it.toString() + "\n") } 39 | }, 40 | Clock { System.currentTimeMillis() } 41 | ) 42 | } 43 | return impl!! 44 | } 45 | 46 | fun lifecycle(text: String) { 47 | toLogger().lifecycle(text) 48 | } 49 | 50 | fun info(text: String) { 51 | toLogger().info(text) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/GitClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (c) 2020, Dropbox, Inc. All rights reserved. 19 | */ 20 | 21 | package com.dropbox.affectedmoduledetector 22 | 23 | import com.dropbox.affectedmoduledetector.GitClientImpl.Companion.CHANGED_FILES_CMD_PREFIX 24 | import com.dropbox.affectedmoduledetector.commitshaproviders.CommitShaProviderConfiguration 25 | import com.dropbox.affectedmoduledetector.commitshaproviders.ForkCommit 26 | import com.dropbox.affectedmoduledetector.commitshaproviders.PreviousCommit 27 | import com.dropbox.affectedmoduledetector.commitshaproviders.SpecifiedBranchCommit 28 | import com.dropbox.affectedmoduledetector.commitshaproviders.SpecifiedBranchCommitMergeBase 29 | import com.dropbox.affectedmoduledetector.commitshaproviders.SpecifiedRawCommitSha 30 | import com.dropbox.affectedmoduledetector.util.toOsSpecificLineEnding 31 | import com.dropbox.affectedmoduledetector.util.toOsSpecificPath 32 | import org.gradle.api.Project 33 | import org.gradle.api.file.DirectoryProperty 34 | import org.gradle.api.logging.Logger 35 | import org.gradle.api.provider.Provider 36 | import org.gradle.api.provider.SetProperty 37 | import org.gradle.api.provider.ValueSource 38 | import org.gradle.api.provider.ValueSourceParameters 39 | import java.io.File 40 | import java.util.concurrent.TimeUnit 41 | 42 | interface GitClient { 43 | fun findChangedFiles( 44 | project: Project, 45 | ): Provider> 46 | 47 | fun getGitRoot(): File 48 | 49 | /** 50 | * Abstraction for running execution commands for testability 51 | */ 52 | interface CommandRunner { 53 | /** 54 | * Executes the given shell command and returns the stdout as a string. 55 | */ 56 | fun execute(command: String): String 57 | 58 | /** 59 | * Executes the given shell command and returns the stdout by lines. 60 | */ 61 | fun executeAndParse(command: String): List 62 | 63 | /** 64 | * Executes the given shell command and returns the first stdout line. 65 | */ 66 | fun executeAndParseFirst(command: String): String 67 | } 68 | } 69 | 70 | typealias Sha = String 71 | 72 | /** 73 | * A simple git client that uses system process commands to communicate with the git setup in the 74 | * given working directory. 75 | */ 76 | @Suppress("UnstableApiUsage") 77 | internal class GitClientImpl( 78 | /** 79 | * The root location for git 80 | */ 81 | private val workingDir: File, 82 | private val logger: FileLogger?, 83 | private val commitShaProviderConfiguration: CommitShaProviderConfiguration, 84 | private val ignoredFiles: Set? 85 | ) : GitClient { 86 | 87 | /** 88 | * Finds changed file paths 89 | */ 90 | override fun findChangedFiles( 91 | project: Project, 92 | ): Provider> { 93 | return project.providers.of(GitChangedFilesSource::class.java) { 94 | it.parameters.commitShaProvider = commitShaProviderConfiguration 95 | it.parameters.workingDir.set(workingDir) 96 | it.parameters.logger = logger 97 | it.parameters.ignoredFiles.set(ignoredFiles) 98 | } 99 | } 100 | 101 | private fun findGitDirInParentFilepath(filepath: File): File? { 102 | var curDirectory: File = filepath 103 | while (curDirectory.path != "/") { 104 | if (File("$curDirectory/.git").exists()) { 105 | return curDirectory 106 | } 107 | curDirectory = curDirectory.parentFile 108 | } 109 | return null 110 | } 111 | 112 | override fun getGitRoot(): File { 113 | return findGitDirInParentFilepath(workingDir) ?: workingDir 114 | } 115 | 116 | companion object { 117 | // -M95 is necessary to detect certain file moves. See https://github.com/dropbox/AffectedModuleDetector/issues/60 118 | const val CHANGED_FILES_CMD_PREFIX = "git --no-pager diff --name-only -M95" 119 | } 120 | } 121 | 122 | private class RealCommandRunner( 123 | private val workingDir: File, 124 | private val logger: Logger? 125 | ) : GitClient.CommandRunner { 126 | override fun execute(command: String): String { 127 | val parts = command.split("\\s".toRegex()) 128 | logger?.info("running command $command in $workingDir") 129 | val proc = ProcessBuilder(*parts.toTypedArray()) 130 | .directory(workingDir) 131 | .redirectOutput(ProcessBuilder.Redirect.PIPE) 132 | .redirectError(ProcessBuilder.Redirect.PIPE) 133 | .start() 134 | 135 | val stdout = proc 136 | .inputStream 137 | .bufferedReader() 138 | .readText() 139 | val stderr = proc 140 | .errorStream 141 | .bufferedReader() 142 | .readText() 143 | 144 | proc.waitFor(5, TimeUnit.MINUTES) 145 | 146 | val message = stdout + stderr 147 | if (stderr != "") { 148 | logger?.error("Response: $message") 149 | } else { 150 | logger?.info("Response: $message") 151 | } 152 | check(proc.exitValue() == 0) { "Nonzero exit value running git command." } 153 | return stdout 154 | } 155 | 156 | override fun executeAndParse(command: String): List { 157 | return execute(command).toOsSpecificLineEnding() 158 | .split(System.lineSeparator()) 159 | .map { it.toOsSpecificPath() } 160 | .filterNot { it.isEmpty() } 161 | } 162 | 163 | override fun executeAndParseFirst(command: String): String { 164 | return requireNotNull( 165 | executeAndParse(command) 166 | .firstOrNull() 167 | ?.split(" ") 168 | ?.firstOrNull() 169 | ) { 170 | "No value from command: $command provided" 171 | } 172 | } 173 | } 174 | 175 | /** Provides changed files since the last merge by calling git in [Parameters.workingDir]. */ 176 | @Suppress("UnstableApiUsage") 177 | internal abstract class GitChangedFilesSource : 178 | ValueSource, GitChangedFilesSource.Parameters> { 179 | interface Parameters : ValueSourceParameters { 180 | var commitShaProvider: CommitShaProviderConfiguration 181 | val workingDir: DirectoryProperty 182 | var logger: FileLogger? 183 | val ignoredFiles: SetProperty 184 | } 185 | 186 | private val gitRoot by lazy { 187 | findGitDirInParentFilepath(parameters.workingDir.get().asFile) 188 | } 189 | 190 | private val commandRunner: GitClient.CommandRunner by lazy { 191 | RealCommandRunner( 192 | workingDir = gitRoot ?: parameters.workingDir.get().asFile, 193 | logger = null 194 | ) 195 | } 196 | 197 | override fun obtain(): List { 198 | val top = parameters.commitShaProvider.top 199 | val sha = getSha() 200 | 201 | // use this if we don't want local changes 202 | val changedFiles = commandRunner.executeAndParse( 203 | if (parameters.commitShaProvider.includeUncommitted) { 204 | "$CHANGED_FILES_CMD_PREFIX $sha" 205 | } else { 206 | "$CHANGED_FILES_CMD_PREFIX $top..$sha" 207 | } 208 | ) 209 | 210 | return parameters.ignoredFiles.orNull 211 | .orEmpty() 212 | .map { it.toRegex() } 213 | .foldRight(changedFiles) { ignoredFileRegex: Regex, fileList: List -> 214 | fileList.filterNot { it.matches(ignoredFileRegex) } 215 | } 216 | .filterNot { it.isEmpty() } 217 | } 218 | 219 | private fun findGitDirInParentFilepath(filepath: File): File? { 220 | var curDirectory: File = filepath 221 | while (curDirectory.path != "/") { 222 | if (File("$curDirectory/.git").exists()) { 223 | return curDirectory 224 | } 225 | curDirectory = curDirectory.parentFile 226 | } 227 | return null 228 | } 229 | 230 | private fun getSha(): Sha { 231 | val specifiedBranch = parameters.commitShaProvider.specifiedBranch 232 | val specifiedSha = parameters.commitShaProvider.specifiedSha 233 | val type = when (parameters.commitShaProvider.type) { 234 | "PreviousCommit" -> PreviousCommit() 235 | "ForkCommit" -> ForkCommit() 236 | "SpecifiedBranchCommit" -> { 237 | requireNotNull(specifiedBranch) { 238 | "Specified branch must be defined" 239 | } 240 | SpecifiedBranchCommit(specifiedBranch) 241 | } 242 | "SpecifiedBranchCommitMergeBase" -> { 243 | requireNotNull(specifiedBranch) { 244 | "Specified branch must be defined" 245 | } 246 | SpecifiedBranchCommitMergeBase(specifiedBranch) 247 | } 248 | "SpecifiedRawCommitSha" -> { 249 | requireNotNull(specifiedSha) { 250 | "Provide a Commit SHA for the specifiedRawCommitSha property when using SpecifiedRawCommitSha comparison strategy." 251 | } 252 | SpecifiedRawCommitSha(specifiedSha) 253 | } 254 | else -> throw IllegalArgumentException("Unsupported compareFrom type") 255 | } 256 | return type.get(commandRunner) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/InternalTaskType.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | internal enum class InternalTaskType( 4 | override val commandByImpact: String, 5 | override val originalGradleCommand: String, 6 | override val taskDescription: String 7 | ) : AffectedModuleTaskType { 8 | 9 | ANDROID_TEST( 10 | commandByImpact = "runAffectedAndroidTests", 11 | originalGradleCommand = AffectedTestConfiguration.DEFAULT_ANDROID_TEST_TASK, 12 | taskDescription = "Runs all affected Android Tests. Requires a connected device." 13 | ), 14 | 15 | ASSEMBLE_ANDROID_TEST( 16 | commandByImpact = "assembleAffectedAndroidTests", 17 | originalGradleCommand = AffectedTestConfiguration.DEFAULT_ASSEMBLE_ANDROID_TEST_TASK, 18 | taskDescription = "Assembles all affected Android Tests. Useful when working with device labs." 19 | ), 20 | 21 | ANDROID_JVM_TEST( 22 | commandByImpact = "runAffectedUnitTests", 23 | originalGradleCommand = AffectedTestConfiguration.DEFAULT_JVM_TEST_TASK, 24 | taskDescription = "Runs all affected unit tests." 25 | ), 26 | 27 | JVM_TEST( 28 | commandByImpact = "", // inner type. This type doesn't registered in gradle 29 | originalGradleCommand = AffectedTestConfiguration.DEFAULT_NON_ANDROID_JVM_TEST_TASK, 30 | taskDescription = "Runs all affected unit tests for non Android modules." 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/ProjectGraph.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (c) 2020, Dropbox, Inc. All rights reserved. 19 | */ 20 | 21 | package com.dropbox.affectedmoduledetector 22 | 23 | import org.gradle.api.Project 24 | import org.gradle.api.logging.Logger 25 | import java.io.File 26 | import java.io.Serializable 27 | 28 | /** Creates a project graph for fast lookup by file path */ 29 | class ProjectGraph(project: Project, logger: Logger? = null) : Serializable { 30 | private val rootNode: Node 31 | 32 | init { 33 | // always use cannonical file: b/112205561 34 | logger?.info("initializing ProjectGraph") 35 | rootNode = Node() 36 | val rootProjectDir = project.getSupportRootFolder().canonicalFile 37 | val projects = 38 | if (rootProjectDir == project.rootDir.canonicalFile) { 39 | project.subprojects 40 | } else { 41 | // include root project if it is not the main AndroidX project. 42 | project.subprojects + project 43 | } 44 | projects.forEach { 45 | logger?.info("creating node for ${it.path}") 46 | val relativePath = it.projectDir.canonicalFile.toRelativeString(rootProjectDir) 47 | val sections = relativePath.split(File.separatorChar) 48 | 49 | // If the subproject is not a child of the root project (in the File directory sense) 50 | // then we are in some weird non standard quixotic project and our sections are going 51 | // to have ".." characters indicating that we need to traverse up one level. However 52 | // we need to filter these out because this will not match the parent child 53 | // dependency relationship that Gradle will produce. 54 | val realSections = sections.filter { section -> section != ".." } 55 | 56 | logger?.info("relative path: $relativePath , sections: $realSections") 57 | val leaf = realSections.fold(rootNode) { left, right -> left.getOrCreateNode(right) } 58 | leaf.projectPath = it.projectPath 59 | } 60 | logger?.info("finished creating ProjectGraph") 61 | } 62 | 63 | /** 64 | * Finds the project that contains the given file. The file's path prefix should match the 65 | * project's path. 66 | */ 67 | fun findContainingProject(filePath: String, logger: Logger? = null): ProjectPath? { 68 | val sections = filePath.split(File.separatorChar) 69 | logger?.info("finding containing project for $filePath , sections: $sections") 70 | return rootNode.find(sections, 0, logger) 71 | } 72 | 73 | fun getRootProjectPath(): ProjectPath? { 74 | return rootNode.projectPath 75 | } 76 | 77 | val allProjects by lazy { 78 | val result = mutableSetOf() 79 | rootNode.addAllProjectPaths(result) 80 | result 81 | } 82 | 83 | private class Node() : Serializable { 84 | var projectPath: ProjectPath? = null 85 | private val children = mutableMapOf() 86 | 87 | fun getOrCreateNode(key: String): Node { 88 | return children.getOrPut(key) { Node() } 89 | } 90 | 91 | fun find(sections: List, index: Int, logger: Logger?): ProjectPath? { 92 | if (sections.size <= index) { 93 | logger?.info("nothing") 94 | return projectPath 95 | } 96 | val child = children[sections[index]] 97 | return if (child == null) { 98 | logger?.info("no child found, returning ${projectPath ?: "root"}") 99 | projectPath 100 | } else { 101 | child.find(sections, index + 1, logger) 102 | } 103 | } 104 | 105 | fun addAllProjectPaths(collection: MutableSet) { 106 | projectPath?.let { path -> collection.add(path) } 107 | for (child in children.values) { 108 | child.addAllProjectPaths(collection) 109 | } 110 | } 111 | } 112 | } 113 | 114 | fun Project.getSupportRootFolder(): File = project.rootDir 115 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/CommitShaProvider.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.GitClient 4 | import com.dropbox.affectedmoduledetector.Sha 5 | import java.io.Serializable 6 | 7 | interface CommitShaProvider : Serializable { 8 | fun get(commandRunner: GitClient.CommandRunner): Sha 9 | } 10 | 11 | data class CommitShaProviderConfiguration( 12 | val type: String, 13 | val specifiedBranch: String? = null, 14 | val specifiedSha: String? = null, 15 | val top: Sha, 16 | val includeUncommitted: Boolean 17 | ) : Serializable 18 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/ForkCommit.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.GitClient 4 | import com.dropbox.affectedmoduledetector.Sha 5 | 6 | class ForkCommit : CommitShaProvider { 7 | override fun get(commandRunner: GitClient.CommandRunner): Sha { 8 | val currentBranch = commandRunner.executeAndParseFirst(CURRENT_BRANCH_CMD) 9 | 10 | val parentBranch = commandRunner.executeAndParse(SHOW_ALL_BRANCHES_CMD) 11 | .firstOrNull { !it.contains(currentBranch) && it.contains("*") } 12 | ?.substringAfter("[") 13 | ?.substringBefore("]") 14 | ?.substringBefore("~") 15 | ?.substringBefore("^") 16 | 17 | requireNotNull(parentBranch) { 18 | "Parent branch not found" 19 | } 20 | 21 | return commandRunner.executeAndParseFirst("git merge-base $currentBranch $parentBranch") 22 | } 23 | 24 | companion object { 25 | const val CURRENT_BRANCH_CMD = "git rev-parse --abbrev-ref HEAD" 26 | const val SHOW_ALL_BRANCHES_CMD = "git show-branch -a" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/PreviousCommit.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.GitClient 4 | import com.dropbox.affectedmoduledetector.Sha 5 | 6 | class PreviousCommit : CommitShaProvider { 7 | override fun get(commandRunner: GitClient.CommandRunner): Sha { 8 | return commandRunner.executeAndParseFirst(PREV_COMMIT_CMD) 9 | } 10 | companion object { 11 | const val PREV_COMMIT_CMD = "git --no-pager rev-parse HEAD~1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/SpecifiedBranchCommit.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.GitClient 4 | import com.dropbox.affectedmoduledetector.Sha 5 | 6 | class SpecifiedBranchCommit(private val branch: String) : CommitShaProvider { 7 | 8 | override fun get(commandRunner: GitClient.CommandRunner): Sha { 9 | return commandRunner.executeAndParseFirst("git rev-parse $branch") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/SpecifiedBranchCommitMergeBase.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.GitClient 4 | import com.dropbox.affectedmoduledetector.Sha 5 | 6 | class SpecifiedBranchCommitMergeBase(private val specifiedBranch: String) : CommitShaProvider { 7 | 8 | override fun get(commandRunner: GitClient.CommandRunner): Sha { 9 | val currentBranch = commandRunner.executeAndParseFirst(CURRENT_BRANCH_CMD) 10 | return commandRunner.executeAndParseFirst("git merge-base $currentBranch $specifiedBranch") 11 | } 12 | 13 | companion object { 14 | 15 | const val CURRENT_BRANCH_CMD = "git rev-parse --abbrev-ref HEAD" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/SpecifiedRawCommitSha.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.GitClient 4 | import com.dropbox.affectedmoduledetector.Sha 5 | 6 | class SpecifiedRawCommitSha(private val commitSha: String) : CommitShaProvider { 7 | override fun get(commandRunner: GitClient.CommandRunner): Sha { 8 | return commitSha 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/util/File.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.util 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Converts a [String] representation of a relative [File] path to sections based on the OS 7 | * specific separator character. 8 | */ 9 | fun String.toPathSections(rootProjectDir: File, gitRootDir: File): List { 10 | val realSections = toOsSpecificPath() 11 | .split(File.separatorChar) 12 | .toMutableList() 13 | val projectRelativeDirectorySections = rootProjectDir 14 | .toRelativeString(gitRootDir) 15 | .split(File.separatorChar) 16 | for (directorySection in projectRelativeDirectorySections) { 17 | if (realSections.isNotEmpty() && realSections.first() == directorySection) { 18 | realSections.removeAt(0) 19 | } else { 20 | break 21 | } 22 | } 23 | return realSections.toList() 24 | } 25 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/main/kotlin/com/dropbox/affectedmoduledetector/util/OsQuirks.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.util 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Returns an OS specific path respecting the separator character for the operating system. 7 | * 8 | * The Git client appears to only talk Unix-like paths however the Gradle client understands all 9 | * OS path variations. This causes issues on systems other than those that use the "/" path 10 | * character i.e. Windows. Therefore we need to normalise the path. 11 | */ 12 | fun String.toOsSpecificPath(): String { 13 | return this.split("/").joinToString(File.separator) 14 | } 15 | 16 | /** 17 | * Returns a String with an OS specific line endings for the operating system. 18 | * 19 | * The Git client appears to only talk Unix-like line endings ("\n") however the Gradle client 20 | * understands all OS line ending variants. This causes issues on systems other than those that 21 | * use Unix-like line endings i.e. Windows. Therefore we need to normalise the line endings. 22 | */ 23 | fun String.toOsSpecificLineEnding(): String { 24 | return this.replace("\n", System.lineSeparator()) 25 | } 26 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleConfigurationTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.Assert.assertFalse 5 | import org.junit.Assert.assertTrue 6 | import org.junit.Assert.fail 7 | import org.junit.Before 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.rules.TemporaryFolder 11 | import org.junit.runner.RunWith 12 | import org.junit.runners.JUnit4 13 | import java.io.File 14 | 15 | @RunWith(JUnit4::class) 16 | class AffectedModuleConfigurationTest { 17 | 18 | @get:Rule 19 | val tmpFolder = TemporaryFolder() 20 | 21 | private lateinit var config: AffectedModuleConfiguration 22 | 23 | private val FAKE_TASK = AffectedModuleConfiguration.CustomTask( 24 | commandByImpact = "runFakeTask", 25 | originalGradleCommand = "fakeOriginalGradleCommand", 26 | taskDescription = "Description of fake task" 27 | ) 28 | 29 | @Before 30 | fun setup() { 31 | config = AffectedModuleConfiguration() 32 | } 33 | 34 | @Test 35 | fun `GIVEN AffectedModuleConfiguration WHEN log folder THEN is null`() { 36 | // GIVEN 37 | // config 38 | 39 | // WHEN 40 | val logFolder = config.logFolder 41 | 42 | // THEN 43 | 44 | assertThat(logFolder).isNull() 45 | } 46 | 47 | @Test 48 | fun `GIVEN AffectedModuleConfiguration WHEN log folder is set THEN log folder is set`() { 49 | // GIVEN 50 | val sample = "sample" 51 | 52 | // WHEN 53 | config.logFolder = sample 54 | 55 | // THEN 56 | 57 | assertThat(config.logFolder).isEqualTo(sample) 58 | } 59 | 60 | @Test 61 | fun `GIVEN AffectedModuleConfiguration WHEN log file name THEN is default`() { 62 | // GIVEN 63 | // config 64 | 65 | // WHEN 66 | val logFilename = config.logFilename 67 | 68 | // THEN 69 | assertThat(logFilename).isEqualTo("affected_module_detector.log") 70 | } 71 | 72 | @Test 73 | fun `GIVEN AffectedModuleConfiguration WHEN log file name is set THEN log file name is updated`() { 74 | // GIVEN 75 | val sample = "sample" 76 | 77 | // WHEN 78 | config.logFilename = sample 79 | 80 | // THEN 81 | assertThat(config.logFilename).isEqualTo(sample) 82 | } 83 | 84 | @Test 85 | fun `GIVEN AffectedModuleConfiguration WHEN base dir is default THEN base dir is null`() { 86 | // GIVEN 87 | // config 88 | 89 | // WHEN 90 | val baseDir = config.baseDir 91 | 92 | // THEN 93 | 94 | assertThat(baseDir).isNull() 95 | } 96 | 97 | @Test 98 | fun `GIVEN AffectedModuleConfiguration WHEN base dir is set THEN new base dir is return`() { 99 | // GIVEN 100 | val sample = "sample" 101 | 102 | // WHEN 103 | config.baseDir = sample 104 | 105 | // THEN 106 | 107 | assertThat(config.baseDir).isEqualTo(sample) 108 | } 109 | 110 | @Test 111 | fun `GIVEN AffectedModuleConfiguration WHEN base dir is not set and paths affecting module is THEN throws an exception`() { 112 | // GIVEN 113 | val sample = setOf("sample") 114 | 115 | // WHEN 116 | try { 117 | config.pathsAffectingAllModules = sample 118 | } catch (e: IllegalArgumentException) { 119 | // THEN 120 | assertThat(e.message).isEqualTo("baseDir must be set to use pathsAffectingAllModules") 121 | return 122 | } 123 | 124 | fail("Expected to catch an exception") 125 | } 126 | 127 | @Test 128 | fun `GIVEN AffectedModuleConfiguration WHEN base dir is set and paths affecting module is set THEN succeeds`() { 129 | // GIVEN 130 | val sampleFileName = "sample.txt" 131 | config.baseDir = tmpFolder.root.absolutePath 132 | val sample = File(tmpFolder.root, sampleFileName) 133 | sample.createNewFile() 134 | 135 | // WHEN 136 | config.pathsAffectingAllModules = setOf(sampleFileName) 137 | 138 | // THEN 139 | assertThat(config.pathsAffectingAllModules).isEqualTo(setOf(sampleFileName)) 140 | } 141 | 142 | @Test 143 | fun `GIVEN AffectedModuleConfiguration WHEN base dir is set and paths affecting module invalid file THEN throws exception`() { 144 | // GIVEN 145 | val sampleFileName = "sample.txt" 146 | config.baseDir = tmpFolder.root.absolutePath 147 | 148 | // WHEN 149 | config.pathsAffectingAllModules = setOf(sampleFileName) 150 | 151 | // THEN 152 | try { 153 | config.pathsAffectingAllModules 154 | } catch (e: IllegalArgumentException) { 155 | // THEN 156 | assertThat(e.message).startsWith("Could not find expected path in pathsAffectingAllModules:") 157 | return 158 | } 159 | 160 | fail("Expected to catch an exception") 161 | } 162 | 163 | @Test 164 | fun `GIVEN AffectedModuleConfiguration WHEN companion object name is returned THEN affectedModuleDetector is returned`() { 165 | assertThat(AffectedModuleConfiguration.name).isEqualTo("affectedModuleDetector") 166 | } 167 | 168 | @Test 169 | fun `GIVEN AffectedModuleConfiguration WHEN compareFrom THEN is PreviousCommit`() { 170 | val actual = config.compareFrom 171 | 172 | assertThat(actual).isEqualTo("PreviousCommit") 173 | } 174 | 175 | @Test 176 | fun `WHEN compareFrom is set to SpecifiedBranchCommitMergeBase AND specifiedBranch is set THEN return SpecifiedBranchCommitMergeBase`() { 177 | val specifiedBranchCommitMergeBase = "SpecifiedBranchCommitMergeBase" 178 | val specifiedBranch = "origin/dev" 179 | 180 | config.specifiedBranch = specifiedBranch 181 | config.compareFrom = specifiedBranchCommitMergeBase 182 | 183 | val actual = config.compareFrom 184 | 185 | assertThat(actual).isEqualTo(specifiedBranchCommitMergeBase) 186 | } 187 | 188 | @Test 189 | fun `WHEN compareFrom is set to SpecifiedBranchCommitMergeBase AND specifiedBranch isn't set THEN throw exception`() { 190 | val specifiedBranchCommitMergeBase = "SpecifiedBranchCommitMergeBase" 191 | 192 | try { 193 | config.compareFrom = specifiedBranchCommitMergeBase 194 | } catch (e: IllegalArgumentException) { 195 | assertThat(e.message).isEqualTo("Specify a branch using the configuration specifiedBranch") 196 | } 197 | } 198 | 199 | @Test 200 | fun `GIVEN AffectedModuleConfiguration WHEN compareFrom is set to ForkCommit THEN is ForkCommit`() { 201 | val forkCommit = "ForkCommit" 202 | 203 | config.compareFrom = forkCommit 204 | 205 | val actual = config.compareFrom 206 | assertThat(actual).isEqualTo(forkCommit) 207 | } 208 | 209 | @Test 210 | fun `GIVEN AffectedModuleConfiguration WHEN compareFrom is set to SpecifiedBranchCommit THEN is SpecifiedBranchCommit`() { 211 | val specifiedBranchCommit = "SpecifiedBranchCommit" 212 | val specifiedBranch = "myBranch" 213 | 214 | config.specifiedBranch = specifiedBranch 215 | config.compareFrom = specifiedBranchCommit 216 | 217 | val actual = config.compareFrom 218 | assertThat(actual).isEqualTo(specifiedBranchCommit) 219 | } 220 | 221 | @Test 222 | fun `GIVEN AffectedModuleConfiguration WHEN compareFrom is set to SpecifiedBranchCommit AND specifiedBranch not defined THEN error thrown`() { 223 | val specifiedBranchCommit = "SpecifiedBranchCommit" 224 | 225 | try { 226 | config.compareFrom = specifiedBranchCommit 227 | } catch (e: IllegalArgumentException) { 228 | // THEN 229 | assertThat(e.message).isEqualTo("Specify a branch using the configuration specifiedBranch") 230 | return 231 | } 232 | 233 | fail("Expected to catch an exception") 234 | } 235 | 236 | @Test 237 | fun `GIVEN AffectedModuleConfiguration WHEN compareFrom is set to SpecifiedRawCommitSha THEN is SpecifiedRawCommitSha`() { 238 | val specifiedRawCommitSha = "SpecifiedRawCommitSha" 239 | val commitSha = "12345" 240 | 241 | config.specifiedRawCommitSha = commitSha 242 | config.compareFrom = specifiedRawCommitSha 243 | 244 | val actual = config.compareFrom 245 | assertThat(actual).isEqualTo(specifiedRawCommitSha) 246 | } 247 | 248 | @Test 249 | fun `GIVEN AffectedModuleConfiguration WHEN compareFrom is set to SpecifiedRawCommitSha AND specifiedRawCommitSha not defined THEN error thrown`() { 250 | val specifiedRawCommitSha = "SpecifiedRawCommitSha" 251 | 252 | try { 253 | config.compareFrom = specifiedRawCommitSha 254 | } catch (e: IllegalArgumentException) { 255 | // THEN 256 | assertThat(e.message).isEqualTo("Provide a Commit SHA for the specifiedRawCommitSha property when using SpecifiedRawCommitSha comparison strategy.") 257 | return 258 | } 259 | } 260 | 261 | @Test 262 | fun `GIVEN AffectedModuleConfiguration WHEN compareFrom is set to invalid sha provider THEN exception thrown and value not set`() { 263 | try { 264 | config.compareFrom = "InvalidInput" 265 | fail() 266 | } catch (e: Exception) { 267 | assertThat(e::class).isEqualTo(IllegalArgumentException::class) 268 | assertThat(e.message).isEqualTo("The property configuration compareFrom must be one of the following: PreviousCommit, ForkCommit, SpecifiedBranchCommit, SpecifiedBranchCommitMergeBase, SpecifiedRawCommitSha") 269 | assertThat(config.compareFrom).isEqualTo("PreviousCommit") 270 | } 271 | } 272 | 273 | @Test 274 | fun `GIVEN AffectedModuleConfiguration WHEN top THEN is HEAD`() { 275 | val actual = config.top 276 | 277 | assertThat(actual).isEqualTo("HEAD") 278 | } 279 | 280 | @Test 281 | fun `GIVEN AffectedModuleConfiguration WHEN includeUncommitted is true top is set to sha THEN exception thrown and value not set`() { 282 | val includeUncommitted = true 283 | val sha = "12345" 284 | 285 | try { 286 | config.includeUncommitted = includeUncommitted 287 | config.top = sha 288 | } catch (e: IllegalArgumentException) { 289 | // THEN 290 | assertThat(e.message).isEqualTo("Set includeUncommitted to false to set a custom top") 291 | return 292 | } 293 | 294 | fail("Expected to catch an exception") 295 | } 296 | 297 | @Test 298 | fun `GIVEN AffectedModuleConfiguration WHEN includeUncommitted is false and top is set to sha THEN top is sha`() { 299 | val includeUncommitted = false 300 | val sha = "12345" 301 | 302 | config.includeUncommitted = includeUncommitted 303 | config.top = sha 304 | 305 | val actual = config.top 306 | assertThat(actual).isEqualTo(sha) 307 | } 308 | 309 | @Test 310 | fun `GIVEN AffectedModuleConfiguration WHEN includeUncommitted THEN is true`() { 311 | val actual = config.includeUncommitted 312 | 313 | assertThat(actual).isTrue() 314 | } 315 | 316 | @Test 317 | fun `GIVEN AffectedModuleConfiguration WHEN customTasks THEN is empty`() { 318 | val actual = config.customTasks 319 | 320 | assertThat(actual).isEmpty() 321 | } 322 | 323 | @Test 324 | fun `GIVEN AffectedModuleConfiguration WHEN customTasks contains task THEN is not empty`() { 325 | config.customTasks = setOf(FAKE_TASK) 326 | val actual = config.customTasks 327 | 328 | assertThat(actual).contains(FAKE_TASK) 329 | } 330 | 331 | @Test 332 | fun `GIVEN AffectedModuleConfiguration WHEN customTasks contains task THEN task contains commandByImpact field`() { 333 | config.customTasks = setOf(FAKE_TASK) 334 | val actual = config.customTasks 335 | 336 | assert(actual.first().commandByImpact == "runFakeTask") 337 | } 338 | 339 | @Test 340 | fun `GIVEN AffectedModuleConfiguration WHEN customTasks contains task THEN task contains originalGradleCommand field`() { 341 | config.customTasks = setOf(FAKE_TASK) 342 | val actual = config.customTasks 343 | 344 | assert(actual.first().originalGradleCommand == "fakeOriginalGradleCommand") 345 | } 346 | 347 | @Test 348 | fun `GIVEN AffectedModuleConfiguration WHEN customTasks contains task THEN task contains taskDescription field`() { 349 | config.customTasks = setOf(FAKE_TASK) 350 | val actual = config.customTasks 351 | 352 | assert(actual.first().taskDescription == "Description of fake task") 353 | } 354 | 355 | @Test 356 | fun `GIVEN AffectedModuleConfiguration WHEN buildAllWhenNoProjectsChanged THEN then default value is true`() { 357 | // GIVEN 358 | // config 359 | 360 | // WHEN 361 | val buildAllWhenNoProjectsChanged = config.buildAllWhenNoProjectsChanged 362 | 363 | // THEN 364 | assertTrue(buildAllWhenNoProjectsChanged) 365 | } 366 | 367 | @Test 368 | fun `GIVEN AffectedModuleConfiguration WHEN buildAllWhenNoProjectsChanged is set to false THEN then value is false`() { 369 | // GIVEN 370 | val buildAll = false 371 | config.buildAllWhenNoProjectsChanged = buildAll 372 | 373 | // WHEN 374 | val actual = config.buildAllWhenNoProjectsChanged 375 | 376 | // THEN 377 | assertFalse(actual) 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import com.dropbox.affectedmoduledetector.rules.SetupAndroidProject 4 | import com.google.common.truth.Truth.assertThat 5 | import org.gradle.testkit.runner.GradleRunner 6 | import org.junit.Rule 7 | import org.junit.Test 8 | 9 | class AffectedModuleDetectorIntegrationTest { 10 | 11 | @Rule 12 | @JvmField 13 | val tmpFolder = SetupAndroidProject() 14 | 15 | @Test 16 | fun `GIVEN single project WHEN plugin is applied THEN tasks are added`() { 17 | // GIVEN 18 | // expected tasks 19 | val tasks = listOf( 20 | "runAffectedUnitTests", 21 | "runAffectedAndroidTests", 22 | "assembleAffectedAndroidTests" 23 | ) 24 | tmpFolder.newFile("build.gradle").writeText( 25 | """plugins { 26 | | id "com.dropbox.affectedmoduledetector" 27 | |}""".trimMargin() 28 | ) 29 | 30 | // WHEN 31 | val result = GradleRunner.create() 32 | .withProjectDir(tmpFolder.root) 33 | .withPluginClasspath() 34 | .withArguments("tasks") 35 | .build() 36 | 37 | // THEN 38 | tasks.forEach { taskName -> 39 | assertThat(result.output).contains(taskName) 40 | } 41 | } 42 | 43 | @Test 44 | fun `GIVEN multiple project WHEN plugin is applied THEN tasks has dependencies`() { 45 | // GIVEN 46 | tmpFolder.setupAndroidSdkLocation() 47 | tmpFolder.newFolder("sample-app") 48 | tmpFolder.newFolder("sample-core") 49 | tmpFolder.newFile("settings.gradle").writeText( 50 | """ 51 | |include ':sample-app' 52 | |include ':sample-core' 53 | """.trimMargin() 54 | ) 55 | 56 | tmpFolder.newFile("build.gradle").writeText( 57 | """buildscript { 58 | | repositories { 59 | | google() 60 | | jcenter() 61 | | } 62 | | dependencies { 63 | | classpath "com.android.tools.build:gradle:7.4.0" 64 | | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25" 65 | | } 66 | |} 67 | |plugins { 68 | | id "com.dropbox.affectedmoduledetector" 69 | |} 70 | |allprojects { 71 | | repositories { 72 | | google() 73 | | jcenter() 74 | | } 75 | |}""".trimMargin() 76 | ) 77 | 78 | tmpFolder.newFile("sample-app/build.gradle").writeText( 79 | """plugins { 80 | | id 'com.android.application' 81 | | id 'kotlin-android' 82 | | } 83 | | android { 84 | | compileSdkVersion 33 85 | | namespace "sample" 86 | | } 87 | | dependencies { 88 | | implementation project(":sample-core") 89 | | }""".trimMargin() 90 | ) 91 | 92 | tmpFolder.newFolder("sample-app/src/main/") 93 | tmpFolder.newFile("sample-app/src/main/AndroidManifest.xml").writeText( 94 | """ 95 | | 96 | | 97 | """.trimMargin() 98 | ) 99 | 100 | tmpFolder.newFile("sample-core/build.gradle").writeText( 101 | """plugins { 102 | | id 'com.android.library' 103 | | id 'kotlin-android' 104 | | } 105 | | affectedTestConfiguration { 106 | | assembleAndroidTestTask = "assembleAndroidTest" 107 | | } 108 | | android { 109 | | namespace 'sample.core' 110 | | compileSdkVersion 33 111 | | }""".trimMargin() 112 | ) 113 | 114 | tmpFolder.newFolder("sample-core/src/main/") 115 | tmpFolder.newFile("sample-core/src/main/AndroidManifest.xml").writeText( 116 | """ 117 | | 118 | | 119 | """.trimMargin() 120 | ) 121 | 122 | // WHEN 123 | val result = GradleRunner.create() 124 | .withProjectDir(tmpFolder.root) 125 | .withPluginClasspath() 126 | .withArguments("assembleAffectedAndroidTests", "--dry-run") 127 | .build() 128 | 129 | // THEN 130 | assertThat(result.output).contains(":sample-app:assembleDebugAndroidTest SKIPPED") 131 | assertThat(result.output).contains(":sample-core:mergeDexDebugAndroidTest SKIPPED") 132 | assertThat(result.output).contains(":sample-core:packageDebugAndroidTest SKIPPED") 133 | assertThat(result.output).contains(":sample-core:assembleDebugAndroidTest SKIPPED") 134 | assertThat(result.output).contains(":sample-core:assembleAndroidTest SKIPPED") 135 | assertThat(result.output).contains(":assembleAffectedAndroidTests SKIPPED") 136 | } 137 | 138 | @Test 139 | fun `GIVEN multiple project with one excluded WHEN plugin is applied THEN tasks has dependencies minus the exclusions`() { 140 | // GIVEN 141 | tmpFolder.newFolder("sample-app") 142 | tmpFolder.newFolder("sample-core") 143 | tmpFolder.newFile("settings.gradle").writeText( 144 | """ 145 | |include ':sample-app' 146 | |include ':sample-core' 147 | """.trimMargin() 148 | ) 149 | 150 | tmpFolder.newFile("build.gradle").writeText( 151 | """buildscript { 152 | | repositories { 153 | | google() 154 | | jcenter() 155 | | } 156 | | dependencies { 157 | | classpath "com.android.tools.build:gradle:7.4.0" 158 | | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25" 159 | | } 160 | |} 161 | |plugins { 162 | | id "com.dropbox.affectedmoduledetector" 163 | |} 164 | |affectedModuleDetector { 165 | | excludedModules = [ "sample-core" ] 166 | |} 167 | |allprojects { 168 | | repositories { 169 | | google() 170 | | jcenter() 171 | | } 172 | |}""".trimMargin() 173 | ) 174 | 175 | tmpFolder.newFile("sample-app/build.gradle").writeText( 176 | """plugins { 177 | | id 'com.android.application' 178 | | id 'kotlin-android' 179 | | } 180 | | android { 181 | | namespace 'sample' 182 | | compileSdkVersion 33 183 | | } 184 | | dependencies { 185 | | implementation project(":sample-core") 186 | | }""".trimMargin() 187 | ) 188 | 189 | tmpFolder.newFolder("sample-app/src/main/") 190 | tmpFolder.newFile("sample-app/src/main/AndroidManifest.xml").writeText( 191 | """ 192 | | 193 | | 194 | """.trimMargin() 195 | ) 196 | 197 | tmpFolder.newFile("sample-core/build.gradle").writeText( 198 | """plugins { 199 | | id 'com.android.library' 200 | | id 'kotlin-android' 201 | | } 202 | | affectedTestConfiguration { 203 | | assembleAndroidTestTask = "assembleAndroidTest" 204 | | } 205 | | android { 206 | | namespace 'sample.core' 207 | | compileSdkVersion 33 208 | | }""".trimMargin() 209 | ) 210 | 211 | tmpFolder.newFolder("sample-core/src/main/") 212 | tmpFolder.newFile("sample-core/src/main/AndroidManifest.xml").writeText( 213 | """ 214 | | 215 | | 216 | """.trimMargin() 217 | ) 218 | 219 | // WHEN 220 | val result = GradleRunner.create() 221 | .withProjectDir(tmpFolder.root) 222 | .withPluginClasspath() 223 | .withArguments("assembleAffectedAndroidTests", "--dry-run") 224 | .build() 225 | 226 | // THEN 227 | assertThat(result.output).contains(":sample-app:assembleDebugAndroidTest SKIPPED") 228 | assertThat(result.output).doesNotContain(":sample-core:mergeDexDebugAndroidTest") 229 | assertThat(result.output).doesNotContain(":sample-core:packageDebugAndroidTest") 230 | assertThat(result.output).doesNotContain(":sample-core:assembleDebugAndroidTest") 231 | assertThat(result.output).doesNotContain(":sample-core:assembleAndroidTest") 232 | assertThat(result.output).contains(":assembleAffectedAndroidTests SKIPPED") 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedModuleDetectorPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.gradle.api.Project 5 | import org.gradle.api.internal.plugins.PluginApplicationException 6 | import org.gradle.testfixtures.ProjectBuilder 7 | import org.junit.Before 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.rules.TemporaryFolder 11 | import org.junit.runner.RunWith 12 | import org.junit.runners.JUnit4 13 | import java.lang.IllegalStateException 14 | 15 | @RunWith(JUnit4::class) 16 | class AffectedModuleDetectorPluginTest { 17 | 18 | private companion object { 19 | 20 | const val FAKE_COMMAND_BY_IMPACT = "fake_command" 21 | const val FAKE_ORIGINAL_COMMAND = "fake_original_gradle_command" 22 | const val FAKE_TASK_DESCRIPTION = "fake_description" 23 | } 24 | 25 | private val fakeTask = AffectedModuleConfiguration.CustomTask( 26 | commandByImpact = FAKE_COMMAND_BY_IMPACT, 27 | originalGradleCommand = FAKE_ORIGINAL_COMMAND, 28 | taskDescription = FAKE_TASK_DESCRIPTION 29 | ) 30 | 31 | @Rule 32 | @JvmField 33 | val tmpFolder = TemporaryFolder() 34 | 35 | lateinit var rootProject: Project 36 | lateinit var childProject: Project 37 | 38 | @Before 39 | fun setup() { 40 | rootProject = ProjectBuilder.builder() 41 | .withName("root") 42 | .withProjectDir(tmpFolder.root) 43 | .build() 44 | 45 | childProject = ProjectBuilder.builder() 46 | .withName("child") 47 | .withParent(rootProject) 48 | .build() 49 | } 50 | 51 | @Test 52 | fun `GIVEN child project WHEN plugin is applied THEN throw exception`() { 53 | // GIVEN 54 | rootProject.pluginManager.apply(AffectedModuleDetectorPlugin::class.java) 55 | 56 | try { 57 | // WHEN 58 | childProject.pluginManager.apply(AffectedModuleDetectorPlugin::class.java) 59 | throw IllegalStateException("Expected to throw exception") 60 | } catch (e: PluginApplicationException) { 61 | // THEN 62 | assertThat(e.message).isEqualTo("Failed to apply plugin class 'com.dropbox.affectedmoduledetector.AffectedModuleDetectorPlugin'.") 63 | } 64 | } 65 | 66 | @Test 67 | fun `GIVEN root project WHEN plugin is applied THEN extensions are added`() { 68 | // GIVEN 69 | // root project 70 | 71 | // WHEN 72 | rootProject.pluginManager.apply(AffectedModuleDetectorPlugin::class.java) 73 | val extension = rootProject.extensions.findByName("affectedModuleDetector") 74 | 75 | // THEN 76 | assertThat(extension).isNotNull() 77 | assertThat(extension).isInstanceOf(AffectedModuleConfiguration::class.java) 78 | } 79 | 80 | @Test 81 | fun `GIVEN affected module detector plugin WHEN register task is called THEN task is added`() { 82 | // GIVEN 83 | val task = fakeTask 84 | val plugin = AffectedModuleDetectorPlugin() 85 | rootProject.pluginManager.apply(AffectedModuleDetectorPlugin::class.java) 86 | 87 | // WHEN 88 | plugin.registerInternalTask( 89 | rootProject = rootProject, 90 | taskType = task, 91 | groupName = "fakeGroup" 92 | ) 93 | val result = rootProject.tasks.findByPath(task.commandByImpact) 94 | 95 | // THEN 96 | assertThat(result).isNotNull() 97 | assertThat(result?.name).isEqualTo(fakeTask.commandByImpact) 98 | assertThat(result?.group).isEqualTo("fakeGroup") 99 | assertThat(result?.description).isEqualTo(fakeTask.taskDescription) 100 | } 101 | 102 | @Test 103 | fun `GIVEN affected module detector plugin WHEN register_custom_task is called AND AffectedModuleConfiguration customTask is not empty THEN task is added`() { 104 | // GIVEN 105 | val configuration = AffectedModuleConfiguration() 106 | configuration.customTasks = setOf(fakeTask) 107 | rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) 108 | 109 | val plugin = AffectedModuleDetectorPlugin() 110 | 111 | // WHEN 112 | plugin.registerCustomTasks(rootProject, setOf(fakeTask)) 113 | val result = rootProject.tasks.findByPath(fakeTask.commandByImpact) 114 | 115 | // THEN 116 | assertThat(result).isNotNull() 117 | assertThat(result?.name).isEqualTo(fakeTask.commandByImpact) 118 | assertThat(result?.group).isEqualTo(AffectedModuleDetectorPlugin.CUSTOM_TASK_GROUP_NAME) 119 | assertThat(result?.description).isEqualTo(fakeTask.taskDescription) 120 | } 121 | 122 | @Test 123 | fun `GIVEN affected module detector plugin WHEN registerCustomTasks is called AND AffectedModuleConfiguration customTask is empty THEN task isn't added`() { 124 | // GIVEN 125 | val configuration = AffectedModuleConfiguration() 126 | rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) 127 | val plugin = AffectedModuleDetectorPlugin() 128 | 129 | // WHEN 130 | plugin.registerCustomTasks(rootProject, emptySet()) 131 | val result = rootProject 132 | .tasks 133 | .filter { it.group == AffectedModuleDetectorPlugin.CUSTOM_TASK_GROUP_NAME } 134 | 135 | // THEN 136 | assertThat(result).isEmpty() 137 | } 138 | 139 | @Test 140 | fun `GIVEN AffectedModuleDetectorPlugin WHEN CUSTOM_TASK_GROUP_NAME compared with TEST_TASK_GROUP_NAME THEN they isn't equal`() { 141 | assert(AffectedModuleDetectorPlugin.CUSTOM_TASK_GROUP_NAME != AffectedModuleDetectorPlugin.TEST_TASK_GROUP_NAME) 142 | } 143 | 144 | @Test 145 | fun `GIVEN affected module detector plugin WHEN registerTestTasks THEN task all task added`() { 146 | // GIVEN 147 | val configuration = AffectedModuleConfiguration() 148 | rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) 149 | val plugin = AffectedModuleDetectorPlugin() 150 | 151 | // WHEN 152 | plugin.registerTestTasks(rootProject) 153 | val androidTestTask = rootProject.tasks.findByPath(InternalTaskType.ANDROID_TEST.commandByImpact) 154 | val assembleAndroidTestTask = rootProject.tasks.findByPath(InternalTaskType.ASSEMBLE_ANDROID_TEST.commandByImpact) 155 | val jvmTestTask = rootProject.tasks.findByPath(InternalTaskType.ANDROID_JVM_TEST.commandByImpact) 156 | 157 | // THEN 158 | assertThat(androidTestTask).isNotNull() 159 | assertThat(androidTestTask?.group).isEqualTo(AffectedModuleDetectorPlugin.TEST_TASK_GROUP_NAME) 160 | 161 | assertThat(assembleAndroidTestTask).isNotNull() 162 | assertThat(assembleAndroidTestTask?.group).isEqualTo(AffectedModuleDetectorPlugin.TEST_TASK_GROUP_NAME) 163 | 164 | assertThat(jvmTestTask).isNotNull() 165 | assertThat(jvmTestTask?.group).isEqualTo(AffectedModuleDetectorPlugin.TEST_TASK_GROUP_NAME) 166 | } 167 | 168 | @Test 169 | fun `GIVEN affected module detector plugin WHEN registerTestTasks called THEN added all tasks from InternalTaskType`() { 170 | // GIVEN 171 | val configuration = AffectedModuleConfiguration() 172 | rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) 173 | val plugin = AffectedModuleDetectorPlugin() 174 | val availableTaskVariants = 3 // runAffectedAndroidTests, assembleAffectedAndroidTests and runAffectedUnitTests 175 | 176 | // WHEN 177 | plugin.registerTestTasks(rootProject) 178 | val testTasks = rootProject 179 | .tasks 180 | .filter { it.group == AffectedModuleDetectorPlugin.TEST_TASK_GROUP_NAME } 181 | 182 | // THEN 183 | assert(testTasks.size == availableTaskVariants) 184 | } 185 | 186 | @Test 187 | fun `GIVEN affected module detector plugin WHEN registerCustomTasks called THEN added all tasks from FakeTaskType`() { 188 | // GIVEN 189 | val givenCustomTasks = setOf(fakeTask, fakeTask.copy(commandByImpact = "otherCommand")) 190 | val configuration = AffectedModuleConfiguration() 191 | configuration.customTasks = givenCustomTasks 192 | rootProject.extensions.add(AffectedModuleConfiguration.name, configuration) 193 | val plugin = AffectedModuleDetectorPlugin() 194 | 195 | // WHEN 196 | plugin.registerCustomTasks(rootProject, givenCustomTasks) 197 | 198 | val customTasks = rootProject 199 | .tasks 200 | .filter { it.group == AffectedModuleDetectorPlugin.CUSTOM_TASK_GROUP_NAME } 201 | 202 | // THEN 203 | assert(customTasks.size == 2) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AffectedTestConfigurationTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.Before 5 | import org.junit.Test 6 | 7 | class AffectedTestConfigurationTest { 8 | 9 | private lateinit var config: AffectedTestConfiguration 10 | 11 | @Before 12 | fun setup() { 13 | config = AffectedTestConfiguration() 14 | } 15 | 16 | @Test 17 | fun `GIVEN AffectedTestConfiguration WHEN default values THEN default values returned`() { 18 | assertThat(config.assembleAndroidTestTask).isEqualTo("assembleDebugAndroidTest") 19 | assertThat(config.runAndroidTestTask).isEqualTo("connectedDebugAndroidTest") 20 | assertThat(config.jvmTestTask).isEqualTo("testDebugUnitTest") 21 | } 22 | 23 | @Test 24 | fun `GIVEN AffectedTestConfiguration WHEN values are updated THEN new values are returned`() { 25 | // GIVEN 26 | val assembleAndroidTestTask = "assembleAndroidTestTask" 27 | val runAndroidTestTask = "runAndroidTestTask" 28 | val jvmTest = "jvmTest" 29 | 30 | // WHEN 31 | config.assembleAndroidTestTask = assembleAndroidTestTask 32 | config.runAndroidTestTask = runAndroidTestTask 33 | config.jvmTestTask = jvmTest 34 | 35 | // THEN 36 | assertThat(config.assembleAndroidTestTask).isEqualTo(assembleAndroidTestTask) 37 | assertThat(config.runAndroidTestTask).isEqualTo(runAndroidTestTask) 38 | assertThat(config.jvmTestTask).isEqualTo(jvmTest) 39 | } 40 | 41 | @Test 42 | fun `GIVEN AffectedTestConfiguration WHEN companion object name is called THEN affectedTestConfiguration is returned`() { 43 | assertThat(AffectedTestConfiguration.name).isEqualTo("affectedTestConfiguration") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/AttachLogsTestRule.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import org.junit.rules.TestRule 4 | import org.junit.runner.Description 5 | import org.junit.runners.model.Statement 6 | import java.io.File 7 | 8 | /** 9 | * Special rule for dependency detector tests that will attach logs to a failure. 10 | */ 11 | class AttachLogsTestRule() : TestRule { 12 | 13 | private val file: File = File.createTempFile("test", "log") 14 | 15 | internal val logger by lazy { FileLogger(file) } 16 | override fun apply(base: Statement, description: Description): Statement { 17 | return object : Statement() { 18 | override fun evaluate() { 19 | try { 20 | file.deleteOnExit() 21 | base.evaluate() 22 | } catch (t: Throwable) { 23 | val bufferedReader = file.bufferedReader() 24 | val logs = bufferedReader.use { it.readText() } 25 | throw Exception( 26 | """ 27 | test failed with msg: ${t.message} 28 | logs: 29 | $logs 30 | """.trimIndent(), 31 | t 32 | ) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/InternalTaskTypeTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import org.junit.Test 4 | import org.junit.runner.RunWith 5 | import org.junit.runners.JUnit4 6 | 7 | @RunWith(JUnit4::class) 8 | class InternalTaskTypeTest { 9 | 10 | @Test 11 | fun `GIVEN InternalTaskType WHEN JVM_TEST WHEN THEN originalGradleCommand is "test"`() { 12 | // GIVEN 13 | val nonAndroidTestTask = InternalTaskType.JVM_TEST.originalGradleCommand 14 | val nonAndroidTestCommand = "test" 15 | 16 | assert(nonAndroidTestTask == nonAndroidTestCommand) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/ProjectGraphTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector 2 | 3 | import junit.framework.TestCase.assertEquals 4 | import junit.framework.TestCase.assertNull 5 | import org.gradle.api.plugins.ExtraPropertiesExtension 6 | import org.gradle.testfixtures.ProjectBuilder 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.junit.rules.TemporaryFolder 10 | import org.junit.runner.RunWith 11 | import org.junit.runners.JUnit4 12 | import java.io.File 13 | 14 | @RunWith(JUnit4::class) 15 | class ProjectGraphTest { 16 | @Rule 17 | @JvmField 18 | val tmpFolder = TemporaryFolder() 19 | 20 | @Test 21 | fun testSimple() { 22 | val tmpDir = tmpFolder.root 23 | val root = ProjectBuilder.builder() 24 | .withProjectDir(tmpDir) 25 | .withName("root") 26 | .build() 27 | // Project Graph expects supportRootFolder. 28 | (root.properties.get("ext") as ExtraPropertiesExtension).set("supportRootFolder", tmpDir) 29 | val p1 = ProjectBuilder.builder() 30 | .withProjectDir(tmpDir.resolve("p1")) 31 | .withName("p1") 32 | .withParent(root) 33 | .build() 34 | val p2 = ProjectBuilder.builder() 35 | .withProjectDir(tmpDir.resolve("p2")) 36 | .withName("p2") 37 | .withParent(root) 38 | .build() 39 | val p3 = ProjectBuilder.builder() 40 | .withProjectDir(tmpDir.resolve("p1").resolve("p3")) 41 | .withName("p3") 42 | .withParent(p1) 43 | .build() 44 | val logFile = File.createTempFile(tmpDir.path, "log") 45 | val graph = ProjectGraph(root, FileLogger(logFile).toLogger()) 46 | assertNull(graph.findContainingProject("nowhere")) 47 | assertNull(graph.findContainingProject("rootfile.java")) 48 | assertEquals( 49 | p1.projectPath, 50 | graph.findContainingProject("p1/px/x.java".toLocalPath()) 51 | ) 52 | assertEquals( 53 | p1.projectPath, 54 | graph.findContainingProject("p1/a.java".toLocalPath()) 55 | ) 56 | assertEquals( 57 | p3.projectPath, 58 | graph.findContainingProject("p1/p3/a.java".toLocalPath()) 59 | ) 60 | assertEquals( 61 | p2.projectPath, 62 | graph.findContainingProject("p2/a/b/c/d/e/f/a.java".toLocalPath()) 63 | ) 64 | } 65 | private fun String.toLocalPath() = this.split("/").joinToString(File.separator) 66 | } 67 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/ForkCommitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.AttachLogsTestRule 4 | import com.dropbox.affectedmoduledetector.mocks.MockCommandRunner 5 | import com.google.common.truth.Truth.assertThat 6 | import org.junit.Assert.fail 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import org.junit.runners.JUnit4 11 | 12 | @RunWith(JUnit4::class) 13 | class ForkCommitTest { 14 | @Rule 15 | @JvmField 16 | val attachLogsRule = AttachLogsTestRule() 17 | private val logger = attachLogsRule.logger 18 | private val commandRunner = MockCommandRunner(logger) 19 | private val forkCommit = ForkCommit() 20 | 21 | @Test 22 | fun whenCURRENT_BRANCH_CMD_thenCommandReturned() { 23 | assertThat(ForkCommit.CURRENT_BRANCH_CMD).isEqualTo("git rev-parse --abbrev-ref HEAD") 24 | } 25 | 26 | @Test 27 | fun whenSHOW_ALL_BRANCHES_CMD_thenCommandReturned() { 28 | assertThat(ForkCommit.SHOW_ALL_BRANCHES_CMD).isEqualTo("git show-branch -a") 29 | } 30 | 31 | @Test 32 | fun givenNoParentBranchThatIsNotCurrentBranch_whenGetCommitSha_thenThrowException() { 33 | try { 34 | commandRunner.addReply(ForkCommit.CURRENT_BRANCH_CMD, "main") 35 | val parentBranches = listOf("[main]") 36 | commandRunner.addReply( 37 | ForkCommit.SHOW_ALL_BRANCHES_CMD, 38 | parentBranches.joinToString(System.lineSeparator()) 39 | ) 40 | 41 | forkCommit.get(commandRunner) 42 | fail() 43 | } catch (e: Exception) { 44 | assertThat(e::class).isEqualTo(IllegalArgumentException::class) 45 | assertThat(e.message).isEqualTo("Parent branch not found") 46 | } 47 | } 48 | 49 | @Test 50 | fun givenNoParentBranchThatContainsAsterisk_whenGetCommitSha_thenThrowException() { 51 | try { 52 | commandRunner.addReply(ForkCommit.CURRENT_BRANCH_CMD, "feature") 53 | val parentBranches = listOf("[main]") 54 | commandRunner.addReply( 55 | ForkCommit.SHOW_ALL_BRANCHES_CMD, 56 | parentBranches.joinToString(System.lineSeparator()) 57 | ) 58 | 59 | forkCommit.get(commandRunner) 60 | fail() 61 | } catch (e: Exception) { 62 | assertThat(e::class).isEqualTo(IllegalArgumentException::class) 63 | assertThat(e.message).isEqualTo("Parent branch not found") 64 | } 65 | } 66 | 67 | @Test 68 | fun givenParentBranchWithSymbols_whenGetCommitSha_thenReturnForkCommitSha() { 69 | commandRunner.addReply(ForkCommit.CURRENT_BRANCH_CMD, "feature") 70 | val parentBranches = listOf("* [main^~SNAPSHOT_01]") 71 | commandRunner.addReply(ForkCommit.SHOW_ALL_BRANCHES_CMD, parentBranches.joinToString(System.lineSeparator())) 72 | commandRunner.addReply("git merge-base feature main", "commit-sha") 73 | 74 | val actual = forkCommit.get(commandRunner) 75 | 76 | assertThat(actual).isEqualTo("commit-sha") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/PreviousCommitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.AttachLogsTestRule 4 | import com.dropbox.affectedmoduledetector.mocks.MockCommandRunner 5 | import com.google.common.truth.Truth.assertThat 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.JUnit4 10 | 11 | @RunWith(JUnit4::class) 12 | class PreviousCommitTest { 13 | @Rule 14 | @JvmField 15 | val attachLogsRule = AttachLogsTestRule() 16 | private val logger = attachLogsRule.logger 17 | private val commandRunner = MockCommandRunner(logger) 18 | private val previousCommit = PreviousCommit() 19 | 20 | @Test 21 | fun whenPREV_COMMIT_CMD_thenCommandReturned() { 22 | assertThat(PreviousCommit.PREV_COMMIT_CMD).isEqualTo("git --no-pager rev-parse HEAD~1") 23 | } 24 | 25 | @Test 26 | fun whenGetCommitSha_thenReturnCommitSha() { 27 | commandRunner.addReply(PreviousCommit.PREV_COMMIT_CMD, "commit-sha") 28 | 29 | val actual = previousCommit.get(commandRunner) 30 | 31 | assertThat(actual).isEqualTo("commit-sha") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/SpecifiedBranchCommitMergeBaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.AttachLogsTestRule 4 | import com.dropbox.affectedmoduledetector.mocks.MockCommandRunner 5 | import com.google.common.truth.Truth 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.JUnit4 10 | 11 | @RunWith(JUnit4::class) 12 | class SpecifiedBranchCommitMergeBaseTest { 13 | 14 | @Rule 15 | @JvmField 16 | val attachLogsRule = AttachLogsTestRule() 17 | private val logger = attachLogsRule.logger 18 | private val commandRunner = MockCommandRunner(logger) 19 | private val previousCommit = SpecifiedBranchCommitMergeBase(SPECIFIED_BRANCH) 20 | 21 | @Test 22 | fun `WHEN CURRENT_BRANCH_CMD THEN command returned`() { 23 | Truth.assertThat(SpecifiedBranchCommitMergeBase.CURRENT_BRANCH_CMD).isEqualTo("git rev-parse --abbrev-ref HEAD") 24 | } 25 | 26 | @Test 27 | fun `WHEN get commit sha THEN return sha`() { 28 | 29 | commandRunner.addReply(SpecifiedBranchCommitMergeBase.CURRENT_BRANCH_CMD, NEW_FEATURE_BRANCH) 30 | commandRunner.addReply("git merge-base $NEW_FEATURE_BRANCH $SPECIFIED_BRANCH", "commit-sha") 31 | 32 | val actual = previousCommit.get(commandRunner) 33 | 34 | Truth.assertThat(actual).isEqualTo("commit-sha") 35 | } 36 | 37 | private companion object { 38 | 39 | const val SPECIFIED_BRANCH = "origin/dev" 40 | const val NEW_FEATURE_BRANCH = "newFeatureBranch" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/commitshaproviders/SpecifiedBranchCommitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.commitshaproviders 2 | 3 | import com.dropbox.affectedmoduledetector.AttachLogsTestRule 4 | import com.dropbox.affectedmoduledetector.mocks.MockCommandRunner 5 | import com.google.common.truth.Truth 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.JUnit4 10 | 11 | @RunWith(JUnit4::class) 12 | class SpecifiedBranchCommitTest { 13 | @Rule 14 | @JvmField 15 | val attachLogsRule = AttachLogsTestRule() 16 | private val logger = attachLogsRule.logger 17 | private val commandRunner = MockCommandRunner(logger) 18 | private val branch = "mybranch" 19 | private val previousCommit = SpecifiedBranchCommit(branch) 20 | 21 | @Test 22 | fun whenGetCommitSha_thenReturnCommitSha() { 23 | commandRunner.addReply("git rev-parse $branch", "commit-sha") 24 | 25 | val actual = previousCommit.get(commandRunner) 26 | 27 | Truth.assertThat(actual).isEqualTo("commit-sha") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockCommandRunner.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.mocks 2 | 3 | import com.dropbox.affectedmoduledetector.FileLogger 4 | import com.dropbox.affectedmoduledetector.GitClient 5 | 6 | internal class MockCommandRunner(private val logger: FileLogger) : GitClient.CommandRunner { 7 | private val replies = mutableMapOf>() 8 | 9 | fun addReply(command: String, response: String) { 10 | logger.info("add reply. cmd: $command response: $response") 11 | replies[command] = response.split(System.lineSeparator()) 12 | } 13 | 14 | override fun execute(command: String): String { 15 | return replies.getOrDefault(command, emptyList()) 16 | .joinToString(System.lineSeparator()).also { 17 | logger.info("cmd: $command response: $it") 18 | } 19 | } 20 | 21 | override fun executeAndParse(command: String): List { 22 | return replies.getOrDefault(command, emptyList()).also { 23 | logger.info("cmd: $command response: $it") 24 | } 25 | } 26 | 27 | override fun executeAndParseFirst(command: String): String { 28 | return replies.getOrDefault(command, emptyList()).first().also { 29 | logger.info("cmd: $command response: $it") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/mocks/MockCommitShaProvider.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.mocks 2 | 3 | import com.dropbox.affectedmoduledetector.GitClient 4 | import com.dropbox.affectedmoduledetector.Sha 5 | import com.dropbox.affectedmoduledetector.commitshaproviders.CommitShaProvider 6 | 7 | class MockCommitShaProvider : CommitShaProvider { 8 | private val replies = mutableListOf() 9 | 10 | fun addReply(sha: Sha) { 11 | replies.add(sha) 12 | } 13 | override fun get(commandRunner: GitClient.CommandRunner): Sha { 14 | return replies.first() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /affectedmoduledetector/src/test/kotlin/com/dropbox/affectedmoduledetector/rules/SetupAndroidProject.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.rules 2 | 3 | import org.junit.rules.TemporaryFolder 4 | import java.io.File 5 | import java.io.FileNotFoundException 6 | 7 | /** 8 | * TestRule that allows setup of the Android SDK within the context of a GradleRunner test. 9 | */ 10 | class SetupAndroidProject : TemporaryFolder() { 11 | 12 | /** 13 | * Setup the Android SDK location for test involving Gradle and the Android Gradle DSL. 14 | * 15 | * Users may have configured their machine with an environment variable ANDROID_SDK_ROOT in 16 | * which case the Android SDK will automatically be found using this. Otherwise we attempt to 17 | * local the machine specific local.properties file and copy the contents. 18 | */ 19 | fun setupAndroidSdkLocation() { 20 | // If we happen to have already set this environment variable then we are all good to go. 21 | // Nothing to see here. Move along. 22 | if (!System.getenv("ANDROID_SDK_ROOT").isNullOrBlank()) return 23 | 24 | // Find the local.properties file. File works relative to the nearest build.gradle which 25 | // is the one for the Gradle module that we are currently running tests in - not the root 26 | // project. But assuming that we will only ever be one level deep then we can simply use 27 | // ".." to go up one level where we _should_ find our file. 28 | val localDotProperties = File("../local.properties") 29 | 30 | if (!localDotProperties.exists()) throw FileNotFoundException( 31 | """ 32 | |Unable to locate Android SDK. Ensure that you have either the ANDROID_SDK_ROOT 33 | |environment variable set or the sdk.dir location correctly set in local 34 | |.properties within the Gradle root project directory. 35 | """.trimMargin() 36 | ) 37 | 38 | // Read our machine specific local.properties file and copy it to our temp Gradle test 39 | // directory for the test. 40 | localDotProperties.bufferedReader().use { 41 | newFile("local.properties").writeText(it.readText()) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.9.25" 4 | repositories { 5 | google() 6 | mavenCentral() 7 | maven { 8 | url "https://plugins.gradle.org/m2/" 9 | } 10 | } 11 | dependencies { 12 | classpath "com.android.tools.build:gradle:8.6.1" 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | classpath("org.jlleitschuh.gradle:ktlint-gradle:11.4.2") 15 | classpath("org.jacoco:org.jacoco.core:0.8.10") 16 | classpath "com.vanniktech:gradle-maven-publish-plugin:0.19.0" 17 | } 18 | } 19 | 20 | apply plugin: "org.jlleitschuh.gradle.ktlint" 21 | 22 | allprojects { 23 | repositories { 24 | google() 25 | mavenCentral() 26 | } 27 | } 28 | 29 | allprojects { 30 | plugins.withId("com.vanniktech.maven.publish") { 31 | mavenPublish { 32 | sonatypeHost = "S01" 33 | } 34 | } 35 | } 36 | 37 | task clean(type: Delete) { 38 | delete rootProject.buildDir 39 | } 40 | -------------------------------------------------------------------------------- /dependency_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/dependency_graph.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | 23 | # POM 24 | GROUP = com.dropbox.affectedmoduledetector 25 | VERSION_NAME=0.5.0 26 | 27 | POM_ARTIFACT_ID = affectedmoduledetector 28 | POM_NAME = Affected Module Detector 29 | POM_DESCRIPTION = A Gradle Plugin and library to determine which modules were affected in a commit. 30 | 31 | POM_URL = https://github.com/dropbox/AffectedModuleDetector/ 32 | POM_SCM_URL = https://github.com/dropbox/AffectedModuleDetector/ 33 | POM_SCM_CONNECTION = scm:git:https://github.com/dropbox/AffectedModuleDetector.git 34 | POM_SCM_DEV_CONNECTION = scm:git:git@github.com:dropbox/AffectedModuleDetector.git 35 | 36 | POM_LICENCE_NAME = The Apache License, Version 2.0 37 | POM_LICENCE_URL = http://www.apache.org/licenses/LICENSE-2.0.txt 38 | POM_LICENCE_DIST = repo 39 | 40 | POM_DEVELOPER_ID = mplat-dbx 41 | POM_DEVELOPER_NAME = Dropbox 42 | POM_DEVELOPER_URL = https://github.com/dropbox/ 43 | POM_DEVELOPER_EMAIL = api-support@dropbox.com 44 | -------------------------------------------------------------------------------- /gradle/jacoco.gradle: -------------------------------------------------------------------------------- 1 | // Merge of 2 | // https://github.com/mgouline/android-samples/blob/master/jacoco/app/build.gradle 3 | // and https://github.com/pushtorefresh/storio/blob/master/gradle/jacoco-android.gradle 4 | 5 | // Enables code coverage for JVM tests. 6 | apply plugin: "jacoco" 7 | // Android Gradle Plugin out of the box supports only code coverage for instrumentation tests. 8 | // Creates a task that will merge coverage for all projects 9 | project.afterEvaluate { 10 | def testTaskName = "test" 11 | def coverageTaskName = "${testTaskName}Coverage" 12 | 13 | // Create coverage task of form 'testFlavorTypeUnitTestCoverage' depending on 'testFlavorTypeUnitTest' 14 | task "${coverageTaskName}"(type: JacocoReport, dependsOn: "$testTaskName") { 15 | group = 'Reporting' 16 | description = "Generate Jacoco coverage reports for the build." 17 | 18 | List fileFilter = ['**/R.class', 19 | '**/R$*.class', 20 | '**/*$ViewInjector*.*', 21 | '**/*$ViewBinder*.*', 22 | '**/BuildConfig.*', 23 | '**/Manifest*.*', 24 | '**/*$Lambda$*.*', // Jacoco can not handle several "$" in class name. 25 | '**/*$inlined$*.*', // Kotlin specific, Jacoco can not handle several "$" in class name. 26 | '**/*Module.*', // Modules for Dagger. 27 | '**/*Dagger*.*', // Dagger auto-generated code. 28 | '**/*MembersInjector*.*', // Dagger auto-generated code. 29 | '**/*_Provide*Factory*.*', // Dagger auto-generated code. 30 | '**/test/**/*.*', // Test code 31 | ] 32 | def kotlinFileTree = fileTree( 33 | dir: "${project.buildDir}/classes", 34 | excludes: fileFilter) 35 | logger.debug("Kotlin classes dirs: " + kotlinFileTree) 36 | 37 | classDirectories.setFrom kotlinFileTree 38 | 39 | def coverageSourceDirs = new File(project.projectDir, "src/main/java") 40 | def coverageData = fileTree(dir: new File(project.buildDir, 'jacoco'), include: '*.exec') 41 | 42 | logger.debug("Coverage source dirs: " + coverageSourceDirs) 43 | logger.debug("Coverage execution data: " + executionData) 44 | 45 | additionalSourceDirs.setFrom files(coverageSourceDirs) 46 | sourceDirectories.setFrom files(coverageSourceDirs) 47 | executionData.setFrom files(coverageData) 48 | 49 | reports { 50 | xml.required = true 51 | xml.destination = file("${buildDir}/reports/jacoco/report.xml") 52 | html.required = true 53 | } 54 | } 55 | 56 | check.dependsOn "${coverageTaskName}" 57 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | buildSrc/build 18 | buildSrc/.idea/ -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | import com.dropbox.affectedmoduledetector.AffectedModuleConfiguration 2 | import com.dropbox.sample.Dependencies 3 | 4 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 5 | buildscript { 6 | repositories { 7 | google() 8 | mavenCentral() 9 | maven { 10 | url "https://plugins.gradle.org/m2/" 11 | } 12 | mavenLocal() 13 | } 14 | dependencies { 15 | classpath Dependencies.Libs.ANDROID_BUILD_TOOLS 16 | classpath Dependencies.Libs.KOTLIN_GRADLE_PLUGIN 17 | classpath Dependencies.Libs.KTLINT 18 | classpath Dependencies.Libs.DETEKT 19 | } 20 | } 21 | 22 | apply plugin: "org.jlleitschuh.gradle.ktlint" 23 | apply plugin: "com.dropbox.affectedmoduledetector" 24 | 25 | affectedModuleDetector { 26 | baseDir = "${project.rootDir}" 27 | pathsAffectingAllModules = [ 28 | "buildSrc/" 29 | ] 30 | specifiedBranch = "origin/main" 31 | compareFrom = "SpecifiedBranchCommitMergeBase" 32 | customTasks = [ 33 | new AffectedModuleConfiguration.CustomTask( 34 | "runDetektByImpact", 35 | "detekt", 36 | "Run static analysis tool by Impact analysis" 37 | ) 38 | ] 39 | logFolder = "${project.rootDir}" 40 | excludedModules = [ 41 | "sample-util" 42 | ] 43 | } 44 | 45 | allprojects { 46 | repositories { 47 | google() 48 | mavenCentral() 49 | } 50 | } 51 | 52 | task clean(type: Delete) { 53 | delete rootProject.buildDir 54 | } 55 | -------------------------------------------------------------------------------- /sample/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Dropbox, Inc. All rights reserved. 3 | */ 4 | plugins { 5 | kotlin("jvm") version "1.9.25" 6 | `java-gradle-plugin` 7 | } 8 | 9 | repositories { 10 | google() 11 | mavenCentral() 12 | mavenLocal() 13 | } 14 | 15 | dependencies { 16 | implementation("com.dropbox.affectedmoduledetector:affectedmoduledetector:0.5.0-SNAPSHOT") 17 | testImplementation("junit:junit:4.13.1") 18 | testImplementation("com.nhaarman:mockito-kotlin:1.5.0") 19 | testImplementation("com.google.truth:truth:1.0.1") 20 | } -------------------------------------------------------------------------------- /sample/buildSrc/src/main/kotlin/com/dropbox/sample/AffectedTestsPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.sample 2 | 3 | import com.dropbox.affectedmoduledetector.AffectedModuleDetector 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.api.tasks.testing.Test 7 | 8 | /** 9 | * Example plugin which uses the [AffectedModuleDetector] to filter out tests which don't need to run 10 | * 11 | * This is something a developer would need to build to use the [AffectedModuleDetector]. 12 | */ 13 | class AffectedTestsPlugin : Plugin { 14 | override fun apply(target: Project) { 15 | // Only allow unit tests to run if the AffectedModuleDetector says to include them 16 | target.tasks.withType(Test::class.java) { task -> 17 | AffectedModuleDetector.configureTaskGuard(task) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/buildSrc/src/main/kotlin/com/dropbox/sample/Dependencies.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.sample 2 | 3 | object Dependencies { 4 | 5 | private object Versions { 6 | const val KOTLIN_VERSION = "1.9.25" 7 | const val DETEKT_VERSION = "1.20.0" 8 | } 9 | 10 | object Libs { 11 | const val KOTLIN_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_VERSION}" 12 | const val KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN_VERSION}" 13 | const val ANDROIDX_CORE_KTX = "androidx.core:core-ktx:1.3.2" 14 | const val ANDROIDX_APP_COMPAT = "androidx.appcompat:appcompat:1.2.0" 15 | const val ANDROID_MATERIAL = "com.google.android.material:material:1.2.1" 16 | const val ANDROIDX_CONSTRAINTLAYOUT = "androidx.constraintlayout:constraintlayout:1.1.3" 17 | const val JUNIT = "junit:junit:4.13.1" 18 | const val ANDROIDX_TEST_EXT = "androidx.test.ext:junit:1.1.2" 19 | const val ANDROIDX_ESPRESSO = "androidx.test.espresso:espresso-core:3.3.0" 20 | const val ANDROID_BUILD_TOOLS = "com.android.tools.build:gradle:8.6.1" 21 | const val KTLINT = "org.jlleitschuh.gradle:ktlint-gradle:9.1.1" 22 | const val DETEKT = "com.android.tools.build:gradle:${Versions.DETEKT_VERSION}" 23 | const val DETEKT_PLUGIN = "io.gitlab.arturbosch.detekt" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/buildSrc/src/main/kotlin/com/dropbox/sample/tasks/AffectedTasksPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.affectedmoduledetector.tasks 2 | 3 | import com.dropbox.affectedmoduledetector.AffectedModuleDetector 4 | import com.dropbox.affectedmoduledetector.DependencyTracker 5 | import com.dropbox.affectedmoduledetector.projectPath 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.Task 9 | import org.gradle.api.tasks.testing.Test 10 | import java.util.* 11 | 12 | var TEST_TASK_TO_RUN_EXTENSION = "TestTasks" 13 | 14 | open class TestTasks { 15 | val assembleAndroidTestTask = "assembleDebugAndroidTest" 16 | val runAndroidTestTask = "connectedDebugAndroidTest" 17 | val jvmTest = "testDebugUnitTest" 18 | val jvmTestBackup = "test" 19 | } 20 | 21 | /** 22 | * unlike [AffectedTestsPlugin] which skips unaffected tests, this plugn instead creates a task and 23 | * registers all affected test tasks. Advantage is speed in not needing to skip modules at a large scale 24 | * 25 | * Registers 3 tasks 26 | * gradlew runAffectedUnitTests - runs jvm tests 27 | * gradlew runAffectedAndroidTests - runs connected tests 28 | * gradlew assembleAffectedAndroidTests - assembles but does not run on device tests, useful when working with device labs 29 | */ 30 | class AffectedTasksPlugin : Plugin { 31 | var ANDROID_TEST_BUILD_VARIANT = "AndroidTest" 32 | 33 | lateinit var testTasks: TestTasks 34 | override fun apply(project: Project) { 35 | project.extensions.add(TEST_TASK_TO_RUN_EXTENSION, TestTasks()) 36 | project.afterEvaluate { 37 | val rootProject = project.rootProject 38 | testTasks = requireNotNull( 39 | project.extensions.findByName(TEST_TASK_TO_RUN_EXTENSION) 40 | ) as TestTasks 41 | registerAffectedTestTask( 42 | "runAffectedUnitTests", 43 | testTasks.jvmTest, testTasks.jvmTestBackup, rootProject, 44 | ) 45 | 46 | registerAffectedAndroidTests(rootProject) 47 | registerAffectedConnectedTestTask(rootProject) 48 | } 49 | 50 | filterAndroidTests(project) 51 | filterUnitTests(project) 52 | } 53 | 54 | private fun registerAffectedTestTask( 55 | taskName: String, testTask: String, testTaskBackup: String?, 56 | rootProject: Project 57 | ) { 58 | rootProject.tasks.register(taskName) { task -> 59 | val paths = getAffectedPaths(testTask, testTaskBackup, rootProject) 60 | paths.forEach { path -> 61 | task.dependsOn(path) 62 | } 63 | task.enabled = paths.isNotEmpty() 64 | task.onlyIf { paths.isNotEmpty() } 65 | } 66 | } 67 | 68 | private fun getAffectedPaths( 69 | task: String, 70 | taskBackup: String?, 71 | rootProject: Project 72 | ): 73 | Set { 74 | val paths = LinkedHashSet() 75 | rootProject.subprojects { subproject -> 76 | val pathName = "${subproject.path}:$task" 77 | val backupPath = "${subproject.path}:$taskBackup" 78 | if (AffectedModuleDetector.isProjectProvided(subproject)) { 79 | if (subproject.tasks.findByPath(pathName) != null) { 80 | paths.add(pathName) 81 | } else if (taskBackup != null && 82 | subproject.tasks.findByPath(backupPath) != null 83 | ) { 84 | paths.add(backupPath) 85 | } 86 | } 87 | } 88 | return paths 89 | } 90 | 91 | private fun registerAffectedConnectedTestTask( 92 | rootProject: Project 93 | ) { 94 | registerAffectedTestTask( 95 | "runAffectedAndroidUnitTests", 96 | testTasks.runAndroidTestTask, null, rootProject 97 | ) 98 | } 99 | 100 | private fun registerAffectedAndroidTests( 101 | rootProject: Project 102 | ) { 103 | registerAffectedTestTask( 104 | "assembleAffectedAndroidTests", 105 | testTasks.assembleAndroidTestTask, null, rootProject 106 | ) 107 | } 108 | 109 | 110 | private fun filterAndroidTests(project: Project) { 111 | val tracker = DependencyTracker(project, null) 112 | project.tasks.configureEach { task -> 113 | if (task.name.contains(ANDROID_TEST_BUILD_VARIANT)) { 114 | tracker.findAllDependents(project.projectPath).forEach { dependentProject -> 115 | project.findProject(dependentProject.path)?.tasks?.forEach { dependentTask -> 116 | AffectedModuleDetector.configureTaskGuard(dependentTask) 117 | } 118 | } 119 | AffectedModuleDetector.configureTaskGuard(task) 120 | } 121 | } 122 | } 123 | 124 | private fun filterUnitTests(project: Project) { 125 | project.tasks.withType(Test::class.java) { task -> 126 | AffectedModuleDetector.configureTaskGuard(task) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /sample/buildSrc/src/main/resources/META-INF/gradle-plugins/com.dropbox.affectedtasksplugin.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.dropbox.affectedmoduledetector.tasks.AffectedTasksPlugin -------------------------------------------------------------------------------- /sample/buildSrc/src/main/resources/META-INF/gradle-plugins/com.dropbox.sample.AffectedTestPlugin.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.dropbox.sample.AffectedTestsPlugin 2 | -------------------------------------------------------------------------------- /sample/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /sample/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /sample/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /sample/sample-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/sample-app/build.gradle: -------------------------------------------------------------------------------- 1 | import com.dropbox.sample.Dependencies 2 | 3 | plugins { 4 | id 'com.android.application' 5 | id 'kotlin-android' 6 | } 7 | 8 | affectedTestConfiguration { 9 | assembleAndroidTestTask = "assembleAndroidTest" 10 | } 11 | 12 | 13 | android { 14 | namespace "com.dropbox.detector.sample" 15 | 16 | compileSdk 34 17 | 18 | defaultConfig { 19 | applicationId "com.dropbox.detector.sample" 20 | minSdkVersion 23 21 | targetSdkVersion 34 22 | versionCode 1 23 | versionName "1.0" 24 | 25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_11 36 | targetCompatibility JavaVersion.VERSION_11 37 | } 38 | kotlinOptions { 39 | jvmTarget = '11' 40 | } 41 | } 42 | 43 | dependencies { 44 | 45 | implementation project(":sample-core") 46 | implementation project(":sample-util") 47 | 48 | implementation Dependencies.Libs.KOTLIN_STDLIB 49 | implementation Dependencies.Libs.ANDROIDX_APP_COMPAT 50 | implementation Dependencies.Libs.ANDROID_MATERIAL 51 | implementation Dependencies.Libs.ANDROIDX_CONSTRAINTLAYOUT 52 | testImplementation Dependencies.Libs.JUNIT 53 | androidTestImplementation Dependencies.Libs.ANDROIDX_TEST_EXT 54 | androidTestImplementation Dependencies.Libs.ANDROIDX_ESPRESSO 55 | } -------------------------------------------------------------------------------- /sample/sample-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/sample-app/src/androidTest/java/com/dropbox/detector/sample/ExampleAndroidTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.detector.sample 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleAndroidTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(5, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/java/com/dropbox/detector/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.detector.sample 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | 6 | class MainActivity : AppCompatActivity() { 7 | 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | setContentView(R.layout.activity_main) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | sample 3 | -------------------------------------------------------------------------------- /sample/sample-app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /sample/sample-app/src/test/java/com/dropbox/detector/sample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.detector.sample 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/sample-core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/sample-core/build.gradle: -------------------------------------------------------------------------------- 1 | import com.dropbox.sample.Dependencies 2 | 3 | plugins { 4 | id 'com.android.library' 5 | id 'kotlin-android' 6 | id("io.gitlab.arturbosch.detekt") version "1.20.0" 7 | } 8 | 9 | affectedTestConfiguration { 10 | jvmTestTask = "test" 11 | } 12 | 13 | android { 14 | namespace "com.dropbox.detector.sample_core" 15 | 16 | compileSdk 34 17 | 18 | defaultConfig { 19 | minSdkVersion 23 20 | targetSdkVersion 34 21 | versionCode 1 22 | versionName "1.0" 23 | 24 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 25 | consumerProguardFiles "consumer-rules.pro" 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_11 36 | targetCompatibility JavaVersion.VERSION_11 37 | } 38 | kotlinOptions { 39 | jvmTarget = '11' 40 | } 41 | } 42 | 43 | dependencies { 44 | 45 | implementation project(":sample-util") 46 | 47 | implementation Dependencies.Libs.KOTLIN_STDLIB 48 | implementation Dependencies.Libs.ANDROIDX_APP_COMPAT 49 | implementation Dependencies.Libs.ANDROID_MATERIAL 50 | implementation Dependencies.Libs.ANDROIDX_CONSTRAINTLAYOUT 51 | testImplementation Dependencies.Libs.JUNIT 52 | androidTestImplementation Dependencies.Libs.ANDROIDX_TEST_EXT 53 | androidTestImplementation Dependencies.Libs.ANDROIDX_ESPRESSO 54 | } -------------------------------------------------------------------------------- /sample/sample-core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-core/consumer-rules.pro -------------------------------------------------------------------------------- /sample/sample-core/detekt-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PackageNaming:ExampleUnitTest.kt$package com.dropbox.detector.sample_core 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/sample-core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/sample-core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/sample-core/src/test/java/com/dropbox/detector/sample_core/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.detector.sample_core 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/sample-jvm-module/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/sample-jvm-module/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("org.jetbrains.kotlin.jvm") 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_11 8 | targetCompatibility = JavaVersion.VERSION_11 9 | } 10 | 11 | kotlin { 12 | jvmToolchain(11) 13 | } -------------------------------------------------------------------------------- /sample/sample-jvm-module/detekt-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EmptyClassBlock:MyClass.kt$MyClass${ } 6 | NewLineAtEndOfFile:MyClass.kt$com.dropbox.sample_jvm_module.MyClass.kt 7 | PackageNaming:MyClass.kt$package com.dropbox.sample_jvm_module 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample/sample-jvm-module/src/main/java/com/dropbox/sample_jvm_module/MyClass.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.sample_jvm_module 2 | 3 | class MyClass { 4 | } -------------------------------------------------------------------------------- /sample/sample-util/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/sample-util/build.gradle: -------------------------------------------------------------------------------- 1 | import com.dropbox.sample.Dependencies 2 | 3 | plugins { 4 | id 'com.android.library' 5 | id 'kotlin-android' 6 | } 7 | 8 | affectedTestConfiguration { 9 | assembleAndroidTestTask = "assembleDebugAndroidTest" 10 | } 11 | 12 | android { 13 | namespace "com.dropbox.detector.sample_util" 14 | compileSdk 34 15 | 16 | defaultConfig { 17 | minSdkVersion 23 18 | targetSdkVersion 34 19 | versionCode 1 20 | versionName "1.0" 21 | 22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 23 | consumerProguardFiles "consumer-rules.pro" 24 | } 25 | 26 | buildTypes { 27 | release { 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_11 34 | targetCompatibility JavaVersion.VERSION_11 35 | } 36 | kotlinOptions { 37 | jvmTarget = '11' 38 | } 39 | } 40 | 41 | dependencies { 42 | 43 | implementation Dependencies.Libs.KOTLIN_STDLIB 44 | implementation Dependencies.Libs.ANDROIDX_APP_COMPAT 45 | implementation Dependencies.Libs.ANDROID_MATERIAL 46 | implementation Dependencies.Libs.ANDROIDX_CONSTRAINTLAYOUT 47 | testImplementation Dependencies.Libs.JUNIT 48 | androidTestImplementation Dependencies.Libs.ANDROIDX_TEST_EXT 49 | androidTestImplementation Dependencies.Libs.ANDROIDX_ESPRESSO 50 | } -------------------------------------------------------------------------------- /sample/sample-util/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/sample/sample-util/consumer-rules.pro -------------------------------------------------------------------------------- /sample/sample-util/detekt-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PackageNaming:ExampleUnitTest.kt$package com.dropbox.detector.sample_util 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/sample-util/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/sample-util/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sample/sample-util/src/test/java/com/dropbox/detector/sample_util/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.dropbox.detector.sample_util 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample-app' 2 | include ':sample-core' 3 | include ':sample-jvm-module' 4 | include ':sample-util' -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':affectedmoduledetector' -------------------------------------------------------------------------------- /specified_branch_difference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/AffectedModuleDetector/cde9e2eeaf20fbf58056d61cb45efbef036c483d/specified_branch_difference.png --------------------------------------------------------------------------------