├── .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 | [](https://maven-badges.herokuapp.com/maven-central/com.dropbox.affectedmoduledetector/affectedmoduledetector/)
4 |
5 | [](https://travis-ci.org/dropbox/AffectedModuleDetector)
6 |
7 | [](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 | 
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
--------------------------------------------------------------------------------