├── .github ├── CODEOWNERS ├── dependabot.yml ├── issue_template.md ├── pull_request_template.md └── workflows │ ├── precommit.yml │ └── publish.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE-APACHE.txt ├── LICENSE-EPL.txt ├── LICENSES.md ├── README.md ├── build.gradle.kts ├── docs ├── Configuration.md ├── Design.md ├── GraphAnalysis.md ├── GraphComparison.md ├── GraphInspection.md ├── GraphValidation.md ├── Motivation.md ├── RunBook-ReleaseProcess.md └── Visualization.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .gitignore ├── README.md ├── app │ └── build.gradle.kts ├── build.gradle.kts ├── buildSrc │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── convention │ │ ├── graph-analytics.gradle.kts │ │ └── test-support.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper ├── gradlew ├── lib1-api │ └── build.gradle.kts ├── lib1-impl │ └── build.gradle.kts ├── lib1-test-support │ └── build.gradle.kts ├── lib2-api │ └── build.gradle.kts ├── lib2-impl │ └── build.gradle.kts ├── lib2-test-support │ └── build.gradle.kts └── settings.gradle.kts ├── settings.gradle.kts └── src ├── main └── kotlin │ └── com │ └── ebay │ └── plugins │ └── graph │ └── analytics │ ├── Attributed.kt │ ├── BaseGraphInputOutputTask.kt │ ├── BaseGraphInputTask.kt │ ├── BaseGraphPersistenceTask.kt │ ├── BasicGraphMetricsAnalysisTask.kt │ ├── BetweennessCentralityAnalysisTask.kt │ ├── ComparisonChange.kt │ ├── ConfigurationClass.kt │ ├── ConfigurationClassifier.kt │ ├── ConfigurationClassifierDefault.kt │ ├── ConsolidationTask.kt │ ├── DirectComparisonTask.kt │ ├── EdgeInfo.kt │ ├── GatherTask.kt │ ├── GradleExtensions.kt │ ├── GraphAnalyticsPaths.kt │ ├── GraphAnalyticsPlugin.kt │ ├── GraphComparisonHelper.kt │ ├── GraphExtension.kt │ ├── GraphPersistence.kt │ ├── GraphPersistenceBuildService.kt │ ├── GraphPersistenceGexf.kt │ ├── GraphPersistenceGraphMl.kt │ ├── GraphRelation.kt │ ├── GraphUtils.kt │ ├── InspectionTask.kt │ ├── NetworkExpansionAnalysisTask.kt │ ├── ReachableNodesDirection.kt │ ├── ReachableNodesScoringAlgorithm.kt │ ├── VertexAttributeCollector.kt │ ├── VertexHeightAnalysisTask.kt │ ├── VertexHeightScoringAlgorithm.kt │ ├── VertexInfo.kt │ └── validation │ ├── GraphValidationExtension.kt │ ├── GraphValidationResult.kt │ ├── GraphValidationRule.kt │ ├── GraphValidationTask.kt │ ├── RootedEdge.kt │ ├── RootedVertex.kt │ ├── Summarized.kt │ └── matchers │ ├── AllOfGraphMatcher.kt │ ├── AnyOfGraphMatcher.kt │ ├── AttributeBooleanGraphMatcher.kt │ ├── AttributeNumberGraphMatcher.kt │ ├── AttributeStringGraphMatcher.kt │ ├── DescribedMatch.kt │ ├── EdgeSourceGraphMatcher.kt │ ├── EdgeTargetGraphMatcher.kt │ ├── EqualToGraphMatcher.kt │ ├── EveryItemGraphMatcher.kt │ ├── GraphMatcher.kt │ ├── GraphMatchers.kt │ ├── GreaterThanGraphMatcher.kt │ ├── HasItemGraphMatcher.kt │ ├── LessThanGraphMatcher.kt │ ├── MatchesPatternGraphMatcher.kt │ ├── NotGraphMatcher.kt │ ├── OutgoingEdgesGraphMatcher.kt │ ├── SummarizedExtensions.kt │ └── VertexPathGraphMatcher.kt └── test ├── kotlin └── com │ └── ebay │ └── plugins │ └── graph │ └── analytics │ ├── BaseGraphPersistenceTest.kt │ ├── BasePluginFunctionalTest.kt │ ├── DependenciesSpec.kt │ ├── GraphEdgeInfoTest.kt │ ├── GraphPersistenceGexfTest.kt │ ├── GraphPersistenceGraphMlTest.kt │ ├── GraphVertexInfoTest.kt │ ├── PluginFunctionalTest.kt │ └── validation │ └── matchers │ ├── AllOfGraphMatcherTest.kt │ ├── AnyOfGraphMatcherTest.kt │ ├── AttributeNumberGraphMatcherTest.kt │ ├── AttributeStringGraphMatcherTest.kt │ ├── DescribedMatchTest.kt │ ├── EdgeSourceGraphMatcherTest.kt │ ├── EdgeTargetGraphMatcherTest.kt │ ├── EqualToGraphMatcherTest.kt │ ├── EveryItemGraphMatcherTest.kt │ ├── GreaterThanGraphMatcherTest.kt │ ├── HasItemGraphMatcherTest.kt │ ├── LessThanGraphMatcherTest.kt │ ├── MatchesPatternGraphMatcherTest.kt │ ├── NotGraphMatcherTest.kt │ └── VertexPathGraphMatcherTest.kt └── resources ├── build.gradle.kts └── settings.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # All files are owned by the maintainers 2 | * @eBay/graph-analytics-plugin-team 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: gradle 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Describe the Issue 2 | 3 | 4 | ### Expected Behavior 5 | 6 | 7 | ### Actual Behavior 8 | 9 | 10 | ### Steps to Reproduce the Behavior 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | This PR relates to Issue # 2 | 3 | Changes proposed in this pull request: 4 | - 5 | -------------------------------------------------------------------------------- /.github/workflows/precommit.yml: -------------------------------------------------------------------------------- 1 | name: PR Build 2 | on: 3 | pull_request: 4 | jobs: 5 | gradle: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | # wrapper = oldest supported version 10 | # current = latest stable version 11 | # release-candidate = next stable version 12 | # nightly = latest development version 13 | gradle-version: ["wrapper", "current", "release-candidate", "nightly"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-java@v4 17 | with: 18 | distribution: temurin 19 | java-version: 17 20 | - name: Setup Gradle 21 | uses: gradle/actions/setup-gradle@v4 22 | with: 23 | gradle-version: ${{ matrix.gradle-version }} 24 | - name: Build Plugin 25 | run: ./gradlew build 26 | - name: Sample Analysis 27 | working-directory: sample 28 | run: ../gradlew graphAnalysis 29 | - name: Sample Inspection 30 | working-directory: sample 31 | run: ../gradlew :app:graphInspection 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Gradle Plugin 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [published] 6 | jobs: 7 | gradle: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-java@v4 12 | with: 13 | distribution: temurin 14 | java-version: 17 15 | - name: Setup Gradle 16 | uses: gradle/actions/setup-gradle@v4 17 | - name: Build Plugin 18 | env: 19 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 20 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} 21 | run: ./gradlew -Dorg.gradle.unsafe.isolated-projects=false --no-configuration-cache publishPlugins 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gradle 3 | .kotlin 4 | build 5 | 6 | # Android Studio / IntelliJ 7 | /.idea/* 8 | !/.idea/codeStyles 9 | !/.idea/runConfigurations/ 10 | *.iws 11 | *.iml 12 | /local.properties 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Contributors to this project are expected to adhere to the 4 | [eBay Code of Conduct](https://github.com/eBay/.github/blob/main/CODE_OF_CONDUCT.md) 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Graph Analytics Plugin 2 | 3 | First, thank you for considering contributing to this project! 4 | 5 | We welcome contributions and have provided a guide to help make this is as straightforward as 6 | possible. 7 | 8 | ## Steps 9 | 10 | 1. [Raise an Issue](#raise-an-issue) 11 | 1. [Write code](#write-code) 12 | 1. [Submit pull request](#submit-pull-request) 13 | 14 | ## Raise an Issue 15 | 16 | Before starting development please look at the existing issues to ensure that your concern or 17 | improvement is not already being addressed. This is important to prevent conflicting work. 18 | 19 | If no issues already match your intentions raise an issue with sufficient detail as to what you 20 | intend to do. The issue can then become a place where discussion relating to the problem 21 | statement and intended approach can be had. 22 | 23 | This step is critical in ensuring that owners are aware of and can provide feedback on incoming 24 | changes. 25 | 26 | This helps eliminate duplicate work, conflicting work, or any other unintended consequences. 27 | 28 | ## Write code 29 | 30 | All code should conform to the project's code style. This project follows the 31 | [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html). 32 | 33 | All work intended for release should be committed against the `main` branch. 34 | 35 | Work will not be considered complete unless it is in a publishable, production-ready 36 | state. This restriction is in place to ensure that the library can be published when 37 | needed without delay. 38 | 39 | ### Tests 40 | 41 | Every bug fix or change should be accompanied by corresponding tests. These tests are 42 | important both to protect the quality of the library as well as to demonstrate scenarios 43 | that the change impacts. 44 | 45 | ## Submit pull request 46 | 47 | Submit a PR to merge the code into the `main` branch and assign to one or more 48 | owners for review. 49 | 50 | The PR should reference the issue being worked on and contain a description that is 51 | useful for understanding the implementation details. 52 | 53 | Once a PR is approved, it will likely be merged to `main`. 54 | 55 | If your change is time-sensitive, please indicate this fact in a comment on the 56 | PR. The project owners will do their best to publish a release containing your change 57 | as soon as possible. 58 | 59 | -------------------------------------------------------------------------------- /LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 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 2017 eBay, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSES.md: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | ## EPL 2.0 4 | 5 | This project is built as a derivative work on top of the [jgrapht](https://jgrapht.org/) 6 | library which is licensed under the Eclipse Public License - v 2.0. The full text of this 7 | license is available in the [LICENSE-EPL.txt](LICENSE-EPL.txt) file. The original source 8 | for this library is available at [https://jgrapht.org/](https://jgrapht.org/). 9 | 10 | Please note that JGraphT is distributed WITHOUT ANY WARRANTY; without even the implied 11 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 12 | 13 | ## Apache 2.0 14 | 15 | This Source Code may also be made available under the following Secondary Licenses 16 | when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 17 | are satisfied: Apache License Version 2.0 18 | 19 | The full text of the Apache License may be found in the [LICENSE-APACHE.txt](LICENSE-APACHE.txt) 20 | file. 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Gradle Plugin Portal Version](https://img.shields.io/gradle-plugin-portal/v/com.ebay.graph-analytics) 2 | 3 | # Graph Analytics Plugin 4 | 5 | ## About This Project 6 | 7 | This [Gradle](https://gradle.org/) plugin was designed to be used in multi-module 8 | projects to analyze the inter-module dependency graph and provide insights into the 9 | structure of the project. This information can then be used to identify areas of 10 | improvement and measure the resulting impact of changes being made. 11 | 12 | Each project module becomes a vertex in the resulting graph. The dependencies between the 13 | project modules are expressed as edges between these vertices. 14 | 15 | The plugin provides the following features: 16 | - Generation of a GraphML file representing the project dependency graph 17 | - Analysis of individual project modules, providing deeper insights into the costs being 18 | incurred by the module 19 | - Extensibility, allowing for custom metrics to be added to the analysis 20 | - Validation of the graph, allowing enforcement of graph metrics on a per-project basis using 21 | a flexible and extensible rule system 22 | 23 | ## Background 24 | 25 | To better understand why this plugin exists and how it works, the following documents 26 | may be referenced: 27 | - [Motivation](docs/Motivation.md): Why the plugin was created and the problems it solves 28 | - [Design Overview](docs/Design.md): High level overview of how the plugin functions 29 | 30 | ## Requirements 31 | 32 | The plugin is designed to work with Gradle 8.11 or later. 33 | 34 | ## Usage 35 | 36 | To use, add the plugin your project's settings.gradle.kts file, as follows. This will ensure 37 | that the plugin is applied to all project modules: 38 | ```kotlin 39 | // settings.gradle.kts 40 | plugins { 41 | id("com.ebay.graph-analytics") version("") 42 | } 43 | ``` 44 | 45 | The following tasks are provided by the plugin on each project module: 46 | | Task Name | Description | 47 | |-------------------|-------------| 48 | | `graphAnalysis` | Runs the graph analysis and generates the GraphML file for the module | 49 | | `graphComparison` | Compares two graph analysis GraphML files and highlights the changes | 50 | | `graphInspection` | Creates a report providing details into the project graph of an individual project module | 51 | | `graphValidation` | Performs a graph analysis and assert the graph validation rules have been adhered to | 52 | 53 | For more details on each of these tasks, reference the following: 54 | - [Graph Analysis](docs/GraphAnalysis.md) 55 | - [Graph Inspection](docs/GraphInspection.md) 56 | - [Graph Comparison](docs/GraphComparison.md) 57 | - [Graph Validation](docs/GraphValidation.md) 58 | 59 | ## Metrics Provided 60 | 61 | Although the plugin is designed to be extensible, it comes with a number of built-in metrics: 62 | 63 | | Metric ID | Description | 64 | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 65 | | `degree` | The total number of incoming (dependents) and outgoing (dependencies) a project has | 66 | | `inDegree` | The number of dependents which depend upon the project | 67 | | `outDegree` | The number of dependencies which the project declares | 68 | | `height` | The size of the longest path in the project's tree of dependencies | 69 | | `networkAbove` | The total number of dependant projects which depend upon the project. This is useful to understand the number of projects impacted by a change to this project. | 70 | | `networkBelow` | The total number of dependency projects which this project depends upon. This is useful to understand the number of projects which would affect this project when changed. | 71 | | `betweennessCentrality` | Calculates the [Betweenness Centrality](https://en.wikipedia.org/wiki/Betweenness_centrality) value for the project | 72 | | `expansionFactor` | A synthetic metric that is the product of the `inDegree` and `networkBelow`. This attempts to capture the relative impact a project has in expanding the overall project graph. | 73 | 74 | ## Configuration 75 | 76 | The plugin will provide utility in its default configuration. However, the addition of 77 | project-specific metrics and rules can greatly extend its capabilities! 78 | 79 | For more information, please refer to the [Configuration](docs/Configuration.md) document. 80 | 81 | ## Integrations 82 | 83 | ### Metrics for Develocity 84 | 85 | Out-of-the-box, this plugin provides project graph information based upon the declared 86 | structure of the project. When working to improve build performance, the next questions 87 | that naturally follow fall along the following lines: 88 | - How much build time does each project module contribute? 89 | - How many frequently are project modules built? 90 | 91 | To answer these questions, the 92 | [Metrics for Develocity](https://github.com/eBay/metrics-for-develocity-plugin) 93 | plugin can be used to gather this information from a 94 | [Develocity](https://gradle.com/gradle-enterprise-solutions/) 95 | server instance, layering this data into the project graph analysis performed by 96 | this plugin. 97 | 98 | For more information, see the 99 | [Project Cost Graph Analytics Integration](https://github.com/eBay/metrics-for-develocity-plugin/tree/main/src/main/kotlin/com/ebay/plugins/metrics/develocity/projectcost#project-cost-graph-analytics-integration) 100 | document. 101 | 102 | ## Contributing 103 | 104 | Contributions are welcome! Please refer to the [CONTRIBUTING](CONTRIBUTING.md) document for 105 | guidelines on how to contribute. 106 | 107 | ## Run Books 108 | 109 | The following documents describe various processes needed for operating and/or maintaining 110 | the plugin: 111 | - [Run Book: Release Process](docs/RunBook-ReleaseProcess.md) 112 | 113 | ## License 114 | 115 | This project is dual-licensed under the Eclipse Public License 2.0 and the Apache License 116 | Version 2.0. 117 | 118 | See [LICENSES](LICENSES.md) for more information. 119 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 3 | 4 | plugins { 5 | `embedded-kotlin` 6 | alias(libs.plugins.gradle.pluginPublish) 7 | } 8 | 9 | group = "com.ebay" 10 | 11 | gradlePlugin { 12 | website = "https://github.com/ebay/graph-analytics-plugin" 13 | vcsUrl = "https://github.com/ebay/graph-analytics-plugin.git" 14 | plugins { 15 | create("com.ebay.graph-analytics") { 16 | id = "com.ebay.graph-analytics" 17 | implementationClass = "com.ebay.plugins.graph.analytics.GraphAnalyticsPlugin" 18 | displayName = "Graph Analytics Plugin" 19 | description = "Gradle plugin to perform project graph analysis, assertion, and reporting for multi-module projects" 20 | tags = listOf( 21 | "graph", "analysis", "assert", "multiprojects", "module", "dependency-graph" 22 | ) 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | api(libs.jgrapht.core) 29 | api(libs.jgrapht.io) { 30 | // Not needed since it is only used for DOT, GML, JSON, and CSV support. 31 | exclude(group = "org.antlr", module = "antlr4-runtime") 32 | } 33 | 34 | testImplementation(libs.test.hamcrest) 35 | testImplementation(libs.test.mockito.kotlin) 36 | testImplementation(libs.test.testng) 37 | } 38 | 39 | java { 40 | sourceCompatibility = JavaVersion.VERSION_11 41 | targetCompatibility = JavaVersion.VERSION_11 42 | } 43 | 44 | tasks.withType(KotlinJvmCompile::class.java) { 45 | compilerOptions { 46 | allWarningsAsErrors.set(true) 47 | jvmTarget.set(JvmTarget.JVM_11) 48 | freeCompilerArgs.addAll(listOf("-opt-in=kotlin.RequiresOptIn")) 49 | } 50 | } 51 | 52 | tasks.withType { 53 | useTestNG() 54 | } -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The plugin is highly configurable and can be customized to suit the needs of your project. 4 | 5 | The plugin will add a `graphAnalytics` extension to the project. This extension can be used to 6 | configure the following: 7 | 8 | ### Configuration Classifier 9 | 10 | Each Gradle configuration is bucketed into a `PRODUCTION`, `TEST`, or `OTHER` category. The 11 | `PRODUCTION` and `TEST` categories are used to build independent directed graphs which are 12 | later combined into a single graph. Configurations falling into the `OTHER` bucket are 13 | ignored altogether. 14 | 15 | It is the job of the 16 | [ConfigurationClassifier](../src/main/kotlin/com/ebay/plugins/graph/analytics/ConfigurationClassifier.kt) 17 | to determine what bucket each Gradle configuration should be assigned to. 18 | 19 | In most scenarios, the default classifier should be sufficient. However, if you have custom 20 | configurations or find that the default classifier is not working as expected, you can provide 21 | your own behavior to override the default. 22 | 23 | Example: 24 | ``` 25 | graphAnalytics { 26 | configurationClassifier.set(MyCustomConfigurationClassifier()) 27 | } 28 | ``` 29 | 30 | ### Vertex Attribute Collectors 31 | 32 | Some vertex attributes may be derived from information defined by the project module and only 33 | be available at configuration-time. For example, detecting whether or not a specific plugin 34 | was applied to the project. Configuration-time data gathering such as this is performed by 35 | implementing custom 36 | [VertexAttributeCollector](../src/main/kotlin/com/ebay/plugins/graph/analytics/VertexAttributeCollector.kt)s. 37 | 38 | For an example, please refer to the sample project's 39 | [CustomVertexAttributeCollector](../sample/buildSrc/src/main/kotlin/convention/graph-analytics.gradle.kts) 40 | 41 | ### Vertex Analysis Tasks 42 | 43 | Vertex analysis tasks are used to compute metrics on each project module in the graph. These 44 | metrics then become part of the GraphML data file and are used in the manual analysis process 45 | as well as in graph validation. 46 | 47 | To add a custom vertex analysis task, the task must extend from the 48 | [BaseGraphInputOutputTask](../src/main/kotlin/com/ebay/plugins/graph/analytics/BaseGraphInputOutputTask.kt) 49 | class and be added to the `graphAnalytics` extension. For an example, please refer to the 50 | sample project's 51 | [VertexClassAnalysisTask](../sample/buildSrc/src/main/kotlin/convention/graph-analytics.gradle.kts) 52 | task. 53 | 54 | ### Graph Data Consumer Tasks 55 | 56 | Some tasks need to consume the graph data after it has been fully generated. To facilitate this, 57 | tasks which extend 58 | [BaseGraphInputTask](../src/main/kotlin/com/ebay/plugins/graph/analytics/BaseGraphInputTask.kt) 59 | may be registered as consumers. 60 | -------------------------------------------------------------------------------- /docs/Design.md: -------------------------------------------------------------------------------- 1 | # Design Overview 2 | 3 | ## Analysis 4 | 5 | Prior to gathering dependency data, the plugin calls "vertex attribute collectors" to gather 6 | configuration-time information about each project module. This information is stored in 7 | the graph as attributes on the vertices. 8 | 9 | Each project defines dependencies which apply to the production code as well as (optionally) 10 | dependencies which apply only to tests for the project in question. For these dependencies, 11 | only those which are references to other projects (e.g., `implementation(projects.featureModule)`) 12 | are considered. 13 | 14 | A "configuration classifier" is used to determine whether a particular configuration represents 15 | a production configuration or a test configuration. 16 | 17 | In order to avoid creating Gradle project dependency cycles, the production dependencies are 18 | collected separately from test dependencies. These separate graphs are then consolidated into 19 | a single project graph containing a holistic view of the project graph. Note that each individual 20 | graph is acyclic _until_ they are combined, at which point the graph may potentially become cyclic. 21 | 22 | Once the consolidated project graph is created it is then run through an analysis phase. 23 | The analysis performs computations on each vertex in the graph and adds attributes to record 24 | the results. Multiple analysis tasks may be run during this phase to gather or compute different 25 | types of data. 26 | 27 | ## Validation 28 | 29 | Validation consumes the generated GraphML file as an input and performs checks on the graph 30 | for each project module. 31 | 32 | In order to create a type-safe, extensible validation system, the plugin uses a "matcher" 33 | system similar to the one used in [Hamcrest](https://hamcrest.org/JavaHamcrest) Matchers. 34 | A small library of matchers is provided with the plugin to enable common checks and to 35 | provide the basis for extending the checks in a project-specific manner. 36 | -------------------------------------------------------------------------------- /docs/GraphAnalysis.md: -------------------------------------------------------------------------------- 1 | # Graph Analysis 2 | 3 | To analyze the project graph, run the `graphAnalysis` task on the project module you with 4 | to analyze. 5 | 6 | ## Execution 7 | 8 | For example, in the [sample project](../sample) we could run: 9 | ```shell 10 | ../gradlew :app:graphAnalysis 11 | ``` 12 | 13 | This will generate a GraphML file at `app/build/graphAnalytics/analysis.graphml`. This file 14 | can then be loaded into a visualization utility, as documented in 15 | [Visualization](Visualization.md). 16 | -------------------------------------------------------------------------------- /docs/GraphComparison.md: -------------------------------------------------------------------------------- 1 | # Graph Comparison 2 | 3 | When considering the impact of changes to a project it can be useful to see what the 4 | impact of the change is prior to merge, or what the change is over a longer period of 5 | time. 6 | 7 | The `graphComparison` task is designed to provide a textual report that highlights 8 | the changes to the tracked metric values, using two GraphML analysis files as inputs. 9 | 10 | ## Execution 11 | 12 | To inspect the project graph for a specific module, run the `graphComparison` task on the 13 | project module you wish to inspect. 14 | 15 | This task takes one or both of the following parameters: 16 | - `--before ` The path to the GraphML analysis file of the starting state 17 | - `--after ` The path to the GraphML analysis file of the end state 18 | 19 | If only one of these parameters is provided the other will be assumed to be the current 20 | analysis file for the project module. 21 | 22 | For both parameters, absolute paths work. If a relative path is specified it will be evaluated 23 | relative to the project module's directory where the task is being run. 24 | 25 | For example, in the [sample project](../sample) we could 26 | run: 27 | 28 | ```shell 29 | ../gradlew :app:graphAnalysis 30 | cp app/build/graphAnalytics/analysis.graphml app/build/graphAnalytics/analysis-before.graphml 31 | # ...some changes to the project graph could be made here... 32 | ../gradlew :app:graphComparison \ 33 | --before build/graphAnalytics/analysis-before.graphml \ 34 | --after build/graphAnalytics/analysis.graphml 35 | # or... 36 | ../gradlew :app:graphComparison \ 37 | --before build/graphAnalytics/analysis-before.graphml 38 | ``` 39 | 40 | This will generate a report file at `app/build/graphAnalytics/directComparison.txt`. The location 41 | of this file will also be printed to the console log whenever it is regenerated. 42 | 43 | Note that in this silly example the analysis state before and after are the same file and therefore 44 | there are no changes reported. 45 | 46 | ## Report Contents 47 | 48 | The resulting report will provide the following: 49 | 50 | - A textual rendering of the graph node and all of its attribute values 51 | - Aggregate change counts for all numeric attributes 52 | - A list of all project modules with changes in their metric values, along with details on the 53 | changes themselves. 54 | -------------------------------------------------------------------------------- /docs/GraphInspection.md: -------------------------------------------------------------------------------- 1 | # Graph Inspection 2 | 3 | When dealing with large projects it can sometimes be non-obvious why a project module's 4 | metrics are what they are. The `graphInspection` task is designed to create a textual 5 | report that can be used to help gain better understanding on the origin of the costs. 6 | 7 | ## Execution 8 | 9 | To inspect the project graph for a specific module, run the `graphInspection` task on the 10 | project module you wish to inspect. The inspection task may be run in one of two ways, 11 | described below. 12 | 13 | The first approach inspects a project module from the perspective of the graph of another 14 | project module. Depending upon the project structure, this may provide a more comprehensive 15 | view of the module's relationships. This approach would typically be used to analyze a 16 | project module from the perspective of an application project module. 17 | 18 | For example, in the [sample project](../sample) we could run: 19 | ```shell 20 | ./gradlew :app:graphInspection --project :lib2-impl 21 | ``` 22 | 23 | This will generate a report file at `app/build/graphAnalytics/projectReport.txt`. 24 | The location of this file will also be printed to the console log whenever it is regenerated. 25 | 26 | Alternately, the inspection task can be run against a specific project module. In this mode 27 | of operation, only the project graph known to that project module will be available. This 28 | will - for instance - exclude information about modules which depend upon the project module. 29 | 30 | For example, in the [sample project](../sample) we could run: 31 | 32 | ```shell 33 | ./gradlew :lib2-impl:graphInspection 34 | ``` 35 | 36 | This will generate a report file at `lib2-impl/build/graphAnalytics/projectReport.txt`. 37 | The location of this file will also be printed to the console log whenever it is regenerated. 38 | 39 | ## Report Contents 40 | 41 | The resulting report will provide the following: 42 | 43 | - A textual rendering of the graph node and all of its attribute values 44 | - If the project module is involved in one or more dependency cycles, these cycles will be 45 | enumerated. This can be useful to find sub-graphs which form a natural cluster of dependencies 46 | that, if broken, could help to isolate the individual libraries. 47 | - For each numeric attribute defined, the top 10 dependencies ordered by the attribute value 48 | (descending) will be listed. 49 | - A full, depth-first traversal of the project module's dependencies, with each dependency 50 | rendered with all defined graph attributes. -------------------------------------------------------------------------------- /docs/GraphValidation.md: -------------------------------------------------------------------------------- 1 | # Graph Validation 2 | 3 | Graph validation is a powerful feature which allows for the enforcement of rules, based on 4 | the graph data itself, upon the project as a whole. This can be used to enforce architectural 5 | structures within the project (e.g., architectural layer restrictions) or to enforce specific 6 | best practices, as defined by the project team. 7 | 8 | Graph validation is defined in terms of 9 | [GraphValidationRule](../src/main/kotlin/com/ebay/plugins/graph/analytics/validation/GraphValidationRule.kt) 10 | implementations. Each rule is defined by specifying a set of conditions which must be met 11 | which indicate a violation of the rule. The conditions are specified in a manner similar to 12 | hamcrest matchers, allowing for complex and custom conditions to be specified. 13 | 14 | For an example, please refer to the sample project's 15 | [rule definitions](../sample/buildSrc/src/main/kotlin/convention/graph-analytics.gradle.kts). 16 | 17 | Rule implementations are registered with the `validation` extension on the `graphAnalytics` 18 | extension. 19 | 20 | ## Execution 21 | 22 | To validate the project graph for all project modules, run the `graphInspection` task from the 23 | root project directory. For example, in the [sample project](../sample) we could 24 | run: 25 | 26 | ```shell 27 | $ ./gradlew graphValidation 28 | ``` 29 | 30 | This will run the graph validation process for all project modules. Since there are no 31 | violations in the sample project, the process should complete successfully. After completion, 32 | manual inspection of the generated report files in each module's `build/graphAnalytics` 33 | directory can provide some insight into how the plugin works. 34 | 35 | #### `sample/app/build/graphAnalytics/analysis.graphml` 36 | 37 | This file contains the complete project graph from the perspective of the `:app` module. 38 | Only project modules which are reachable from the `:app` module are included in this graph. 39 | 40 | This analysis file can be loaded into Gephi for visualization. 41 | 42 | #### `sample/app/build/graphAnalytics/validationReport.txt` 43 | 44 | This file contains a summary of the validation process for the `:app` module. Since there 45 | are no violations in the sample project, this file should be mostly empty. If violations 46 | are present, details about the violations - also printed to the console - will be found 47 | here. 48 | 49 | -------------------------------------------------------------------------------- /docs/Motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | When working with very large and complex Android applications that have evolved organically 4 | over many years, it can be difficult to understand how the project modules relate to one another. 5 | When the number of project modules increases beyond a certain point, it becomes an increasingly 6 | overwhelming task. 7 | 8 | Adding to this challenge is the need to provide automatic enforcement of key architectural 9 | concerns, such as maintaining proper isolation between API and implementation details. 10 | The specific application of this sort of project structure rules tend to be very project- or 11 | team- specific. Some solutions exist to help in this area but make assumptions that may not 12 | be suitable for many projects. 13 | 14 | As we surveyed the available tools, we found that none of them fully satisfied the 15 | requirements that we wanted to solve for, including: 16 | - Extensible and customizable graph analysis data layering model 17 | - Flexibility in defining and enforcing project conventions 18 | - Operation on a project module basis rather than on a per task basis 19 | - Usefulness in providing insights into finding - and more importantly correcting - existing 20 | poorly chosen module relationships 21 | - Use of a common, rich file format that can be used with existing open source visualization 22 | tools, such as [Gephi](https://gephi.org/) 23 | - Compliance with modern Gradle plugin development best practices, such as configuration 24 | cache support and project isolation 25 | 26 | The Graph Analytics Plugin was born out of the need to address these requirements. 27 | 28 | ## Inspiration 29 | 30 | The following projects were sources of inspiration for the Graph Analytics Plugin. We 31 | are thankful for the work of these projects and the insights they provided: 32 | - [Graph Assert Plugin](https://github.com/jraska/modules-graph-assert) 33 | - [Graph Untangler Plugin](https://github.com/siggijons/graph-untangler-plugin) 34 | - [Talaoit Plugin](https://github.com/cdsap/Talaiot) 35 | -------------------------------------------------------------------------------- /docs/RunBook-ReleaseProcess.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | ## Overview 4 | 5 | - At the beginning of the development cycle, the `gradle.properties` file should be updated to 6 | the next anticipated version number, following semantic versioning conventions. Generally, 7 | this should just increment the patch level. For example, `0.0.2`. 8 | - Development takes place... 9 | - When development is completed and a release is desired, evaluate the version number to ensure 10 | that it is still accurate with respect to the expectations of the semantic versioning scheme. 11 | Adjust the version in `gradle.properties` accordingly. 12 | - Publish the release with change notes. This process is detailed below. 13 | 14 | ## Publishing a Release 15 | 16 | - Navigate to the [Releases](https://github.com/eBay/graph-analytics-plugin/releases) page on 17 | GitHub. 18 | - Click the "Draft a new release" button. 19 | - Click the "Choose a tag" button and enter the version being published, prefixed with a `v`. 20 | For example, `v0.0.2`. Click the "Create new tag: on publish" button. 21 | - Ensure the `Target` is set to `main`. 22 | - Ensure the `Previous tag` is set to the last release tag. 23 | - Click `Generate release notes` to populate the release notes. 24 | - Use the prefixed version number as the title of the release. 25 | - Edit the release notes, as/if necessary. 26 | - Ensure that `Set as the latest release` is checked. 27 | - Click the "Publish release" button. 28 | 29 | The act of publishing the release will trigger the build and publish the release to the Gradle 30 | Plugin Repository. This process can be monitored by navigating to the 31 | [Actions](https://github.com/eBay/graph-analytics-plugin/actions) page. -------------------------------------------------------------------------------- /docs/Visualization.md: -------------------------------------------------------------------------------- 1 | # Visualization 2 | 3 | For manual exploration of the project graph, we recommend the use of [Gephi](https://gephi.org/). 4 | Gephi can directly load the GraphML files generated by the Graph Analytics Plugin and provide 5 | a rich set of tools for exploring and visualizing the project graph. 6 | 7 | ## Generating the GraphML File 8 | 9 | To use Gephi, you must first generate a GraphML file from the project graph. 10 | 11 | The Graph Analysis Plugin provides a Gradle task (`graphAnalysis`), available on every 12 | project module, to generate this file. Selecting a project module at the top of the 13 | project (e.g., the application module that contains the other modules as dependencies) 14 | is recommended. 15 | 16 | For example, in the [sample project](../sample) we could run: 17 | ```shell 18 | $ ./gradlew :app:graphAnalysis 19 | ``` 20 | 21 | Upon successful completion, the resulting GraphML file will be located at: 22 | ``` 23 | app/build/graphAnalytics/analysis.graphml 24 | ``` 25 | 26 | This file may then be loaded into Gephi for visualization. 27 | 28 | ## Recommended Settings 29 | 30 | The following settings are recommended as a decent starting point for visualization 31 | of the project graph data: 32 | 1. In the `Graph` window, adjust visibility settings: 33 | - Enable `Show Node Labels` (Outlined 'T' in the bottom left) 34 | - Adjust the label font size down 35 | 2. Configure the `Layout`: 36 | - Use `Force Atlas` layout with the following parameter changes: 37 | - `Repulsion strength`: 500.0 38 | - `Attraction distribution`: checked 39 | - `Adjust by sizes`: checked 40 | - `Run` the `Force Atlas` layout until it stabilizes, then hit `Stop` 41 | 3. Use the `LabelAdjust` layout engine. Hit `Run` and then `Stop` after it stabilizes 42 | 4. Adjust appearance settings to highlight problematic dependencies: 43 | - `Nodes` appearance. Adjust and hit `Apply` on each: 44 | - `Color` tab, use `betweennessCentrality` ranking and select a color palette 45 | - `Size` tab, use `networkBelow` ranking 46 | - Min size: 1 47 | - Max size: 500 48 | - `Label Color` tab, use `outDegree` and select a color palette 49 | - `Label Size` tab, leave at default 50 | - `Edges` appearance: 51 | - `Color` tab, use `class` partitioning and select a color palette 52 | - `Label Color` tab, leave at default 53 | - `Label Size` tab, leave at default 54 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=true 2 | org.gradle.caching=true 3 | org.gradle.parallel=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.unsafe.isolated-projects=true 6 | version=1.1.0 7 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | gradle-pluginPublish = "1.3.1" 3 | jgrapht = "1.5.2" 4 | test-hamcrest = "3.0" 5 | test-mockito = "5.4.0" 6 | test-testng = "7.11.0" 7 | 8 | [libraries] 9 | jgrapht-core = { module = "org.jgrapht:jgrapht-core", version.ref = "jgrapht" } 10 | jgrapht-io = { module = "org.jgrapht:jgrapht-io", version.ref = "jgrapht" } 11 | 12 | ### Test Libraries 13 | test-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "test-hamcrest" } 14 | test-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "test-mockito" } 15 | test-testng = { module = "org.testng:testng", version.ref="test-testng" } 16 | 17 | [plugins] 18 | gradle-pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "gradle-pluginPublish" } 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eBay/graph-analytics-plugin/6f792d30e6c6223d5febef7e602740610670492f/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.11-bin.zip 4 | networkTimeout=120000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | # Android Studio / IntelliJ 2 | /.idea/* 3 | !/.idea/codeStyles 4 | !/.idea/runConfigurations/ 5 | /local.properties -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | # Sample Project 2 | 3 | This is a sample project which acts as a technology demonstrator for the Graph Analytics 4 | Plugin. 5 | 6 | ## Overview 7 | 8 | The sample project contains one high level module which simulates an application project. 9 | This module depends upon two library modules. Each of the library modules exports 10 | a "test support" module - effectively test fixtures for use by consumers of the library. 11 | One of the libraries depends upon the other. 12 | 13 | ## Demonstration 14 | 15 | For this demonstration, global project-wide settings are used to configure the plugin. 16 | These are defined in the 17 | [graph-analytics.gradle.kts](buildSrc/src/main/kotlin/convention/graph-analytics.gradle.kts) 18 | convention plugin. 19 | 20 | The test support libraries appear to Gradle as if they are ordinary libraries. A custom 21 | vertex attribute collector is add a vertex attribute indicating whether or not a specific 22 | `convention.test-support` convention plugin has been applied to the project. This attribute 23 | is then consumed by a custom vertex analysis task which assigns a project module into 24 | one of a small number of archetype categories ("api", "impl", "test-support", and "app"). 25 | 26 | Try running the tasks outlined in the [README](../README.md) to see the plugin in action. 27 | -------------------------------------------------------------------------------- /sample/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("convention.graph-analytics") 4 | } 5 | 6 | dependencies { 7 | implementation(projects.lib1Impl) 8 | implementation(projects.lib2Impl) 9 | } 10 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("convention.graph-analytics") 4 | } 5 | -------------------------------------------------------------------------------- /sample/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation("com.ebay:graph-analytics-plugin") 7 | } 8 | -------------------------------------------------------------------------------- /sample/buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | } 7 | includeBuild("../..") 8 | } 9 | 10 | dependencyResolutionManagement { 11 | repositories { 12 | gradlePluginPortal() 13 | } 14 | } 15 | 16 | rootProject.name = "graph-analytics-sample-conventions" -------------------------------------------------------------------------------- /sample/buildSrc/src/main/kotlin/convention/graph-analytics.gradle.kts: -------------------------------------------------------------------------------- 1 | package convention 2 | 3 | import com.ebay.plugins.graph.analytics.BaseGraphInputOutputTask 4 | import com.ebay.plugins.graph.analytics.EdgeInfo 5 | import com.ebay.plugins.graph.analytics.VertexAttributeCollector 6 | import com.ebay.plugins.graph.analytics.VertexInfo 7 | import com.ebay.plugins.graph.analytics.validation 8 | import com.ebay.plugins.graph.analytics.validation.GraphValidationRule 9 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.allOf 10 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.edgeTarget 11 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.equalTo 12 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.greaterThan 13 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.hasOutgoingEdge 14 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.not 15 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.numericAttribute 16 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.stringAttribute 17 | import org.jgrapht.graph.DefaultDirectedGraph 18 | import org.jgrapht.nio.AttributeType 19 | import org.jgrapht.nio.DefaultAttribute 20 | 21 | plugins { 22 | id("com.ebay.graph-analytics") 23 | } 24 | 25 | /** 26 | * Custom [VertexAttributeCollector] implementation, demonstrating how information can be 27 | * gathered from the project at configuration-time for use in subsequent analysis. Note that 28 | * this work needs to be very lightweight as it executes during the configuration phase. 29 | */ 30 | class CustomVertexAttributeCollector : VertexAttributeCollector { 31 | override fun collectConfigurationTimeAttributes(vertexInfo: VertexInfo) { 32 | val isTestSupport = project.plugins.findPlugin("convention.test-support") != null 33 | vertexInfo.attributes["test-support"] = DefaultAttribute(isTestSupport, AttributeType.BOOLEAN) 34 | } 35 | } 36 | 37 | /** 38 | * Custom analysis tasks which categorizes the purpose of the modules in a project-specific 39 | * manner. 40 | * 41 | * This demonstrates the use of a custom [VertexAttributeCollector] (to identify test support 42 | * modules) as well as project path name parsing, resulting in a custom graph attribute being 43 | * applied to all nodes in the graph. 44 | */ 45 | abstract class VertexClassAnalysisTask : BaseGraphInputOutputTask() { 46 | override fun processGraph(graph: DefaultDirectedGraph) { 47 | graph.vertexSet().forEach { vertexInfo -> 48 | val vertexClass = when { 49 | vertexInfo.attributes["test-support"]?.value == "true" -> "test-support" 50 | vertexInfo.path.endsWith("-api") -> "api" 51 | vertexInfo.path.endsWith("-impl") -> "impl" 52 | vertexInfo.path.endsWith("app") -> "application" 53 | else -> "other" 54 | } 55 | vertexInfo.attributes["vertexClass"] = DefaultAttribute(vertexClass, AttributeType.STRING) 56 | } 57 | } 58 | } 59 | 60 | graphAnalytics { 61 | vertexAttributeCollectors.add(CustomVertexAttributeCollector()) 62 | analysisTasks.add(project.tasks.register("vertexClassAnalysis", VertexClassAnalysisTask::class)) 63 | 64 | validation { 65 | // Perform validation relative to the :app project's graph. This gives us a complete 66 | // picture. With the example rules defined below this is not actually required since 67 | // each module validates against metrics that are based only upn dependencies and do 68 | // not need to take into account dependents. 69 | validatedProjects.set(listOf(":app")) 70 | 71 | rules.put("no-api-to-impl", GraphValidationRule( 72 | reason = "API modules must be expressed in terms of public API only", 73 | matcher = allOf( 74 | stringAttribute("vertexClass", equalTo("api")), 75 | hasOutgoingEdge(edgeTarget(allOf( 76 | stringAttribute("vertexClass", not(equalTo("api")), 77 | )))) 78 | ) 79 | )) 80 | 81 | // Example of a rule on API modules to limit the size of their total transitive 82 | // dependency graph. The `lib2-api` module violates this and defines a module-specific 83 | // override. 84 | rules.put("api-must-be-lightweight", GraphValidationRule( 85 | reason = "API modules should not have a lot of dependencies in order to protect the \n" + 86 | "build performance of the consuming projects", 87 | matcher = allOf( 88 | stringAttribute("vertexClass", equalTo("api")), 89 | numericAttribute("networkBelow", greaterThan(1)) 90 | ) 91 | )) 92 | } 93 | } -------------------------------------------------------------------------------- /sample/buildSrc/src/main/kotlin/convention/test-support.gradle.kts: -------------------------------------------------------------------------------- 1 | package convention 2 | 3 | // This is a marker plugin used to identify test-support modules -------------------------------------------------------------------------------- /sample/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=true 2 | org.gradle.caching=true 3 | org.gradle.parallel=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.unsafe.isolated-projects=true 6 | -------------------------------------------------------------------------------- /sample/gradle/wrapper: -------------------------------------------------------------------------------- 1 | ../../gradle/wrapper -------------------------------------------------------------------------------- /sample/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec ../gradlew "${@}" 3 | -------------------------------------------------------------------------------- /sample/lib1-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("convention.graph-analytics") 4 | } 5 | 6 | dependencies { 7 | // None 8 | } 9 | -------------------------------------------------------------------------------- /sample/lib1-impl/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("convention.graph-analytics") 4 | } 5 | 6 | dependencies { 7 | api(projects.lib1Api) 8 | 9 | testImplementation(projects.lib1TestSupport) 10 | } 11 | -------------------------------------------------------------------------------- /sample/lib1-test-support/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("convention.graph-analytics") 4 | id("convention.test-support") 5 | } 6 | 7 | dependencies { 8 | implementation(projects.lib1Api) 9 | } 10 | -------------------------------------------------------------------------------- /sample/lib2-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.allOf 2 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.greaterThan 3 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.numericAttribute 4 | 5 | plugins { 6 | `embedded-kotlin` 7 | id("convention.graph-analytics") 8 | } 9 | 10 | dependencies { 11 | implementation(projects.lib1Api) 12 | } 13 | 14 | graphAnalytics { 15 | validation { 16 | // Override the default rule for the "api-must-be-lightweight" rule for this module 17 | ruleOverrides.put("api-must-be-lightweight", allOf( 18 | numericAttribute("networkBelow", greaterThan(2)) 19 | )) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/lib2-impl/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("convention.graph-analytics") 4 | } 5 | 6 | dependencies { 7 | api(projects.lib2Api) 8 | 9 | testImplementation(projects.lib1TestSupport) 10 | testImplementation(projects.lib2TestSupport) 11 | } 12 | -------------------------------------------------------------------------------- /sample/lib2-test-support/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("convention.graph-analytics") 4 | id("convention.test-support") 5 | } 6 | 7 | dependencies { 8 | implementation(projects.lib2Api) 9 | implementation(projects.lib1TestSupport) 10 | } 11 | -------------------------------------------------------------------------------- /sample/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | } 7 | includeBuild("..") 8 | } 9 | 10 | plugins { 11 | id("com.ebay.graph-analytics") 12 | id("com.gradle.develocity") version("4.0.2") 13 | } 14 | 15 | dependencyResolutionManagement { 16 | repositories { 17 | mavenCentral() 18 | gradlePluginPortal() 19 | } 20 | } 21 | 22 | rootProject.name = "graph-analytics-sample" 23 | 24 | develocity { 25 | buildScan { 26 | termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") 27 | termsOfUseAgree.set("yes") 28 | } 29 | } 30 | 31 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 32 | include(":app") 33 | include(":lib1-api") 34 | include(":lib1-impl") 35 | include(":lib1-test-support") 36 | include(":lib2-api") 37 | include(":lib2-impl") 38 | include(":lib2-test-support") 39 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | plugins { 10 | id("com.gradle.develocity") version("4.0.2") 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | rootProject.name = "graph-analytics-plugin" 21 | 22 | develocity { 23 | buildScan { 24 | termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") 25 | termsOfUseAgree.set("yes") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/Attributed.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.nio.Attribute 4 | 5 | /** 6 | * An interface which is added to types which expose graph attributes. In the base graph, 7 | * both [VertexInfo] and [EdgeInfo] expose attributes. 8 | * 9 | * This indirection allows both to be treated the same in the DSL. 10 | */ 11 | interface Attributed { 12 | var attributes: MutableMap 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/BaseGraphInputOutputTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.file.RegularFileProperty 4 | import org.gradle.api.tasks.CacheableTask 5 | import org.gradle.api.tasks.InputFile 6 | import org.gradle.api.tasks.OutputFile 7 | import org.gradle.api.tasks.PathSensitive 8 | import org.gradle.api.tasks.PathSensitivity 9 | import org.gradle.api.tasks.TaskAction 10 | import org.jgrapht.graph.DefaultDirectedGraph 11 | 12 | @CacheableTask 13 | abstract class BaseGraphInputOutputTask : BaseGraphPersistenceTask() { 14 | /** 15 | * The graph input to analyze. 16 | */ 17 | @get:InputFile 18 | @get:PathSensitive(PathSensitivity.NONE) 19 | internal abstract val inputGraph: RegularFileProperty 20 | 21 | /** 22 | * The graph output file containing the graph including the applied analysis data. 23 | */ 24 | @get:OutputFile 25 | internal abstract val outputGraph: RegularFileProperty 26 | 27 | @TaskAction 28 | fun taskAction() { 29 | val graph = DefaultDirectedGraph(EdgeInfo::class.java) 30 | val persistence = persistenceBuildService.get() 31 | persistence.import(graph, inputGraph.asFile.get()) 32 | processGraph(graph) 33 | persistence.export(graph, outputGraph.asFile.get()) 34 | } 35 | 36 | abstract fun processGraph(graph: DefaultDirectedGraph) 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/BaseGraphInputTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.file.RegularFileProperty 4 | import org.gradle.api.tasks.InputFile 5 | import org.gradle.api.tasks.PathSensitive 6 | import org.gradle.api.tasks.PathSensitivity 7 | import org.gradle.api.tasks.TaskAction 8 | import org.jgrapht.graph.DefaultDirectedGraph 9 | 10 | /** 11 | * Base class which can be used by tasks which need to read the graph file as 12 | * an input (only). 13 | */ 14 | abstract class BaseGraphInputTask : BaseGraphPersistenceTask() { 15 | /** 16 | * The graph input to analyze. 17 | */ 18 | @get:InputFile 19 | @get:PathSensitive(PathSensitivity.NONE) 20 | internal abstract val inputGraph: RegularFileProperty 21 | 22 | @TaskAction 23 | fun taskAction() { 24 | val graph = DefaultDirectedGraph(EdgeInfo::class.java) 25 | val persistence = persistenceBuildService.get() 26 | persistence.import(graph, inputGraph.asFile.get()) 27 | processInputGraph(graph) 28 | } 29 | 30 | abstract fun processInputGraph(graph: DefaultDirectedGraph) 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/BaseGraphPersistenceTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.tasks.Input 6 | import org.gradle.api.tasks.Internal 7 | 8 | /** 9 | * Base class used for tasks which need to read and write the graph file. 10 | */ 11 | abstract class BaseGraphPersistenceTask : DefaultTask() { 12 | // Pass the graph format in so that version changes invalidate the cache 13 | @get:Input 14 | internal abstract val graphFormat: Property 15 | 16 | // Pass the graph version in so that version changes invalidate the cache 17 | @get:Input 18 | internal abstract val graphVersion: Property 19 | 20 | @get:Internal 21 | internal abstract val persistenceBuildService: Property 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/BasicGraphMetricsAnalysisTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.tasks.CacheableTask 4 | import org.jgrapht.graph.DefaultDirectedGraph 5 | import org.jgrapht.nio.AttributeType 6 | import org.jgrapht.nio.DefaultAttribute 7 | 8 | /** 9 | * This task applies the most basic graph metrics that require no scoring algorithm to the graph. 10 | */ 11 | @CacheableTask 12 | abstract class BasicGraphMetricsAnalysisTask : BaseGraphInputOutputTask() { 13 | override fun processGraph(graph: DefaultDirectedGraph) { 14 | graph.vertexSet().forEach { vertexInfo -> 15 | vertexInfo.attributes["degree"] = DefaultAttribute(graph.degreeOf(vertexInfo), AttributeType.INT) 16 | vertexInfo.attributes["inDegree"] = DefaultAttribute(graph.inDegreeOf(vertexInfo), AttributeType.INT) 17 | vertexInfo.attributes["outDegree"] = DefaultAttribute(graph.outDegreeOf(vertexInfo), AttributeType.INT) 18 | } 19 | } 20 | 21 | companion object { 22 | const val TASK_NAME = "basicGraphMetricsAnalysis" 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/BetweennessCentralityAnalysisTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.tasks.CacheableTask 4 | import org.jgrapht.alg.scoring.BetweennessCentrality 5 | import org.jgrapht.graph.DefaultDirectedGraph 6 | import org.jgrapht.nio.AttributeType 7 | import org.jgrapht.nio.DefaultAttribute 8 | 9 | /** 10 | * This task applies the Betweenness Centrality analysis to the graph. 11 | */ 12 | @CacheableTask 13 | abstract class BetweennessCentralityAnalysisTask : BaseGraphInputOutputTask() { 14 | override fun processGraph(graph: DefaultDirectedGraph) { 15 | val betweennessCentrality by lazy { BetweennessCentrality(graph) } 16 | 17 | graph.vertexSet().forEach { vertexInfo -> 18 | vertexInfo.attributes["betweennessCentrality"] = DefaultAttribute(betweennessCentrality.getVertexScore(vertexInfo), AttributeType.DOUBLE) 19 | } 20 | } 21 | 22 | companion object { 23 | const val TASK_NAME = "graphBetweennessCentralityAnalysis" 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/ComparisonChange.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.nio.AttributeType 4 | 5 | /** 6 | * Data class encapsulating an attribute change. Used in [ComparisonTask]. 7 | */ 8 | internal data class ComparisonChange( 9 | val project: String, 10 | val attributeName: String, 11 | val attributeType: AttributeType, 12 | val originalValue: String, 13 | val changedValue: String, 14 | ) { 15 | val comparisonResult by lazy { 16 | when(attributeType) { 17 | AttributeType.BOOLEAN -> { 18 | originalValue.toBoolean().compareTo(changedValue.toBoolean()) 19 | } 20 | AttributeType.INT -> { 21 | originalValue.toInt().compareTo(changedValue.toInt()) 22 | } 23 | AttributeType.LONG -> { 24 | originalValue.toLong().compareTo(changedValue.toLong()) 25 | } 26 | AttributeType.DOUBLE -> { 27 | originalValue.toDouble().compareTo(changedValue.toDouble()) 28 | } 29 | AttributeType.FLOAT -> { 30 | originalValue.toFloat().compareTo(changedValue.toFloat()) 31 | } 32 | else -> { 33 | originalValue.compareTo(changedValue) 34 | } 35 | } 36 | } 37 | 38 | fun originalAsNumber(): Number? { 39 | return toNumber(originalValue) 40 | } 41 | 42 | fun changedAsNumber(): Number? { 43 | return toNumber(changedValue) 44 | } 45 | 46 | private fun toNumber(value: String): Number? { 47 | return when(attributeType) { 48 | AttributeType.INT -> { 49 | value.toInt() 50 | } 51 | AttributeType.LONG -> { 52 | value.toLong() 53 | } 54 | AttributeType.DOUBLE -> { 55 | value.toDouble() 56 | } 57 | AttributeType.FLOAT -> { 58 | value.toFloat() 59 | } 60 | else -> { 61 | null 62 | } 63 | } 64 | } 65 | 66 | override fun toString(): String { 67 | return "$attributeName ${comparisonStr()} from $originalValue to $changedValue" 68 | } 69 | 70 | private fun comparisonStr(): String { 71 | return if (comparisonResult < 0) { 72 | "increased" 73 | } else if (comparisonResult > 0) { 74 | "decreased" 75 | } else { 76 | "unchanged" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/ConfigurationClass.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | /** 4 | * Gradle project configuration classifications. 5 | */ 6 | enum class ConfigurationClass { 7 | /** 8 | * The configuration is included in production code. 9 | */ 10 | PRODUCTION, 11 | 12 | /** 13 | * The configuration is included for testing. 14 | */ 15 | TEST, 16 | 17 | /** 18 | * The configuration is something else that we don't care about. 19 | */ 20 | OTHER 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/ConfigurationClassifier.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.artifacts.Configuration 4 | 5 | /** 6 | * Used to determine what [ConfigurationClassifier] a [Configuration] should considered to 7 | * be in. 8 | */ 9 | interface ConfigurationClassifier { 10 | fun classify(configuration: Configuration): ConfigurationClass 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/ConfigurationClassifierDefault.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.artifacts.Configuration 4 | 5 | /** 6 | * Default implementation of the [ConfigurationClassifier]. 7 | */ 8 | internal class ConfigurationClassifierDefault : ConfigurationClassifier { 9 | override fun classify(configuration: Configuration): ConfigurationClass { 10 | val configName = configuration.name 11 | return when { 12 | configName.startsWith("test") || configName.startsWith("androidTest") -> { 13 | ConfigurationClass.TEST 14 | } 15 | configName.startsWith("api") || configName.startsWith("implementation") -> { 16 | ConfigurationClass.PRODUCTION 17 | } 18 | else -> { 19 | ConfigurationClass.OTHER 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/ConsolidationTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import com.ebay.plugins.graph.analytics.GraphUtils.merge 4 | import org.gradle.api.file.ConfigurableFileCollection 5 | import org.gradle.api.file.RegularFileProperty 6 | import org.gradle.api.tasks.CacheableTask 7 | import org.gradle.api.tasks.InputFiles 8 | import org.gradle.api.tasks.OutputFile 9 | import org.gradle.api.tasks.PathSensitive 10 | import org.gradle.api.tasks.PathSensitivity 11 | import org.gradle.api.tasks.TaskAction 12 | import org.jgrapht.graph.DefaultDirectedGraph 13 | 14 | /** 15 | * Task used to consolidate the production dependencies and the test dependencies into a single, holistic 16 | * project graph (sans analysis). 17 | */ 18 | @CacheableTask 19 | internal abstract class ConsolidationTask : BaseGraphPersistenceTask() { 20 | /** 21 | * Dependencies of the project should publish their own dependency graph which is then included 22 | * into this project's graph. 23 | */ 24 | @get:InputFiles 25 | @get:PathSensitive(PathSensitivity.NONE) 26 | internal abstract val graphFiles: ConfigurableFileCollection 27 | 28 | /** 29 | * The output location of this project's dependency graph. 30 | */ 31 | @get:OutputFile 32 | internal abstract val outputFile: RegularFileProperty 33 | 34 | @TaskAction 35 | fun execute() { 36 | val graph = DefaultDirectedGraph(EdgeInfo::class.java) 37 | val persistence = persistenceBuildService.get() 38 | graphFiles.forEach { file -> 39 | val subgraph = DefaultDirectedGraph(EdgeInfo::class.java) 40 | persistence.import(subgraph, file) 41 | graph.merge(subgraph) 42 | } 43 | persistence.export(graph, outputFile.asFile.get()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/DirectComparisonTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.file.ProjectLayout 5 | import org.gradle.api.file.RegularFile 6 | import org.gradle.api.file.RegularFileProperty 7 | import org.gradle.api.tasks.Input 8 | import org.gradle.api.tasks.InputFile 9 | import org.gradle.api.tasks.Optional 10 | import org.gradle.api.tasks.OutputFile 11 | import org.gradle.api.tasks.TaskAction 12 | import org.gradle.api.tasks.options.Option 13 | import org.gradle.work.DisableCachingByDefault 14 | import org.jgrapht.graph.DefaultDirectedGraph 15 | import javax.inject.Inject 16 | 17 | /** 18 | * Task used to compare two graph analysis files and create a delta report. 19 | */ 20 | @DisableCachingByDefault(because = "The task argument inputs do not consider file contents") 21 | internal abstract class DirectComparisonTask : BaseGraphPersistenceTask() { 22 | @get:Inject 23 | internal abstract val projectLayout: ProjectLayout 24 | 25 | @get:InputFile 26 | internal abstract val defaultAnalysisFile: RegularFileProperty 27 | 28 | /** 29 | * Relative path to the project analysis graph file. 30 | */ 31 | @get:Input 32 | @get:Optional 33 | @set:Option( 34 | option = "before", 35 | description = "Path to the base graph file, relative to the project the task is run within" 36 | ) 37 | internal abstract var beforeFilePath: String? 38 | 39 | /** 40 | * Relative path to the changed project analysis graph file. 41 | */ 42 | @get:Input 43 | @get:Optional 44 | @set:Option( 45 | option = "after", 46 | description = "Path to the graph file containing the changes, relative to the project the task is run within" 47 | ) 48 | internal abstract var afterFilePath: String? 49 | 50 | /** 51 | * The output location of this project's report. 52 | */ 53 | @get:OutputFile 54 | internal abstract val outputFile: RegularFileProperty 55 | 56 | @TaskAction 57 | fun execute() { 58 | val beforeFilePathLocal = beforeFilePath 59 | val afterFilePathLocal = afterFilePath 60 | if (beforeFilePathLocal == null && afterFilePath == null) { 61 | throw GradleException("One or both of --before and --after must be provided") 62 | } 63 | 64 | val beforeFile: RegularFile = if (beforeFilePathLocal == null) { 65 | defaultAnalysisFile.get() 66 | } else { 67 | projectLayout.projectDirectory.file(beforeFilePathLocal) 68 | } 69 | val afterFile: RegularFile = if (afterFilePathLocal == null) { 70 | defaultAnalysisFile.get() 71 | } else { 72 | projectLayout.projectDirectory.file(afterFilePathLocal) 73 | } 74 | 75 | val beforeGraph = DefaultDirectedGraph(EdgeInfo::class.java) 76 | val persistence = persistenceBuildService.get() 77 | persistence.import(beforeGraph, beforeFile.asFile) 78 | 79 | val afterGraph = DefaultDirectedGraph(EdgeInfo::class.java) 80 | persistence.import(afterGraph, afterFile.asFile) 81 | 82 | val report = GraphComparisonHelper() 83 | .compare(beforeGraph, afterGraph) 84 | .replace("\n", System.lineSeparator()) 85 | 86 | outputFile.get().asFile.writeText(report) 87 | logger.lifecycle("Graph analysis comparison report available at: file://${outputFile.asFile.get()}") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/EdgeInfo.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.graph.DefaultEdge 4 | import org.jgrapht.nio.Attribute 5 | import java.util.Objects 6 | 7 | /** 8 | * Class which collects information about a single graph edge. This class must be 9 | * mutable due to assumptions made by the jgrapht persistence layer. The mutability then 10 | * causes issues with map lookups, so only the identifying fields are included in 11 | * [equals] and [hashCode] calculations. 12 | */ 13 | class EdgeInfo( 14 | override var attributes: MutableMap = mutableMapOf() 15 | ) : DefaultEdge(), Attributed { 16 | /** 17 | * Override to force inclusion of [source] and [target] 18 | */ 19 | override fun equals(other: Any?): Boolean { 20 | if (this === other) return true 21 | if (javaClass != other?.javaClass) return false 22 | other as EdgeInfo 23 | 24 | if (source != other.source) return false 25 | if (target != other.target) return false 26 | return true 27 | } 28 | 29 | /** 30 | * Override to force inclusion of (only) [source] and [target] 31 | */ 32 | override fun hashCode(): Int { 33 | return Objects.hash(source, target) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GatherTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import com.ebay.plugins.graph.analytics.GraphUtils.merge 4 | import org.gradle.api.GradleException 5 | import org.gradle.api.file.ConfigurableFileCollection 6 | import org.gradle.api.file.RegularFileProperty 7 | import org.gradle.api.provider.Property 8 | import org.gradle.api.provider.SetProperty 9 | import org.gradle.api.tasks.CacheableTask 10 | import org.gradle.api.tasks.Input 11 | import org.gradle.api.tasks.InputFiles 12 | import org.gradle.api.tasks.OutputFile 13 | import org.gradle.api.tasks.PathSensitive 14 | import org.gradle.api.tasks.PathSensitivity 15 | import org.gradle.api.tasks.TaskAction 16 | import org.jgrapht.graph.DefaultDirectedGraph 17 | 18 | /** 19 | * Task to gather the dependencies graph of a project. 20 | */ 21 | @CacheableTask 22 | internal abstract class GatherTask : BaseGraphPersistenceTask() { 23 | @get:Input 24 | internal abstract val explicitRelationships: SetProperty 25 | 26 | @get:Input 27 | internal abstract val selfInfoProp: Property 28 | 29 | @get:InputFiles 30 | @get:PathSensitive(value = PathSensitivity.NONE) 31 | internal abstract val contributedGraphs: ConfigurableFileCollection 32 | 33 | @get:OutputFile 34 | internal abstract val outputFile: RegularFileProperty 35 | 36 | @TaskAction 37 | fun execute() { 38 | val graph = DefaultDirectedGraph(EdgeInfo::class.java) 39 | val persistence = persistenceBuildService.get() 40 | graph.addVertex(selfInfoProp.get()) 41 | 42 | contributedGraphs.forEach { file -> 43 | val subgraph = DefaultDirectedGraph(EdgeInfo::class.java) 44 | persistence.import(subgraph, file) 45 | graph.merge(subgraph) 46 | } 47 | val mapOfPathToVertexInfo = graph.vertexSet().associateBy { it.path } 48 | explicitRelationships.get().forEach { relation -> 49 | val fromVertex = mapOfPathToVertexInfo[relation.from] 50 | ?: throw GradleException("Relation from ${relation.from} not found. This should not happen.") 51 | val toVertex = mapOfPathToVertexInfo[relation.to] 52 | ?: throw GradleException("Relation to ${relation.to} not found. This should not happen.") 53 | graph.addEdge(fromVertex, toVertex, relation.edge) 54 | } 55 | persistence.export(graph, outputFile.asFile.get()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GradleExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import com.ebay.plugins.graph.analytics.validation.GraphValidationExtension 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.TaskProvider 6 | 7 | /** 8 | * Executes the provided block to configure the [GraphExtension] if the [GraphAnalyticsPlugin] is applied. 9 | */ 10 | fun Project.graph(block: GraphExtension.() -> Unit) { 11 | plugins.withType(GraphAnalyticsPlugin::class.java) { 12 | block.invoke(extensions.getByType(GraphExtension::class.java)) 13 | } 14 | } 15 | 16 | /** 17 | * Executes the provided block to configure the [GraphValidationExtension]. 18 | */ 19 | fun GraphExtension.validation(block: GraphValidationExtension.() -> Unit) { 20 | block.invoke(extensions.getByType(GraphValidationExtension::class.java)) 21 | } 22 | 23 | /** 24 | * Helper function to configure the task to use the inputs of the given task. 25 | */ 26 | @Suppress("unused") // API method 27 | fun TaskProvider.inputsFrom( 28 | project: Project, 29 | taskName: String, 30 | taskType: Class, 31 | ) { 32 | val inputTask = project.tasks.named(taskName, taskType) 33 | configure { task -> 34 | with(task) { 35 | dependsOn(inputTask) 36 | inputGraph.set(inputTask.get().outputGraph) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphAnalyticsPaths.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.file.DirectoryProperty 4 | import org.gradle.api.file.RegularFile 5 | import org.gradle.api.provider.Provider 6 | 7 | /** 8 | * Helper class to provide paths for the graph analytics plugin. 9 | */ 10 | class GraphAnalyticsPaths( 11 | private val defaultBuildDirectory: DirectoryProperty, 12 | private val graphPersistenceBuildServiceProvider: Provider, 13 | ) { 14 | private val ext by lazy { 15 | graphPersistenceBuildServiceProvider.get().delegate.fileExtension 16 | } 17 | 18 | fun intermediateGraph(id: String, buildDirectory: DirectoryProperty = defaultBuildDirectory): Provider { 19 | return intermediate("$id.$ext", buildDirectory) 20 | } 21 | 22 | private fun intermediate(filename: String, buildDirectory: DirectoryProperty = defaultBuildDirectory): Provider { 23 | return buildDirectory.file("$PLUGIN_BUILD_DIR/intermediate/$filename") 24 | } 25 | 26 | fun reportGraph(id: String, buildDirectory: DirectoryProperty = defaultBuildDirectory): Provider { 27 | return report("$id.$ext", buildDirectory) 28 | } 29 | 30 | fun report(filename: String, buildDirectory: DirectoryProperty = defaultBuildDirectory): Provider { 31 | return buildDirectory.file("$PLUGIN_BUILD_DIR/$filename") 32 | } 33 | 34 | companion object { 35 | private const val PLUGIN_BUILD_DIR = "graphAnalytics" 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphComparisonHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.graph.DefaultDirectedGraph 4 | 5 | /** 6 | * Helper used to create a comparison report between two graph instances. 7 | */ 8 | internal class GraphComparisonHelper { 9 | fun compare( 10 | beforeGraph: DefaultDirectedGraph, 11 | afterGraph: DefaultDirectedGraph, 12 | ): String { 13 | val changes = mutableListOf() 14 | beforeGraph.vertexSet().forEach { baseVertex -> 15 | afterGraph.vertexSet().find { it.path == baseVertex.path }?.let { changedVertex -> 16 | baseVertex.attributes.forEach { (key, baseAttr) -> 17 | changedVertex.attributes[key]?.let { changedAttr -> 18 | changes.add(ComparisonChange( 19 | project = baseVertex.path, 20 | attributeName = key, 21 | attributeType = baseAttr.type, 22 | originalValue = baseAttr.value, 23 | changedValue = changedAttr.value, 24 | )) 25 | } 26 | } 27 | } 28 | } 29 | 30 | val report = buildString { 31 | append("Graph nodes: ${beforeGraph.vertexSet().size} before, ${afterGraph.vertexSet().size} after\n") 32 | append("Graph edges: ${beforeGraph.edgeSet().size} before, ${afterGraph.edgeSet().size} after\n") 33 | append("\n") 34 | 35 | val deltas = changes.filter { it.comparisonResult != 0 } 36 | val deltasByProject = deltas.groupBy { it.project }.toSortedMap() 37 | append("${deltasByProject.keys.size} project(s) reporting a total of ${deltas.size} change(s)\n") 38 | append("\n") 39 | 40 | append("Aggregate metric changes:\n") 41 | val allAttributes = changes.map { it.attributeName }.toSortedSet() 42 | allAttributes.forEach { attributeName -> 43 | val compared = deltas.filter { it.attributeName == attributeName }.map { it.comparisonResult } 44 | val increased = compared.count { it < 0 } 45 | val decreased = compared.count { it > 0 } 46 | val same = compared.count { it == 0 } 47 | append("\t${attributeName}: $decreased decreased, $same stayed the same, $increased increased\n") 48 | 49 | val attrChanges = changes.filter { it.attributeName == attributeName } 50 | val originals = attrChanges.mapNotNull { it.originalAsNumber()?.toDouble() } 51 | val changed = attrChanges.mapNotNull { it.changedAsNumber()?.toDouble() } 52 | if (originals.isNotEmpty() && changed.isNotEmpty()) { 53 | val sumBefore = originals.sum().toInt() 54 | val sumAfter = changed.sum().toInt() 55 | val sumDelta = sumAfter - sumBefore 56 | if (sumDelta != 0) { 57 | append("\t\tsum: $sumBefore -> $sumAfter (delta: $sumDelta / ${percentage(sumBefore, sumDelta)})\n") 58 | } 59 | val minBefore = originals.min().toInt() 60 | val minAfter = changed.min().toInt() 61 | val minDelta = minAfter - minBefore 62 | if (minDelta != 0) { 63 | append("\t\tmin: $minBefore -> $minAfter (delta: $minDelta / ${percentage(minBefore, minDelta)})\n") 64 | } 65 | val maxBefore = originals.max().toInt() 66 | val maxAfter = changed.max().toInt() 67 | val maxDelta = maxAfter - maxBefore 68 | if (maxDelta != 0) { 69 | append("\t\tmax: $maxBefore -> $maxAfter (delta: $maxDelta / ${percentage(maxBefore, maxDelta)})\n") 70 | } 71 | val avgBefore = originals.average().toInt() 72 | val avgAfter = changed.average().toInt() 73 | val avgDelta = avgAfter - avgBefore 74 | if (avgDelta != 0) { 75 | append("\t\taverage: $avgBefore -> $avgAfter (delta: $avgDelta / ${percentage(avgBefore, avgDelta)})\n") 76 | } 77 | } 78 | } 79 | append("\n") 80 | 81 | append("Deltas by project:\n") 82 | deltasByProject.forEach { (project, deltas) -> 83 | append("$project:\n") 84 | deltas.forEach { change -> 85 | append("\t$change\n") 86 | } 87 | } 88 | } 89 | return report 90 | } 91 | 92 | private fun percentage(original: Number, delta: Number): String { 93 | val percentage = ((delta.toDouble() / original.toDouble()) * 100) 94 | return String.format("%1\$.2f%%", percentage) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphExtension.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.plugins.ExtensionAware 4 | import org.gradle.api.provider.ListProperty 5 | import org.gradle.api.provider.Property 6 | import org.gradle.api.tasks.TaskProvider 7 | 8 | /** 9 | * Gradle extension used to configure the [GraphAnalyticsPlugin]. 10 | */ 11 | abstract class GraphExtension : ExtensionAware { 12 | /** 13 | * Specify the classifier implementation used to categorize Gradle configuration classes 14 | * into [ConfigurationClass] categories. 15 | */ 16 | abstract val configurationClassifier: Property 17 | 18 | /** 19 | * Provide a custom project (vertex) attribute collector. 20 | */ 21 | abstract val vertexAttributeCollectors: ListProperty 22 | 23 | /** 24 | * Provide additional graph analysis tasks. These task will be automatically configured 25 | * by the [GraphAnalyticsPlugin] with the following: 26 | * - [GraphPersistenceBuildService] 27 | * - [BaseGraphPersistenceTask.graphFormat] 28 | * - [BaseGraphPersistenceTask.graphVersion] 29 | * - [BaseGraphInputOutputTask.inputGraph] (if not explicitly specified) 30 | * - [BaseGraphInputOutputTask.outputGraph] (if not explicitly specified) 31 | * The results of these tasks will be merged into the final analysis. 32 | */ 33 | abstract val analysisTasks: ListProperty> 34 | 35 | /** 36 | * Provide addition tasks which should be configured to receive the fully constituted 37 | * graph data as an input. 38 | */ 39 | abstract val consumerTasks: ListProperty> 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphPersistence.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.Graph 4 | import org.jgrapht.nio.AttributeType 5 | import java.io.File 6 | 7 | /** 8 | * Interface ued to hide the persistence layer from the rest of the plugin code so that it can 9 | * be easily swapped out. 10 | */ 11 | interface GraphPersistence { 12 | /** 13 | * The file extension to use when persisting a graph of this type. e.g. `.xml` 14 | */ 15 | val fileExtension: String 16 | 17 | /** 18 | * The serialized format revision. Used to invalidate attempts to read versions from the 19 | * cache if the format changes. i.e., when changing the marshalled format, increment this 20 | * number. 21 | */ 22 | val version: Int 23 | 24 | /** 25 | * The set of attribute types tha the format supports. 26 | */ 27 | val supportedAttributeTypes: Set 28 | 29 | /** 30 | * Import a graph from a file. 31 | */ 32 | fun import(graph: Graph, file: File) 33 | 34 | /** 35 | * Export a graph to a file. 36 | */ 37 | fun export(graph: Graph, file: File) 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphPersistenceBuildService.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.services.BuildService 4 | import org.gradle.api.services.BuildServiceParameters 5 | import org.jgrapht.Graph 6 | import java.io.File 7 | 8 | /** 9 | * A service used to share the graph persistence implementation between tasks. 10 | */ 11 | abstract class GraphPersistenceBuildService : BuildService { 12 | internal val delegate: GraphPersistence = GraphPersistenceGraphMl() 13 | 14 | fun import(graph: Graph, file: File) { 15 | delegate.import(graph, file) 16 | } 17 | 18 | fun export(graph: Graph, file: File) { 19 | delegate.export(graph, file) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphPersistenceGexf.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.GradleException 4 | import org.jgrapht.Graph 5 | import org.jgrapht.nio.AttributeType 6 | import org.jgrapht.nio.DefaultAttribute 7 | import org.jgrapht.nio.gexf.GEXFAttributeType 8 | import org.jgrapht.nio.gexf.GEXFExporter 9 | import org.jgrapht.nio.gexf.SimpleGEXFImporter 10 | import java.io.File 11 | 12 | /** 13 | * Persistence implementation that writes GEXF XML files, as per: 14 | * https://gexf.net/ 15 | */ 16 | internal class GraphPersistenceGexf : GraphPersistence { 17 | override val fileExtension = "gexf" 18 | override val version = 1 19 | override val supportedAttributeTypes = setOf( 20 | AttributeType.BOOLEAN, 21 | AttributeType.INT, 22 | AttributeType.LONG, 23 | AttributeType.FLOAT, 24 | AttributeType.DOUBLE, 25 | AttributeType.STRING, 26 | ) 27 | 28 | override fun import(graph: Graph, file: File) { 29 | val importer = SimpleGEXFImporter().apply { 30 | addGraphAttributeConsumer { str, attr -> 31 | if (str == GRAPH_VERSION_ATTR && attr.value != version.toString()) { 32 | // Should never happen since we pass the version in as an input to each task 33 | throw GradleException("Attempting to load unsupported version") 34 | } 35 | } 36 | addVertexAttributeConsumer { pair, attribute -> 37 | // pair.first can be null if the same vertex is added multiple times 38 | if (pair.first == null) return@addVertexAttributeConsumer 39 | 40 | if (!VERTEX_RESERVED_ATTRIBUTES.contains(pair.second.lowercase())) { 41 | pair.first.attributes[pair.second] = attribute 42 | } 43 | } 44 | addEdgeAttributeConsumer { pair, attribute -> 45 | // pair.first can be null if the same edge is added multiple times 46 | if (pair.first == null) return@addEdgeAttributeConsumer 47 | 48 | if (!EDGE_RESERVED_ATTRIBUTES.contains(pair.second.lowercase())) { 49 | pair.first.attributes[pair.second] = attribute 50 | } 51 | } 52 | vertexFactory = java.util.function.Function { str -> 53 | VertexInfo(path = str) 54 | } 55 | } 56 | 57 | importer.importGraph(graph, file) 58 | } 59 | 60 | override fun export(graph: Graph, file: File) { 61 | val vertexKeyMap = mutableMapOf() 62 | graph.vertexSet().map { it.attributes.entries }.forEach { attrMap -> 63 | attrMap.forEach { (key, attr) -> 64 | vertexKeyMap[key] = attr.type 65 | } 66 | } 67 | val edgeKeyMap = mutableMapOf() 68 | graph.edgeSet().map { it.attributes.entries }.forEach { attrMap -> 69 | attrMap.forEach { (key, attr) -> 70 | edgeKeyMap[key] = attr.type 71 | } 72 | } 73 | val exporter = GEXFExporter().apply { 74 | creator = "eBay Project Cost Plugin" 75 | setVertexIdProvider { it.path } 76 | vertexKeyMap.forEach { (key, type) -> 77 | registerAttribute(key, GEXFExporter.AttributeCategory.NODE, toGexfType(type)) 78 | } 79 | edgeKeyMap.forEach { (key, type) -> 80 | registerAttribute(key, GEXFExporter.AttributeCategory.EDGE, toGexfType(type)) 81 | } 82 | setGraphAttributeProvider { 83 | mapOf(GRAPH_VERSION_ATTR to DefaultAttribute(version, AttributeType.INT)) 84 | } 85 | setVertexAttributeProvider { vertexInfo -> 86 | vertexInfo.attributes 87 | } 88 | setEdgeAttributeProvider { edgeInfo -> 89 | edgeInfo.attributes 90 | } 91 | } 92 | exporter.exportGraph(graph, file) 93 | } 94 | 95 | private fun toGexfType(attrType: AttributeType): GEXFAttributeType { 96 | return when(attrType) { 97 | AttributeType.BOOLEAN -> { GEXFAttributeType.BOOLEAN } 98 | AttributeType.INT -> { GEXFAttributeType.INTEGER } 99 | AttributeType.LONG -> { GEXFAttributeType.LONG } 100 | AttributeType.FLOAT -> { GEXFAttributeType.FLOAT } 101 | AttributeType.DOUBLE -> { GEXFAttributeType.DOUBLE } 102 | AttributeType.STRING -> { GEXFAttributeType.STRING } 103 | else -> throw UnsupportedOperationException("Unsupported attribute type: $attrType") 104 | } 105 | } 106 | 107 | companion object { 108 | private const val GRAPH_VERSION_ATTR = "version" 109 | 110 | // From: org.jgrapht.nio.gexf.GEXFExporter.VERTEX_RESERVED_ATTRIBUTES 111 | private val VERTEX_RESERVED_ATTRIBUTES = setOf("id", "label") 112 | 113 | // From: org.jgrapht.nio.gexf.GEXFExporter.EDGE_RESERVED_ATTRIBUTES 114 | private val EDGE_RESERVED_ATTRIBUTES = setOf("id", "type", "label", "source", "target", "weight") 115 | } 116 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphPersistenceGraphMl.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.GradleException 4 | import org.jgrapht.Graph 5 | import org.jgrapht.nio.AttributeType 6 | import org.jgrapht.nio.DefaultAttribute 7 | import org.jgrapht.nio.graphml.GraphMLExporter 8 | import org.jgrapht.nio.graphml.GraphMLImporter 9 | import java.io.File 10 | 11 | /** 12 | * Persistence implementation which writes GraphML XML files, as per: 13 | * http://graphml.graphdrawing.org/ 14 | */ 15 | internal class GraphPersistenceGraphMl : GraphPersistence { 16 | override val fileExtension = "graphml" 17 | override val version = 1 18 | override val supportedAttributeTypes = setOf( 19 | AttributeType.BOOLEAN, 20 | AttributeType.INT, 21 | AttributeType.LONG, 22 | AttributeType.FLOAT, 23 | AttributeType.DOUBLE, 24 | AttributeType.STRING, 25 | ) 26 | 27 | override fun import(graph: Graph, file: File) { 28 | val importer = GraphMLImporter().apply { 29 | addGraphAttributeConsumer { str, attr -> 30 | if (str == GRAPH_VERSION_ATTR && attr.value != version.toString()) { 31 | // Should never happen since we pass the version in as an input to each task 32 | throw GradleException("Attempting to load unsupported version") 33 | } 34 | } 35 | addVertexAttributeConsumer { pair, str -> 36 | // pair.first can be null if the same vertex is added multiple times 37 | if (pair.first == null) return@addVertexAttributeConsumer 38 | 39 | if (pair.second == "ID") { 40 | pair.first.path = str.value 41 | } else { 42 | pair.first.attributes[pair.second] = str 43 | } 44 | } 45 | addEdgeAttributeConsumer { pair, str -> 46 | // pair.first can be null if the same edge is added multiple times 47 | if (pair.first == null) return@addEdgeAttributeConsumer 48 | 49 | pair.first.attributes[pair.second] = str 50 | } 51 | vertexFactory = java.util.function.Function { str -> 52 | VertexInfo(path = str) 53 | } 54 | } 55 | 56 | importer.importGraph(graph, file) 57 | } 58 | 59 | override fun export(graph: Graph, file: File) { 60 | val vertexKeyMap = mutableMapOf() 61 | graph.vertexSet().map { it.attributes.entries }.forEach { attrMap -> 62 | attrMap.forEach { (key, attr) -> 63 | vertexKeyMap[key] = attr.type 64 | } 65 | } 66 | val edgeKeyMap = mutableMapOf() 67 | graph.edgeSet().map { it.attributes.entries }.forEach { attrMap -> 68 | attrMap.forEach { (key, attr) -> 69 | edgeKeyMap[key] = attr.type 70 | } 71 | } 72 | val exporter = GraphMLExporter().apply { 73 | setVertexIdProvider { it.path } 74 | vertexKeyMap.forEach { (key, type) -> 75 | registerAttribute(key, GraphMLExporter.AttributeCategory.NODE, type) 76 | } 77 | edgeKeyMap.forEach { (key, type) -> 78 | registerAttribute(key, GraphMLExporter.AttributeCategory.EDGE, type) 79 | } 80 | setGraphAttributeProvider { 81 | mapOf(GRAPH_VERSION_ATTR to DefaultAttribute(version, AttributeType.INT)) 82 | } 83 | setVertexIdProvider { vertexInfo -> 84 | vertexInfo.path 85 | } 86 | setVertexAttributeProvider { vertexInfo -> 87 | vertexInfo.attributes 88 | } 89 | setEdgeAttributeProvider { edgeInfo -> 90 | edgeInfo.attributes 91 | } 92 | } 93 | exporter.exportGraph(graph, file) 94 | } 95 | 96 | companion object { 97 | const val GRAPH_VERSION_ATTR = "version" 98 | } 99 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphRelation.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | /** 4 | * Relationship between two vertices and and edge. 5 | */ 6 | internal data class GraphRelation( 7 | val from: String, 8 | val to: String, 9 | val edge: EdgeInfo 10 | ) : java.io.Serializable 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/GraphUtils.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.Graph 4 | 5 | /** 6 | * Internal helper to merge two graphs together, preserving attributes. 7 | */ 8 | internal object GraphUtils { 9 | fun Graph.merge(other: Graph) { 10 | other.vertexSet().forEach { vertex -> 11 | mergeVertex(vertex) 12 | } 13 | other.edgeSet().forEach { otherEdge -> 14 | val source = mergeVertex(other.getEdgeSource(otherEdge)) 15 | val target = mergeVertex(other.getEdgeTarget(otherEdge)) 16 | val removedEdge = removeEdge(source, target) 17 | addEdge(source, target).apply { 18 | removedEdge?.let { merge(it) } 19 | merge(otherEdge) 20 | } 21 | } 22 | } 23 | 24 | private fun Graph.mergeVertex( 25 | vertex: VertexInfo 26 | ): VertexInfo { 27 | return if (addVertex(vertex)) { 28 | vertex 29 | } else { 30 | vertexSet().find { it.hashCode() == vertex.hashCode() }!!.merge(vertex) 31 | } 32 | } 33 | 34 | private fun VertexInfo.merge(other: VertexInfo) = apply { 35 | require(path == other.path) 36 | attributes.putAll(other.attributes) 37 | } 38 | 39 | private fun EdgeInfo.merge(other: EdgeInfo) = apply { 40 | attributes.putAll(other.attributes) 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/InspectionTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.file.RegularFileProperty 5 | import org.gradle.api.provider.Property 6 | import org.gradle.api.tasks.CacheableTask 7 | import org.gradle.api.tasks.Input 8 | import org.gradle.api.tasks.Optional 9 | import org.gradle.api.tasks.OutputFile 10 | import org.gradle.api.tasks.options.Option 11 | import org.jgrapht.graph.DefaultDirectedGraph 12 | import org.jgrapht.nio.AttributeType 13 | import java.time.Duration 14 | import java.time.Instant 15 | 16 | /** 17 | * Task to create a project-specific inspection report for use in helping to investigate/expose potentially hidden 18 | * costs associated with the project definition. 19 | */ 20 | @CacheableTask 21 | internal abstract class InspectionTask : BaseGraphInputTask() { 22 | @get:Input 23 | internal abstract val selfInfoProp: Property 24 | 25 | @get:Input 26 | @set:Option(option = "project", description = "Project to report upon") 27 | @get:Optional // By default we use the project that the task was run against 28 | internal abstract var projectPath: String? 29 | 30 | @get:OutputFile 31 | internal abstract val outputFile: RegularFileProperty 32 | 33 | override fun processInputGraph(graph: DefaultDirectedGraph) { 34 | val targetProject = projectPath ?: selfInfoProp.get().path 35 | 36 | val selfInfo = graph.vertexSet().find { it.path == targetProject } 37 | ?: throw GradleException("Project path not found in graph: $targetProject") 38 | 39 | val report = buildString { 40 | appendLine("Project inspection report for: $targetProject ${selfInfo.attributes}") 41 | appendLine() 42 | 43 | dependencyCyclesReport(graph, selfInfo) 44 | topNodesReport(graph, selfInfo) 45 | 46 | append("Dependency tree:\n") 47 | addDependency(graph = graph, indent = "", vertex = selfInfo) 48 | append("* Indicates a vertex that has already been rendered\n") 49 | }.replace("\n", System.lineSeparator()) 50 | 51 | outputFile.asFile.get().writeText(report) 52 | logger.lifecycle("Project inspection analysis comparison report available at: file://${outputFile.asFile.get()}") 53 | } 54 | 55 | /** 56 | * Builds up a list of cycles by performing a depth-first traversal of the dependency edges. 57 | */ 58 | private fun detectCycles( 59 | graph: DefaultDirectedGraph, 60 | projectNode: VertexInfo, 61 | currentNode: VertexInfo, 62 | stopTime: Instant, 63 | detectedCycles: MutableList> = mutableListOf(), 64 | traversedPath: List = listOf(currentNode) 65 | ): List> { 66 | // Truncate the results if the graph is excessive: 67 | if (Instant.now().isAfter(stopTime)) { 68 | return emptyList() 69 | } 70 | 71 | graph.outgoingEdgesOf(currentNode).map { edge -> 72 | graph.getEdgeTarget(edge) 73 | }.sortedBy { 74 | it.path 75 | }.forEach { node -> 76 | val updatedTraversedPath = traversedPath + node 77 | if (node == projectNode) { 78 | detectedCycles.add(updatedTraversedPath) 79 | } else if (!traversedPath.contains(node)) { 80 | detectCycles(graph, projectNode, node, stopTime, detectedCycles, updatedTraversedPath) 81 | } 82 | } 83 | return if (traversedPath.size == 1) { 84 | detectedCycles.toList() 85 | } else { 86 | // Work avoidance 87 | emptyList() 88 | } 89 | } 90 | 91 | /** 92 | * Renders a report detailing any project dependency cycles that may be found for the target project. 93 | */ 94 | private fun StringBuilder.dependencyCyclesReport( 95 | graph: DefaultDirectedGraph, 96 | selfInfo: VertexInfo, 97 | ) { 98 | val stopTime = Instant.now().plus(MAX_TRAVERSAL_DURATION) 99 | val detectedCycles = detectCycles(graph, selfInfo, selfInfo, stopTime = stopTime) 100 | if (Instant.now().isAfter(stopTime)) { 101 | appendLine("WARNING: Project graph traversal has exceeded the maximum duration of $MAX_TRAVERSAL_DURATION.") 102 | appendLine(" Cycle detection will be incomplete. This can happen when the module being inspected") 103 | appendLine(" has a very large dependency graph. Consider breaking up the module into a set of smaller,") 104 | appendLine(" more cohesive modules.") 105 | } 106 | if (detectedCycles.isEmpty()) { 107 | appendLine("No project dependency cycles involving ${selfInfo.path} were detected.") 108 | } else { 109 | appendLine("${detectedCycles.size} project dependency cycle(s) involving ${selfInfo.path} were detected:") 110 | detectedCycles.sortedByDescending { it.size }.forEachIndexed { index, cycle -> 111 | val cycleString = cycle.joinToString(separator = " --> ") { it.path } 112 | appendLine("${index + 1}: $cycleString") 113 | } 114 | 115 | val pathToCount = mutableMapOf() 116 | detectedCycles.flatten().forEach { node -> 117 | pathToCount.compute(node.path) { _, existing -> (existing ?: 0) + 1 } 118 | } 119 | pathToCount.remove(selfInfo.path) 120 | appendLine() 121 | appendLine("Projects involved in cycles, by frequency of occurrence:") 122 | pathToCount.keys.sortedByDescending { pathToCount[it] }.forEach { path -> 123 | appendLine("\t${pathToCount[path]}: $path") 124 | } 125 | } 126 | appendLine() 127 | appendLine() 128 | } 129 | 130 | /** 131 | * Generates a list of all dependencies, direct and transitive, of the target project. 132 | */ 133 | private fun computeAllDependencies( 134 | graph: DefaultDirectedGraph, 135 | currentNode: VertexInfo, 136 | processedVertices: MutableSet = mutableSetOf() 137 | ): Set { 138 | graph.outgoingEdgesOf(currentNode).forEach { edge -> 139 | val node = graph.getEdgeTarget(edge) 140 | if (!processedVertices.contains(node)) { 141 | processedVertices.add(currentNode) 142 | computeAllDependencies(graph, node, processedVertices) 143 | } 144 | } 145 | return processedVertices 146 | } 147 | 148 | /** 149 | * Renders a series of reports detailing each numeric attribute/metric. Each individual report is 150 | * rendered by [topNodesReportForMetric]. 151 | */ 152 | private fun StringBuilder.topNodesReport( 153 | graph: DefaultDirectedGraph, 154 | projectNode: VertexInfo, 155 | ) { 156 | val allDependencies = computeAllDependencies(graph, projectNode) 157 | 158 | val allNumericAttrNames = allDependencies.flatMap { vertexInfo -> 159 | vertexInfo.attributes.entries 160 | }.filter { (_, attr) -> 161 | when (attr.type) { 162 | AttributeType.INT -> true 163 | AttributeType.LONG -> true 164 | AttributeType.DOUBLE -> true 165 | AttributeType.FLOAT -> true 166 | else -> false 167 | } 168 | }.map { (key, _) -> key }.toSortedSet() 169 | 170 | allNumericAttrNames.forEach { attrName -> 171 | topNodesReportForMetric(allDependencies, attrName) 172 | } 173 | } 174 | 175 | /** 176 | * Renders a report detailing a specific numeric attribute/metric, listing the top N nodes by metric value. 177 | */ 178 | private fun StringBuilder.topNodesReportForMetric( 179 | allDependencies: Set, 180 | attrName: String 181 | ) { 182 | val metricValues = allDependencies.mapNotNull { node -> 183 | node.attributes[attrName]?.value?.toDoubleOrNull()?.let { 184 | Pair(node, it) 185 | } 186 | }.sortedByDescending { (_, value) -> 187 | value 188 | }.take(TOP_N).map { it.first } 189 | 190 | if (metricValues.isNotEmpty()) { 191 | appendLine("Top $TOP_N dependencies by '$attrName':") 192 | metricValues.forEachIndexed { index, node -> 193 | appendLine("\t${index + 1}: ${node.attributes[attrName]} -- ${node.path} ${node.attributes}") 194 | } 195 | appendLine() 196 | appendLine() 197 | } 198 | } 199 | 200 | private fun StringBuilder.addDependency( 201 | graph: DefaultDirectedGraph, 202 | indent: String, 203 | vertex: VertexInfo, 204 | processedVertices: MutableSet = mutableSetOf() 205 | ) { 206 | val nextIndent = if (indent.isEmpty()) { 207 | " --> " 208 | } else { 209 | " $indent" 210 | } 211 | append(indent) 212 | append(vertex.path) 213 | if (processedVertices.add(vertex)) { 214 | append(" (") 215 | var firstAttribute = true 216 | vertex.attributes.forEach { (name, value) -> 217 | if (!firstAttribute) append(", ") 218 | append(name) 219 | append("=") 220 | append(value) 221 | firstAttribute = false 222 | } 223 | append(")\n") 224 | 225 | graph.outgoingEdgesOf(vertex).forEach { edge -> 226 | val target = graph.getEdgeTarget(edge) 227 | // Only process outbound edges 228 | if (target != vertex) { 229 | addDependency(graph, nextIndent, target, processedVertices) 230 | } 231 | } 232 | } else { 233 | append(" *\n") 234 | } 235 | } 236 | 237 | companion object { 238 | private const val TOP_N = 10 239 | private val MAX_TRAVERSAL_DURATION = Duration.ofMinutes(5) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/NetworkExpansionAnalysisTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.tasks.CacheableTask 5 | import org.jgrapht.graph.DefaultDirectedGraph 6 | import org.jgrapht.nio.AttributeType 7 | import org.jgrapht.nio.DefaultAttribute 8 | 9 | /** 10 | * This task analyzes the graph and calculates network size information as well as the network 11 | * expansion factor for each vertex. 12 | */ 13 | @CacheableTask 14 | internal abstract class NetworkExpansionAnalysisTask : BaseGraphInputOutputTask() { 15 | override fun processGraph(graph: DefaultDirectedGraph) { 16 | val networkAbove by lazy { ReachableNodesScoringAlgorithm(graph, ReachableNodesDirection.INCOMING) } 17 | val networkBelow by lazy { ReachableNodesScoringAlgorithm(graph, ReachableNodesDirection.OUTGOING) } 18 | 19 | graph.vertexSet().forEach { vertexInfo -> 20 | val networkAboveValue = networkAbove.getVertexScore(vertexInfo) 21 | val networkBelowValue = networkBelow.getVertexScore(vertexInfo) 22 | 23 | // This data comes from the [BasicGraphAnalysisTask] and is required to be present. This task must 24 | // depend upon that task in order to ensure this data will exist. 25 | val inDegree = vertexInfo.attributes["inDegree"]?.value?.toIntOrNull() 26 | ?: throw GradleException("Unable to load 'inDegree' attribute for vertex ${vertexInfo.path}. " + 27 | "Does this task dependOn the BasicGraphAnalysisTask?") 28 | 29 | vertexInfo.attributes["networkAbove"] = DefaultAttribute(networkAboveValue, AttributeType.INT) 30 | vertexInfo.attributes["networkBelow"] = DefaultAttribute(networkBelowValue, AttributeType.INT) 31 | vertexInfo.attributes["expansionFactor"] = DefaultAttribute(networkBelowValue * inDegree, AttributeType.INT) 32 | } 33 | } 34 | 35 | companion object { 36 | const val TASK_NAME = "networkExpansionAnalysis" 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/ReachableNodesDirection.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | /** 4 | * Edge direction to use when building the subgraph. 5 | */ 6 | enum class ReachableNodesDirection { 7 | INCOMING, 8 | OUTGOING, 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/ReachableNodesScoringAlgorithm.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.Graph 4 | import org.jgrapht.alg.interfaces.VertexScoringAlgorithm 5 | import java.util.Queue 6 | import java.util.concurrent.LinkedBlockingQueue 7 | 8 | /** 9 | * Determines the size of the subgraph visible by a vertex.. 10 | */ 11 | class ReachableNodesScoringAlgorithm( 12 | private val graph: Graph, 13 | edgeDirection: ReachableNodesDirection, 14 | ) : VertexScoringAlgorithm { 15 | private val scores = mutableMapOf() 16 | 17 | private val edgesOf: (V) -> Set = when(edgeDirection) { 18 | ReachableNodesDirection.INCOMING -> graph::incomingEdgesOf 19 | ReachableNodesDirection.OUTGOING -> graph::outgoingEdgesOf 20 | } 21 | 22 | private val subjectOf: (E) -> V = when(edgeDirection) { 23 | ReachableNodesDirection.INCOMING -> graph::getEdgeSource 24 | ReachableNodesDirection.OUTGOING -> graph::getEdgeTarget 25 | } 26 | 27 | override fun getScores(): Map { 28 | return scores.toMap() 29 | } 30 | 31 | override fun getVertexScore(v: V): Int { 32 | require(graph.containsVertex(v)) { "Cannot return score of unknown vertex" } 33 | 34 | val alreadyComputed = scores[v] 35 | if (alreadyComputed != null) return alreadyComputed 36 | 37 | return compute(v).also { 38 | scores[v] = it 39 | } 40 | } 41 | 42 | private fun compute(vertex: V): Int { 43 | val processedVertices: MutableSet = mutableSetOf(vertex) 44 | val remainingVertices: Queue = LinkedBlockingQueue().apply { add(vertex) } 45 | var count = 0 46 | do { 47 | count++ 48 | val v = remainingVertices.remove() 49 | edgesOf.invoke(v).forEach { edge -> 50 | val target = subjectOf.invoke(edge) 51 | if (processedVertices.add(target)) { 52 | remainingVertices.add(target) 53 | } 54 | } 55 | } while (remainingVertices.isNotEmpty()) 56 | 57 | return count 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/VertexAttributeCollector.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | /** 4 | * Collector which can generate vertex information at configuration time. This work must be 5 | * very lightweight but gives an opportunity to collect information from or about installed 6 | * plugins, etc. 7 | * 8 | * The collected information is applied and aggregated prior to the analysis phase. 9 | */ 10 | interface VertexAttributeCollector { 11 | fun collectConfigurationTimeAttributes(vertexInfo: VertexInfo) 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/VertexHeightAnalysisTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.gradle.api.tasks.CacheableTask 4 | import org.jgrapht.graph.DefaultDirectedGraph 5 | import org.jgrapht.nio.AttributeType 6 | import org.jgrapht.nio.DefaultAttribute 7 | 8 | /** 9 | * Analysis task which calculates the vertex height for all vertices in the graph. 10 | */ 11 | @CacheableTask 12 | abstract class VertexHeightAnalysisTask : BaseGraphInputOutputTask() { 13 | override fun processGraph(graph: DefaultDirectedGraph) { 14 | val height by lazy { VertexHeightScoringAlgorithm(graph) } 15 | 16 | graph.vertexSet().forEach { vertexInfo -> 17 | vertexInfo.attributes["height"] = DefaultAttribute(height.getVertexScore(vertexInfo), AttributeType.INT) 18 | } 19 | } 20 | 21 | companion object { 22 | const val TASK_NAME = "graphVertexHeightAnalysis" 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/VertexHeightScoringAlgorithm.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.Graph 4 | import org.jgrapht.alg.interfaces.VertexScoringAlgorithm 5 | import java.util.Queue 6 | import java.util.concurrent.LinkedBlockingQueue 7 | 8 | /** 9 | * Vertex height scoring algorithm. 10 | */ 11 | class VertexHeightScoringAlgorithm( 12 | private val graph: Graph, 13 | ) : VertexScoringAlgorithm { 14 | private val scores = mutableMapOf() 15 | 16 | override fun getScores(): Map { 17 | return scores.toMap() 18 | } 19 | 20 | override fun getVertexScore(v: VertexInfo): Int { 21 | require(graph.containsVertex(v)) { "Cannot return score of unknown vertex" } 22 | 23 | val alreadyComputed = scores[v] 24 | if (alreadyComputed != null) return alreadyComputed 25 | 26 | return compute(v).also { 27 | scores[v] = it 28 | } 29 | } 30 | 31 | private fun compute(vertex: VertexInfo): Int { 32 | val processedVertices: MutableSet = mutableSetOf() 33 | val remainingVertices: Queue = LinkedBlockingQueue().apply { add(vertex) } 34 | var height = 0 35 | while(remainingVertices.isNotEmpty()) { 36 | height++ 37 | val toProcess = remainingVertices.toMutableSet() 38 | remainingVertices.clear() 39 | 40 | toProcess.forEach { v -> 41 | processedVertices.add(v) 42 | graph.edgesOf(v).forEach { edge -> 43 | val target = graph.getEdgeTarget(edge) 44 | // Only process outbound edges 45 | if (target != v) { 46 | if (!processedVertices.contains(target)) { 47 | remainingVertices.add(target) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | return height 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/VertexInfo.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.jgrapht.nio.Attribute 4 | import java.io.Serializable 5 | 6 | /** 7 | * Class which collects information about a single graph vertex. This class must be 8 | * mutable due to assumptions made by the jgrapht persistence layer. The mutability then 9 | * causes issues with map lookups so only the identifying fields are included in 10 | * [equals] and [hashCode] calculations. 11 | */ 12 | class VertexInfo( 13 | override var attributes: MutableMap = mutableMapOf(), 14 | var path: String, 15 | ) : Attributed, Serializable { 16 | override fun equals(other: Any?): Boolean { 17 | if (this === other) return true 18 | if (javaClass != other?.javaClass) return false 19 | 20 | other as VertexInfo 21 | 22 | if (path != other.path) return false 23 | 24 | return true 25 | } 26 | 27 | /** 28 | * NOTE: The hashCode must remain constant based upon node ID only. Ths is due to the 29 | * fact that the jgrapht persistence layer creates an inserts the node into a `Map` prior to 30 | * updating it with attribute data. 31 | */ 32 | override fun hashCode(): Int { 33 | return path.hashCode() 34 | } 35 | 36 | override fun toString(): String { 37 | return "VertexInfo(path=$path, attributes=$attributes)" 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/GraphValidationExtension.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation 2 | 3 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatcher 4 | import org.gradle.api.provider.ListProperty 5 | import org.gradle.api.provider.MapProperty 6 | 7 | /** 8 | * Gradle extension for configuring [GraphValidationRule]s. 9 | */ 10 | abstract class GraphValidationExtension { 11 | /** 12 | * List of project paths to use as analysis inputs. Default value: The project path of the project being validated. 13 | * 14 | * This is useful when the project verification needs to take into account a holistic graph 15 | * (e.g., from an application module's perspective) and not just a graph containing information 16 | * from itself and its dependencies. 17 | * 18 | * Each project added to this list will be individually validated against the rules. The task will 19 | * fail if any project fails validation. 20 | * 21 | * If a project graph does not contain the project being validated, the task will ignore the verification 22 | * rules for that project and the task will succeed. 23 | */ 24 | abstract val validatedProjects: ListProperty 25 | 26 | /** 27 | * Map of Rule ID to rule definition. These rules would typically be applied by a convention plugin and 28 | * describe the overall project requirements. 29 | */ 30 | abstract val rules: MapProperty 31 | 32 | /** 33 | * Map of Rule ID to a custom rule definition for the project module. This can be used to - for example - 34 | * modify a threshold for a specific project. 35 | */ 36 | abstract val ruleOverrides: MapProperty> 37 | 38 | /** 39 | * List of Rule IDs to ignore. 40 | */ 41 | abstract val ignore: ListProperty 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/GraphValidationResult.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation 2 | 3 | /** 4 | * The result of validating an individual project graph. 5 | */ 6 | internal sealed interface GraphValidationResult { 7 | val graphId: String 8 | } 9 | 10 | internal data class GraphVertexNotFound( 11 | override val graphId: String 12 | ): GraphValidationResult 13 | 14 | internal data class GraphValidation( 15 | override val graphId: String, 16 | val rootedVertex: RootedVertex, 17 | val violations: Map = emptyMap(), 18 | val ignoredViolations: Map = emptyMap(), 19 | val ignoredButValid: List = emptyList(), 20 | ) : GraphValidationResult -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/GraphValidationRule.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation 2 | 3 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatcher 4 | 5 | /** 6 | * A rule which describes a violation of standards. 7 | */ 8 | data class GraphValidationRule( 9 | /** 10 | * The reason why this rule is important to the developer. This will be used in the report to guide them 11 | * to a correct factoring. 12 | */ 13 | val reason: String, 14 | 15 | /** 16 | * Determines whether or not the provided [RootedVertex] is in violation of this rule. 17 | */ 18 | val matcher: GraphMatcher, 19 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/GraphValidationTask.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation 2 | 3 | import com.ebay.plugins.graph.analytics.BaseGraphPersistenceTask 4 | import com.ebay.plugins.graph.analytics.EdgeInfo 5 | import com.ebay.plugins.graph.analytics.VertexInfo 6 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatcher 7 | import org.gradle.api.GradleException 8 | import org.gradle.api.file.ConfigurableFileCollection 9 | import org.gradle.api.file.DirectoryProperty 10 | import org.gradle.api.file.RegularFileProperty 11 | import org.gradle.api.provider.ListProperty 12 | import org.gradle.api.provider.MapProperty 13 | import org.gradle.api.provider.Property 14 | import org.gradle.api.tasks.CacheableTask 15 | import org.gradle.api.tasks.Input 16 | import org.gradle.api.tasks.InputFiles 17 | import org.gradle.api.tasks.Internal 18 | import org.gradle.api.tasks.OutputFile 19 | import org.gradle.api.tasks.PathSensitive 20 | import org.gradle.api.tasks.PathSensitivity 21 | import org.gradle.api.tasks.TaskAction 22 | import org.jgrapht.graph.DefaultDirectedGraph 23 | 24 | /** 25 | * Gradle task used to perform the project analysis to determine if there are any violations 26 | * of the configured rules. 27 | */ 28 | @CacheableTask 29 | abstract class GraphValidationTask : BaseGraphPersistenceTask() { 30 | /** 31 | * The project path of the project being validated. 32 | */ 33 | @get:Input 34 | abstract val projectPathProp: Property 35 | 36 | /** 37 | * A collection of project analysis graphs to validate against. 38 | */ 39 | @get:InputFiles 40 | @get:PathSensitive(PathSensitivity.NONE) 41 | abstract val inputGraphs: ConfigurableFileCollection 42 | 43 | /** 44 | * List of Rule IDs to ignore. 45 | */ 46 | @get:Input 47 | abstract val ignoredRulesProp: ListProperty 48 | 49 | /** 50 | * The [rules] field is `@Internal` since `GraphValidationRule` is not a serializable type. In order to 51 | * force the cache to be invalidated when the rules change, we accept string representation of the defined 52 | * rules. 53 | */ 54 | @get:Input 55 | abstract val definedRules: ListProperty 56 | 57 | /** 58 | * The [ruleOverrides] field is `@Internal` since `GraphMatcher` is not a serializable type. In order to 59 | * force the cache to be invalidated when the overridden rules change, we accept a string representation 60 | * of the defined overrides. 61 | */ 62 | @get:Input 63 | abstract val definedRuleOverrides: ListProperty 64 | 65 | /** 66 | * Textual report output file. 67 | */ 68 | @get:OutputFile 69 | abstract val outputFile: RegularFileProperty 70 | 71 | /** 72 | * The absolute path to the root project. This is used to remove this prefix from the input graph paths 73 | * when rendering the report. 74 | */ 75 | @get:Internal 76 | abstract val rootProjectPath: DirectoryProperty 77 | 78 | /** 79 | * The rules to evaluate when validating each project graph. 80 | */ 81 | @get:Internal 82 | abstract val rules: MapProperty 83 | 84 | /** 85 | * Graph matcher which should be used in place of the rules' defined matchers. This allows for 86 | * threshold-based rules to be overridden on a per-project basis. 87 | */ 88 | @get:Internal 89 | abstract val ruleOverrides: MapProperty> 90 | 91 | /** 92 | * Processes each of the analysis input graphs, validating them against the configured rules. 93 | */ 94 | @TaskAction 95 | fun validateGraphs() { 96 | val rootDir = rootProjectPath.get().asFile 97 | val persistence = persistenceBuildService.get() 98 | val validationResults = inputGraphs.files.map { inputGraph -> 99 | val graph = DefaultDirectedGraph(EdgeInfo::class.java) 100 | persistence.import(graph, inputGraph) 101 | val graphId = inputGraph.toRelativeString(rootDir) 102 | validateGraph(graphId, graph) 103 | } 104 | 105 | val totalReport = validationResults.joinToString(separator = "\n") { validationResult -> 106 | buildReportString(validationResult) 107 | }.replace("\n", System.lineSeparator()) 108 | outputFile.asFile.get().writeText(totalReport) 109 | 110 | val errors = validationResults.filterIsInstance() 111 | .map { validation -> 112 | validation.violations.size + validation.ignoredButValid.size 113 | } 114 | .filter { it > 0 } 115 | val errorCount = if (errors.isEmpty()) { 116 | 0 117 | } else { 118 | errors.reduce { acc, i -> acc + i } 119 | } 120 | 121 | if (errorCount > 0) { 122 | logger.error(totalReport) 123 | throw GradleException("$errorCount graph validation error(s) detected. " + 124 | "Please see the task's console output for more details.") 125 | } 126 | } 127 | 128 | /** 129 | * Validates the provided graph against the configured rules, returning a validation result summary. 130 | */ 131 | private fun validateGraph(graphId: String, graph: DefaultDirectedGraph): GraphValidationResult { 132 | val vertexInfo = graph.vertexSet().find { it.path == projectPathProp.get() } 133 | ?: return GraphVertexNotFound(graphId = graphId) 134 | 135 | val rootedVertex = RootedVertex(graph, vertexInfo) 136 | 137 | val rulesWithOverridesApplied = rules.get().mapValues { (id, rule) -> 138 | ruleOverrides.get()[id]?.let { override -> 139 | GraphValidationRule( 140 | "This rule is a project module- specific override. Look for its definition in the\n" + 141 | "project module's `build.gradle.kts` file", 142 | override 143 | ) 144 | } ?: rule 145 | } 146 | 147 | val violations = rulesWithOverridesApplied.filter { (_, rule) -> 148 | rule.matcher.matches(rootedVertex).matched 149 | } 150 | val allIgnoredRules = ignoredRulesProp.get() 151 | val ignoredViolations = violations.filter { (id, _) -> id in allIgnoredRules } 152 | val unIgnoredViolations = violations.filterNot { (id, _) -> id in allIgnoredRules } 153 | val ignoresWithoutViolations = allIgnoredRules.filterNot { ignoredViolations.containsKey(it) } 154 | 155 | return GraphValidation( 156 | graphId = graphId, 157 | rootedVertex = rootedVertex, 158 | violations = unIgnoredViolations.toMap(), 159 | ignoredViolations = ignoredViolations, 160 | ignoredButValid = ignoresWithoutViolations, 161 | ) 162 | } 163 | 164 | /** 165 | * Build a report string for the provided validation result. 166 | */ 167 | private fun buildReportString(validationResult: GraphValidationResult) = buildString { 168 | appendLine("=== Validation using graph analysis: ${validationResult.graphId}") 169 | appendLine() 170 | val summary = when(validationResult) { 171 | is GraphVertexNotFound -> "Project path ${projectPathProp.get()} not found in graph. Validation skipped." 172 | is GraphValidation -> buildValidationReport(validationResult) 173 | } 174 | appendLine(summary) 175 | } 176 | 177 | /** 178 | * Builds a report string for an individual validation result. 179 | */ 180 | private fun buildValidationReport(validation: GraphValidation) = buildString { 181 | if (validation.violations.isNotEmpty()) { 182 | appendLine("ERROR: ${validation.violations.size} rule(s) violations found:") 183 | validation.violations.forEach { (id, rule) -> 184 | appendLine("Rule: $id") 185 | appendLine(" Description:") 186 | appendLine(rule.reason.prependIndent(" ")) 187 | appendLine(" Details:") 188 | val match = rule.matcher.matches(validation.rootedVertex) 189 | appendLine(match.render(onlyMatches = true, indent = " ")) 190 | } 191 | } 192 | if (validation.ignoredButValid.isNotEmpty()) { 193 | appendLine("ERROR: Ignored rule(s) which did not have any violations:") 194 | validation.ignoredButValid.forEach { appendLine(" $it") } 195 | } 196 | if (validation.ignoredViolations.isNotEmpty()) { 197 | appendLine("INFO ${validation.ignoredViolations.size} ignored rule violation(s) found:") 198 | appendLine(validation.ignoredViolations.keys.joinToString(separator = "\n", prefix = " ")) 199 | } 200 | if (validation.violations.isEmpty() 201 | && validation.ignoredButValid.isEmpty() 202 | && validation.ignoredViolations.isEmpty()) { 203 | appendLine("SUCCESS: No graph issues found.") 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/RootedEdge.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation 2 | 3 | import com.ebay.plugins.graph.analytics.Attributed 4 | import com.ebay.plugins.graph.analytics.EdgeInfo 5 | import com.ebay.plugins.graph.analytics.VertexInfo 6 | import org.jgrapht.graph.DefaultDirectedGraph 7 | import org.jgrapht.nio.Attribute 8 | 9 | /** 10 | * A tuple of a [VertexInfo] representing the root vertex being analyzed for volations and 11 | * an [EdgeInfo] representing the edge being immediately considered for match (e.g., a depenency 12 | * upon another vertex). This allows for more complex rules to be created which consider the overall scope. 13 | */ 14 | class RootedEdge( 15 | val graph: DefaultDirectedGraph, 16 | val root: VertexInfo, 17 | val edge: EdgeInfo, 18 | ): Attributed, Summarized { 19 | /** 20 | * Expose the attributes of the underlying edge 21 | */ 22 | override var attributes: MutableMap = edge.attributes 23 | 24 | override fun toString(): String { 25 | return edge.toString() 26 | } 27 | 28 | override fun getSummary(): String { 29 | val source = graph.getEdgeSource(edge) 30 | val target = graph.getEdgeTarget(edge) 31 | return "${source.path} -> ${target.path}" 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/RootedVertex.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation 2 | 3 | import com.ebay.plugins.graph.analytics.Attributed 4 | import com.ebay.plugins.graph.analytics.EdgeInfo 5 | import com.ebay.plugins.graph.analytics.VertexInfo 6 | import org.jgrapht.graph.DefaultDirectedGraph 7 | import org.jgrapht.nio.Attribute 8 | 9 | /** 10 | * A tuple of a [VertexInfo] representing the root vertex being analyzed for volations and 11 | * an [VertexInfo] representing the vertex being immediately considered for match (e.g., a child 12 | * vertex). This allows for more complex rules to be created which consider the overall scope. 13 | */ 14 | class RootedVertex( 15 | val graph: DefaultDirectedGraph, 16 | val root: VertexInfo, 17 | val vertex: VertexInfo = root, 18 | ) : Attributed, Summarized { 19 | /** 20 | * Expose the attributes of the underlying vertex 21 | */ 22 | override var attributes: MutableMap = vertex.attributes 23 | 24 | override fun toString(): String { 25 | return vertex.toString() 26 | } 27 | 28 | override fun getSummary(): String { 29 | return vertex.path 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/Summarized.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation 2 | 3 | /** 4 | * Interface which is applied to objects which can be summarized into a simpler form 5 | * that will be more readily understood by a human in a report. 6 | */ 7 | interface Summarized { 8 | fun getSummary(): String 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AllOfGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which matches only if all delegate rules match the input. 5 | */ 6 | internal class AllOfGraphMatcher( 7 | private val delegates: Iterable> 8 | ) : GraphMatcher { 9 | override fun matches(value: T): DescribedMatch { 10 | val delegateResults = delegates.map { delegate -> 11 | delegate.matches(value) 12 | } 13 | val result = delegateResults.all { it.matched } 14 | return DescribedMatch( 15 | actual = value::toString, 16 | description = value.summarizedDescription("all of"), 17 | matched = result, 18 | subResults = delegateResults, 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AnyOfGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which matches only if any of the delegate rules match the input. 5 | */ 6 | internal class AnyOfGraphMatcher( 7 | private val delegates: Iterable> 8 | ) : GraphMatcher { 9 | override fun matches(value: T): DescribedMatch { 10 | // Evaluate all delegates to get a complete description 11 | val delegateResults = delegates.map { delegate -> 12 | delegate.matches(value) 13 | } 14 | val result = delegateResults.any { it.matched } 15 | return DescribedMatch( 16 | actual = value::toString, 17 | description = value.summarizedDescription("any of"), 18 | matched = result, 19 | subResults = delegateResults, 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AttributeBooleanGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.Attributed 4 | import org.jgrapht.nio.Attribute 5 | 6 | /** 7 | * Matcher which extracts the specified `Boolean` attribute, delegating the match of the 8 | * value to the specified matcher. 9 | */ 10 | internal class AttributeBooleanGraphMatcher( 11 | private val attributeName: String, 12 | private val delegate: GraphMatcher 13 | ): GraphMatcher { 14 | override fun matches(value: Attributed): DescribedMatch { 15 | val attributeValue = value.attributes[attributeName] 16 | var delegateResult = delegate.matches(attributeValue.nullSafeValue()) 17 | if (!delegateResult.matched) { 18 | // tweak the actual value to provide additional information 19 | delegateResult = delegateResult.copy( 20 | actual = { attributeValue.buildActual() } 21 | ) 22 | } 23 | return DescribedMatch( 24 | actual = { attributeValue.buildActual() }, 25 | description = value.summarizedDescription("attribute '$attributeName' boolean value ${attributeValue.quote()}"), 26 | matched = delegateResult.matched, 27 | subResults = listOf(delegateResult) 28 | ) 29 | } 30 | 31 | private fun Attribute?.buildActual(): Any { 32 | return if (this == null) { 33 | "'$attributeName' attribute not found" 34 | } else { 35 | "$type attribute with value ${value.quote()}" 36 | } 37 | } 38 | 39 | private fun Attribute?.nullSafeValue(): Boolean { 40 | return this?.value.toBoolean() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AttributeNumberGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.Attributed 4 | import org.jgrapht.nio.Attribute 5 | import org.jgrapht.nio.AttributeType 6 | 7 | /** 8 | * Matcher which extracts the specified `Number` attribute, delegating the match of the 9 | * value to the specified matcher. 10 | */ 11 | internal class AttributeNumberGraphMatcher( 12 | private val attributeName: String, 13 | private val delegate: GraphMatcher 14 | ): GraphMatcher { 15 | override fun matches(value: Attributed): DescribedMatch { 16 | val attributeValue = value.attributes[attributeName] 17 | var delegateResult = delegate.matches(attributeValue.value()) 18 | if (!delegateResult.matched) { 19 | // tweak the actual value to provide additional information 20 | delegateResult = delegateResult.copy( 21 | actual = { attributeValue.buildActual() } 22 | ) 23 | } 24 | return DescribedMatch( 25 | actual = { attributeValue.buildActual() }, 26 | description = value.summarizedDescription("attribute '$attributeName' numeric value ${attributeValue.quote()}"), 27 | matched = delegateResult.matched, 28 | subResults = listOf(delegateResult) 29 | ) 30 | } 31 | 32 | private fun Attribute?.buildActual(): Any? { 33 | return when (this?.type) { 34 | null -> { 35 | "no '$attributeName' attribute found" 36 | } 37 | AttributeType.INT, AttributeType.LONG, AttributeType.FLOAT, AttributeType.DOUBLE -> { 38 | this.value() 39 | } 40 | else -> { 41 | "$type attribute with value ${value.quote()}" 42 | } 43 | } 44 | } 45 | 46 | private fun Attribute?.value(): Number? { 47 | return when (this?.type) { 48 | AttributeType.INT -> value.toIntOrNull() 49 | AttributeType.LONG -> value.toLongOrNull() 50 | AttributeType.FLOAT -> value.toFloatOrNull() 51 | AttributeType.DOUBLE -> value.toDoubleOrNull() 52 | else -> null 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AttributeStringGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.Attributed 4 | import org.jgrapht.nio.Attribute 5 | 6 | /** 7 | * Matcher which extracts the specified `String` attribute, delegating the match of the 8 | * value to the specified matcher. 9 | */ 10 | internal class AttributeStringGraphMatcher( 11 | private val attributeName: String, 12 | private val delegate: GraphMatcher 13 | ): GraphMatcher { 14 | override fun matches(value: Attributed): DescribedMatch { 15 | val attributeValue = value.attributes[attributeName] 16 | var delegateResult = delegate.matches(attributeValue.nullSafeValue()) 17 | if (!delegateResult.matched) { 18 | // tweak the actual value to provide additional information 19 | delegateResult = delegateResult.copy( 20 | actual = { attributeValue.buildActual() } 21 | ) 22 | } 23 | return DescribedMatch( 24 | actual = { attributeValue.buildActual() }, 25 | description = value.summarizedDescription("attribute '$attributeName' string value ${attributeValue.quote()}"), 26 | matched = delegateResult.matched, 27 | subResults = listOf(delegateResult) 28 | ) 29 | } 30 | 31 | private fun Attribute?.buildActual(): Any { 32 | return if (this == null) { 33 | "'$attributeName' attribute not found" 34 | } else { 35 | "$type attribute with value ${value.quote()}" 36 | } 37 | } 38 | 39 | private fun Attribute?.nullSafeValue(): String { 40 | return this?.value.toString() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/DescribedMatch.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * The result of evaluating a [GraphMatcher] against an input. 5 | */ 6 | data class DescribedMatch( 7 | /** 8 | * The actual value of the input. 9 | */ 10 | val actual: () -> Any?, 11 | /** 12 | * A descriptive blurb of what the matcher operation was (e.g., `equal to`) 13 | */ 14 | val description: String, 15 | /** 16 | * Flag indicating whether the match was successful. 17 | */ 18 | val matched: Boolean, 19 | /** 20 | * If a matcher delegates to other matchers, this list should contain the results of each 21 | * of the delegate matchers' results. 22 | */ 23 | val subResults: List = emptyList(), 24 | /** 25 | * Flag indicating that the match is the result of an inversion rule (e.g., `not`) 26 | **/ 27 | val inversion: Boolean = false 28 | ) { 29 | /** 30 | * Render a human-readable representation of the match result. 31 | */ 32 | fun render( 33 | /** 34 | * Flag indicating that only matching results should be rendered. 35 | */ 36 | onlyMatches: Boolean = false, 37 | indent: String = "" 38 | ): String { 39 | return buildString { 40 | renderInternal( 41 | onlyMatches = onlyMatches, 42 | value = this@DescribedMatch, 43 | builder = this, 44 | indent = indent) 45 | }.trimEnd() 46 | } 47 | 48 | private fun renderInternal( 49 | onlyMatches: Boolean, 50 | value: DescribedMatch, 51 | builder: StringBuilder, 52 | indent: String, 53 | invertMatch: Boolean = false, 54 | ) { 55 | val matched = if (invertMatch) { 56 | !value.matched 57 | } else { 58 | value.matched 59 | } 60 | 61 | val indicator = if (value.matched) { 62 | "\u2713 " 63 | } else { 64 | "\u2717 " 65 | } 66 | if (matched || !onlyMatches || value.inversion) { 67 | if (value.subResults.isEmpty()) { 68 | builder.append(indent) 69 | .append(indicator) 70 | .append(value.description) 71 | if (!matched) { 72 | builder.append(" (was: ") 73 | .append(value.actual()) 74 | .append(")") 75 | } 76 | builder.appendLine() 77 | } else { 78 | builder.append(indent) 79 | .append(indicator) 80 | .append(value.description) 81 | .append(":\n") 82 | value.subResults.forEach { subResult -> 83 | renderInternal(onlyMatches, subResult, builder, "$indent ", invertMatch = invertMatch xor value.inversion) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EdgeSourceGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.validation.RootedEdge 4 | import com.ebay.plugins.graph.analytics.validation.RootedVertex 5 | 6 | /** 7 | * Matcher which extracts the edge's source vertex, delegating the match of the source 8 | * vertex to the specified delegate matcher. 9 | */ 10 | internal class EdgeSourceGraphMatcher( 11 | private val delegate: GraphMatcher 12 | ): GraphMatcher { 13 | override fun matches(value: RootedEdge): DescribedMatch { 14 | val delegateResult = delegate.matches(value.edgeSource()) 15 | return DescribedMatch( 16 | actual = { value }, 17 | description = value.summarizedDescription("edge source"), 18 | matched = delegateResult.matched, 19 | subResults = listOf(delegateResult) 20 | ) 21 | } 22 | 23 | private fun RootedEdge.edgeSource(): RootedVertex { 24 | return RootedVertex(graph, root, graph.getEdgeSource(edge)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EdgeTargetGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.validation.RootedEdge 4 | import com.ebay.plugins.graph.analytics.validation.RootedVertex 5 | 6 | /** 7 | * Matcher which extracts the edge's target vertex, delegating the match of the target 8 | * vertex to the specified delegate matcher. 9 | */ 10 | internal class EdgeTargetGraphMatcher( 11 | private val delegate: GraphMatcher 12 | ): GraphMatcher { 13 | override fun matches(value: RootedEdge): DescribedMatch { 14 | val delegateResult = delegate.matches(value.edgeTarget()) 15 | return DescribedMatch( 16 | actual = value::toString, 17 | description = value.summarizedDescription("edge target"), 18 | matched = delegateResult.matched, 19 | subResults = listOf(delegateResult), 20 | ) 21 | } 22 | 23 | private fun RootedEdge.edgeTarget(): RootedVertex { 24 | return RootedVertex(graph, root, graph.getEdgeTarget(edge)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EqualToGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which compares the input to the expected value using the equals operator. 5 | */ 6 | internal class EqualToGraphMatcher(private val expected: T) : GraphMatcher { 7 | override fun matches(value: T): DescribedMatch { 8 | val result = value == expected 9 | return DescribedMatch( 10 | actual = { value.quote() }, 11 | description = value.summarizedDescription("equal to ${expected.quote()}"), 12 | matched = result, 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EveryItemGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which matches only if all of the items in the input match the delegate matcher. 5 | */ 6 | internal class EveryItemGraphMatcher( 7 | private val delegate: GraphMatcher 8 | ) : GraphMatcher> { 9 | override fun matches(value: Iterable): DescribedMatch { 10 | val matchResults = value.map { delegate.matches(it) } 11 | val result = matchResults.all { it.matched } 12 | return DescribedMatch( 13 | actual = value::toString, 14 | description = "every item", 15 | matched = result, 16 | subResults = matchResults, 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/GraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * The base interface for all graph matchers. 5 | */ 6 | interface GraphMatcher { 7 | /** 8 | * Evaluates the matcher against the specified value. 9 | * 10 | * @return the result of the match 11 | */ 12 | fun matches(value: T): DescribedMatch 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/GraphMatchers.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.Attributed 4 | import com.ebay.plugins.graph.analytics.validation.RootedEdge 5 | import com.ebay.plugins.graph.analytics.validation.RootedVertex 6 | 7 | /** 8 | * DSL methods used to formulate graph validation expressions. 9 | */ 10 | @Suppress("MemberVisibilityCanBePrivate", "unused") // This is public API 11 | object GraphMatchers { 12 | /** 13 | * Matcher which extracts the specified `String` attribute, delegating the match of the 14 | * value to the specified matcher. 15 | */ 16 | @JvmStatic 17 | fun stringAttribute(key: String, delegate: GraphMatcher): GraphMatcher { 18 | return AttributeStringGraphMatcher(key, delegate) 19 | } 20 | 21 | /** 22 | * Matcher which extracts the specified `Number` attribute, delegating the match of the 23 | * value to the specified matcher. 24 | */ 25 | @JvmStatic 26 | fun numericAttribute(key: String, delegate: GraphMatcher): GraphMatcher { 27 | return AttributeNumberGraphMatcher(key, delegate) 28 | } 29 | 30 | /** 31 | * Matcher which extracts the specified `Boolean` attribute, delegating the match of the 32 | * value to the specified matcher. 33 | */ 34 | @JvmStatic 35 | fun booleanAttribute(key: String, delegate: GraphMatcher): GraphMatcher { 36 | return AttributeBooleanGraphMatcher(key, delegate) 37 | } 38 | 39 | /** 40 | * Matches all outgoing edges (dependencies) of a vertex. 41 | */ 42 | @JvmStatic 43 | fun outgoingEdges(delegate: GraphMatcher>): GraphMatcher { 44 | return OutgoingEdgesGraphMatcher(delegate) 45 | } 46 | 47 | /** 48 | * Matches if there is an outgoing edge (dependency) of a vertex which matches the 49 | * supplied delegate matcher. 50 | */ 51 | // Shortcut 52 | @JvmStatic 53 | fun hasOutgoingEdge(delegate: GraphMatcher): GraphMatcher { 54 | return outgoingEdges(hasItem(delegate)) 55 | } 56 | 57 | /** 58 | * Matcher which extracts the edge's source vertex, delegating the match of the source 59 | * vertex to the specified delegate matcher. 60 | */ 61 | @JvmStatic 62 | fun edgeSource(delegate: GraphMatcher): GraphMatcher { 63 | return EdgeSourceGraphMatcher(delegate) 64 | } 65 | 66 | /** 67 | * Matcher which extracts the edge's target vertex, delegating the match of the target 68 | * vertex to the specified delegate matcher. 69 | */ 70 | @JvmStatic 71 | fun edgeTarget(delegate: GraphMatcher): GraphMatcher { 72 | return EdgeTargetGraphMatcher(delegate) 73 | } 74 | 75 | /** 76 | * Matcher which extracts the vertex's path, delegating the match of the path to the 77 | * supplied matcher. 78 | */ 79 | @JvmStatic 80 | fun path(delegate: GraphMatcher): GraphMatcher { 81 | return VertexPathGraphMatcher(delegate) 82 | } 83 | 84 | /** 85 | * Matcher which matches only if any of the delegate rules match the input. 86 | */ 87 | @JvmStatic 88 | fun anyOf(vararg delegates: GraphMatcher): GraphMatcher { 89 | return AnyOfGraphMatcher(delegates.toList()) 90 | } 91 | 92 | /** 93 | * Matcher which matches only if all delegate rules match the input. 94 | */ 95 | @JvmStatic 96 | fun allOf(vararg delegates: GraphMatcher): GraphMatcher { 97 | return AllOfGraphMatcher(delegates.toList()) 98 | } 99 | 100 | /** 101 | * Matcher which matches only if any one of the items in the input match the delegate matcher. 102 | */ 103 | @JvmStatic 104 | fun hasItem(delegate: GraphMatcher): GraphMatcher> { 105 | return HasItemGraphMatcher(delegate) 106 | } 107 | 108 | /** 109 | * Matcher which matches only if all of the items in the input match the delegate matcher. 110 | */ 111 | @JvmStatic 112 | fun everyItem(delegate: GraphMatcher): GraphMatcher> { 113 | return EveryItemGraphMatcher(delegate) 114 | } 115 | 116 | /** 117 | * Matcher which inverts the result of the delegate matcher. 118 | */ 119 | @JvmStatic 120 | fun not(delegate: GraphMatcher): GraphMatcher { 121 | return NotGraphMatcher(delegate) 122 | } 123 | 124 | /** 125 | * Matcher which compares the input to the expected value using the equals operator. 126 | */ 127 | @JvmStatic 128 | fun equalTo(value: T): GraphMatcher { 129 | return EqualToGraphMatcher(value) 130 | } 131 | 132 | /** 133 | * Matcher which compares the input to the expected value using the less than operator. 134 | */ 135 | @JvmStatic 136 | fun lessThan(value: T): GraphMatcher { 137 | return LessThanGraphMatcher(value) 138 | } 139 | 140 | /** 141 | * Matcher which compares the input to the expected value using the greater than operator. 142 | */ 143 | @JvmStatic 144 | fun greaterThan(value: T): GraphMatcher { 145 | return GreaterThanGraphMatcher(value) 146 | } 147 | 148 | /** 149 | * Matcher which matches an input string against a supplied regular expression. 150 | */ 151 | @JvmStatic 152 | fun matchesPattern(value: String): GraphMatcher { 153 | return MatchesPatternGraphMatcher(value) 154 | } 155 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/GreaterThanGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which compares the input to the expected value using the greater than operator. 5 | */ 6 | internal class GreaterThanGraphMatcher(private val expected: T) : GraphMatcher { 7 | override fun matches(value: T?): DescribedMatch { 8 | val result = value != null && value.toDouble() > expected.toDouble() 9 | return DescribedMatch( 10 | actual = { value.quote() }, 11 | description = value.summarizedDescription("greater than ${expected.quote()}"), 12 | matched = result, 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/HasItemGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which matches only if any one of the items in the input match the delegate matcher. 5 | */ 6 | internal class HasItemGraphMatcher( 7 | private val delegate: GraphMatcher 8 | ) : GraphMatcher> { 9 | override fun matches(value: Iterable): DescribedMatch { 10 | val matchResults = value.map { delegate.matches(it) } 11 | val result = matchResults.any { it.matched } 12 | return DescribedMatch( 13 | actual = value::toString, 14 | description = "any item", 15 | matched = result, 16 | subResults = matchResults, 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/LessThanGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which compares the input to the expected value using the less than operator. 5 | */ 6 | internal class LessThanGraphMatcher(private val expected: T) : GraphMatcher { 7 | override fun matches(value: T?): DescribedMatch { 8 | val result = value != null && value.toDouble() < expected.toDouble() 9 | return DescribedMatch( 10 | actual = { value.quote() }, 11 | description = value.summarizedDescription("less than ${expected.quote()}"), 12 | matched = result, 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/MatchesPatternGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import java.util.regex.Pattern 4 | 5 | /** 6 | * Matcher which matches an input string against a supplied regular expression. 7 | */ 8 | internal class MatchesPatternGraphMatcher( 9 | private val patternString: String 10 | ): GraphMatcher { 11 | private val pattern = Pattern.compile(patternString) 12 | 13 | override fun matches(value: String): DescribedMatch { 14 | val actual = pattern.matcher(value).matches() 15 | return DescribedMatch( 16 | actual = { value }, 17 | description = "matches pattern '$patternString'", 18 | matched = actual, 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/NotGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | /** 4 | * Matcher which inverts the result of the delegate matcher. 5 | */ 6 | internal class NotGraphMatcher(private val delegate: GraphMatcher) : GraphMatcher { 7 | override fun matches(value: T): DescribedMatch { 8 | val delegateResult = delegate.matches(value) 9 | return DescribedMatch( 10 | actual = { value }, 11 | description = value.summarizedDescription("not"), 12 | matched = !delegateResult.matched, 13 | subResults = listOf(delegateResult), 14 | inversion = true 15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/OutgoingEdgesGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.validation.RootedEdge 4 | import com.ebay.plugins.graph.analytics.validation.RootedVertex 5 | 6 | /** 7 | * Matches all outgoing edges (dependencies) of a vertex. 8 | */ 9 | internal class OutgoingEdgesGraphMatcher( 10 | private val delegate: GraphMatcher> 11 | ): GraphMatcher { 12 | override fun matches(value: RootedVertex): DescribedMatch { 13 | val delegateResult = delegate.matches(value.outgoingEdges()) 14 | return DescribedMatch( 15 | actual = { value }, 16 | description = value.summarizedDescription("outgoing edges"), 17 | matched = delegateResult.matched, 18 | subResults = listOf(delegateResult), 19 | ) 20 | } 21 | 22 | private fun RootedVertex.outgoingEdges(): Iterable { 23 | return graph.outgoingEdgesOf(vertex).map { edge -> 24 | RootedEdge(graph, root, edge) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/SummarizedExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.validation.Summarized 4 | 5 | internal fun Any?.quote(): String { 6 | return when(this) { 7 | null -> "" 8 | is Number -> this.toString() 9 | else -> "'$this'" 10 | } 11 | } 12 | 13 | internal fun Any?.summarizedDescription(description: String): String { 14 | return if (this is Summarized) { 15 | "[${getSummary()}] $description" 16 | } else { 17 | description 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/VertexPathGraphMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.validation.RootedVertex 4 | 5 | /** 6 | * Matcher which extracts the vertex's path, delegating the match of the path to the 7 | * supplied matcher. 8 | */ 9 | internal class VertexPathGraphMatcher( 10 | private val delegate: GraphMatcher 11 | ): GraphMatcher { 12 | override fun matches(value: RootedVertex): DescribedMatch { 13 | val delegateResult = delegate.matches(value.vertex.path) 14 | return DescribedMatch( 15 | actual = { value.vertex.path }, 16 | description = value.summarizedDescription("path"), 17 | matched = delegateResult.matched, 18 | subResults = listOf(delegateResult), 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/BasePluginFunctionalTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.testng.annotations.AfterClass 4 | import java.io.File 5 | import java.nio.file.Files 6 | 7 | abstract class BasePluginFunctionalTest { 8 | protected val persistence: GraphPersistence = GraphPersistenceGraphMl() 9 | protected val projectDir: File by lazy { 10 | Files.createTempDirectory(javaClass.simpleName).toFile() 11 | } 12 | 13 | protected fun resetProjectDir() { 14 | projectDir.deleteRecursively() 15 | projectDir.mkdirs() 16 | } 17 | 18 | @AfterClass 19 | fun cleanupTempDir() { 20 | projectDir.deleteRecursively() 21 | } 22 | 23 | protected fun createProjectStructure(projects: Map){ 24 | resetProjectDir() 25 | copyResourceToFile("settings.gradle.kts", "settings.gradle.kts") { template -> 26 | val includeStatements = projects.keys.joinToString("\n") { 27 | val name = it.replace("/", ":") 28 | "include(\":$name\")" 29 | } 30 | template.replace("// INCLUDE PLACEHOLDER", includeStatements) 31 | } 32 | projects.forEach { (name, dependencies) -> 33 | copyResourceToFile("build.gradle.kts", "$name/build.gradle.kts") { template -> 34 | val prodDeps = dependencies.dependencies.joinToString("\n") { 35 | " implementation(project(\"${it}\"))" 36 | } 37 | val testDeps = dependencies.testDependencies.joinToString("\n") { 38 | " testImplementation(project(\"${it}\"))" 39 | } 40 | template 41 | .replace("// PRODUCTION DEPENDENCIES PLACEHOLDER", prodDeps) 42 | .replace("// TEST DEPENDENCIES PLACEHOLDER", testDeps) 43 | } 44 | } 45 | } 46 | 47 | private fun copyResourceToFile( 48 | resource: String, 49 | targetFile: String, 50 | transform: (String) -> String? = { it } 51 | ) { 52 | val content = javaClass.classLoader.getResourceAsStream(resource)?.use { 53 | transform.invoke(it.readAllBytes().decodeToString()) 54 | } ?: throw IllegalArgumentException("Resource not found: $resource") 55 | 56 | val target = projectDir.resolve(targetFile) 57 | target.parentFile.mkdirs() 58 | target.writeText(content) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/DependenciesSpec.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | /** 4 | * Model for dependencies of a project module. 5 | */ 6 | data class DependenciesSpec( 7 | val dependencies: List = emptyList(), 8 | val testDependencies: List = emptyList(), 9 | ) 10 | -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/GraphEdgeInfoTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.hamcrest.Matchers.not 6 | import org.jgrapht.graph.DefaultDirectedGraph 7 | import org.jgrapht.nio.AttributeType 8 | import org.jgrapht.nio.DefaultAttribute 9 | import org.testng.annotations.Test 10 | 11 | class GraphEdgeInfoTest { 12 | @Test 13 | fun equality() { 14 | val graph1 = DefaultDirectedGraph(EdgeInfo::class.java) 15 | val vertex1 = VertexInfo(path = ":1").also { graph1.addVertex(it) } 16 | val vertex2 = VertexInfo(path = ":2").also { graph1.addVertex(it) } 17 | graph1.addEdge(vertex1, vertex2, EdgeInfo(attributes = mutableMapOf( 18 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 19 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 20 | ))) 21 | val one = graph1.getEdge(vertex1, vertex2) 22 | 23 | val graph2 = DefaultDirectedGraph(EdgeInfo::class.java) 24 | graph2.addVertex(vertex1) 25 | graph2.addVertex(vertex2) 26 | graph2.addEdge(vertex1, vertex2, EdgeInfo(attributes = mutableMapOf( 27 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 28 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 29 | ))) 30 | val two = graph2.getEdge(vertex1, vertex2) 31 | 32 | assertThat(one, equalTo(two)) 33 | assertThat(one.hashCode(), equalTo(two.hashCode())) 34 | } 35 | 36 | @Test 37 | fun nonAttributeEquality() { 38 | val graph1 = DefaultDirectedGraph(EdgeInfo::class.java) 39 | val vertex1 = VertexInfo(path = ":1").also { graph1.addVertex(it) } 40 | val vertex2 = VertexInfo(path = ":2").also { graph1.addVertex(it) } 41 | graph1.addEdge(vertex1, vertex2, EdgeInfo(attributes = mutableMapOf( 42 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 43 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 44 | ))) 45 | val one = graph1.getEdge(vertex1, vertex2) 46 | 47 | val graph2 = DefaultDirectedGraph(EdgeInfo::class.java) 48 | graph2.addVertex(vertex1) 49 | graph2.addVertex(vertex2) 50 | graph2.addEdge(vertex1, vertex2, EdgeInfo(attributes = mutableMapOf( 51 | "attr3" to DefaultAttribute("value3", AttributeType.STRING), 52 | "attr4" to DefaultAttribute("value4", AttributeType.STRING), 53 | ))) 54 | val two = graph2.getEdge(vertex1, vertex2) 55 | 56 | assertThat(one, equalTo(two)) 57 | assertThat(one.hashCode(), equalTo(two.hashCode())) 58 | } 59 | 60 | @Test 61 | fun nonSourceEquality() { 62 | val graph1 = DefaultDirectedGraph(EdgeInfo::class.java) 63 | val vertex1 = VertexInfo(path = ":1").also { graph1.addVertex(it) } 64 | val vertex2 = VertexInfo(path = ":2").also { graph1.addVertex(it) } 65 | graph1.addEdge(vertex1, vertex2, EdgeInfo(attributes = mutableMapOf( 66 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 67 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 68 | ))) 69 | val one = graph1.getEdge(vertex1, vertex2) 70 | 71 | val graph2 = DefaultDirectedGraph(EdgeInfo::class.java) 72 | val vertex3 = VertexInfo(path = ":3").also { graph2.addVertex(it) } 73 | graph2.addVertex(vertex2) 74 | graph2.addVertex(vertex3) 75 | graph2.addEdge(vertex3, vertex2, EdgeInfo(attributes = mutableMapOf( 76 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 77 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 78 | ))) 79 | val two = graph2.getEdge(vertex3, vertex2) 80 | 81 | assertThat(one, not(equalTo(two))) 82 | assertThat(one.hashCode(), not(equalTo(two.hashCode()))) 83 | } 84 | 85 | @Test 86 | fun nonTargetEquality() { 87 | val graph1 = DefaultDirectedGraph(EdgeInfo::class.java) 88 | val vertex1 = VertexInfo(path = ":1").also { graph1.addVertex(it) } 89 | val vertex2 = VertexInfo(path = ":2").also { graph1.addVertex(it) } 90 | graph1.addEdge(vertex1, vertex2, EdgeInfo(attributes = mutableMapOf( 91 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 92 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 93 | ))) 94 | val one = graph1.getEdge(vertex1, vertex2) 95 | 96 | val graph2 = DefaultDirectedGraph(EdgeInfo::class.java) 97 | val vertex3 = VertexInfo(path = ":3").also { graph2.addVertex(it) } 98 | graph2.addVertex(vertex1) 99 | graph2.addVertex(vertex3) 100 | graph2.addEdge(vertex1, vertex3, EdgeInfo(attributes = mutableMapOf( 101 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 102 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 103 | ))) 104 | val two = graph2.getEdge(vertex1, vertex3) 105 | 106 | assertThat(one, not(equalTo(two))) 107 | assertThat(one.hashCode(), not(equalTo(two.hashCode()))) 108 | } 109 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/GraphPersistenceGexfTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | class GraphPersistenceGexfTest : BaseGraphPersistenceTest() { 4 | override val uut = GraphPersistenceGexf() 5 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/GraphPersistenceGraphMlTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | class GraphPersistenceGraphMlTest : BaseGraphPersistenceTest() { 4 | override val uut = GraphPersistenceGraphMl() 5 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/GraphVertexInfoTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.jgrapht.nio.AttributeType 6 | import org.jgrapht.nio.DefaultAttribute 7 | import org.testng.annotations.Test 8 | 9 | class GraphVertexInfoTest { 10 | @Test 11 | fun equality() { 12 | val one = VertexInfo(path = ":some:path", attributes = mutableMapOf( 13 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 14 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 15 | )) 16 | val two = VertexInfo(path = ":some:path").apply { 17 | attributes["attr1"] = DefaultAttribute("value1", AttributeType.STRING) 18 | attributes["attr2"] = DefaultAttribute("value2", AttributeType.STRING) 19 | } 20 | assertThat(one, equalTo(two)) 21 | assertThat(one.hashCode(), equalTo(two.hashCode())) 22 | } 23 | 24 | @Test 25 | fun equalityInMapKey() { 26 | val map = LinkedHashMap() 27 | val one = VertexInfo(path = ":some:path", attributes = mutableMapOf( 28 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 29 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 30 | )) 31 | map[one] = "one" 32 | 33 | val two = VertexInfo(path = ":some:path").apply { 34 | attributes["attr1"] = DefaultAttribute("value1", AttributeType.STRING) 35 | attributes["attr2"] = DefaultAttribute("value2", AttributeType.STRING) 36 | } 37 | assertThat(map.keys.contains(one), equalTo(true)) 38 | assertThat(map.keys.contains(two), equalTo(true)) 39 | } 40 | 41 | // NOTE: See comment on [GraphVertexInfo.hashCode] 42 | @Test 43 | fun nonAttributeEquality() { 44 | val one = VertexInfo(path = ":some:path", attributes = mutableMapOf( 45 | "attr1" to DefaultAttribute("value1", AttributeType.STRING), 46 | "attr2" to DefaultAttribute("value2", AttributeType.STRING), 47 | )) 48 | val two = VertexInfo(path = ":some:path", attributes = mutableMapOf( 49 | "attr3" to DefaultAttribute("value3", AttributeType.STRING), 50 | "attr4" to DefaultAttribute("value4", AttributeType.STRING), 51 | )) 52 | assertThat(one, equalTo(two)) 53 | assertThat(one.hashCode(), equalTo(two.hashCode())) 54 | } 55 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AllOfGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class AllOfGraphMatcherTest { 8 | @Test 9 | fun match() { 10 | val result = AllOfGraphMatcher( 11 | listOf( 12 | EqualToGraphMatcher("test string"), 13 | ) 14 | ).matches("test string") 15 | println(result.render()) 16 | assertThat(result.matched, equalTo(true)) 17 | } 18 | 19 | @Test 20 | fun mismatch() { 21 | val result = AllOfGraphMatcher( 22 | listOf( 23 | EqualToGraphMatcher("test string"), 24 | EqualToGraphMatcher("other string"), 25 | ) 26 | ).matches("test string") 27 | println(result.render()) 28 | assertThat(result.matched, equalTo(false)) 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AnyOfGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class AnyOfGraphMatcherTest { 8 | @Test 9 | fun match() { 10 | val result = AnyOfGraphMatcher( 11 | listOf( 12 | EqualToGraphMatcher("test string"), 13 | EqualToGraphMatcher("other string"), 14 | ) 15 | ).matches("test string") 16 | println(result.render()) 17 | assertThat(result.matched, equalTo(true)) 18 | } 19 | 20 | @Test 21 | fun mismatch() { 22 | val result = AnyOfGraphMatcher( 23 | listOf( 24 | EqualToGraphMatcher("other string"), 25 | EqualToGraphMatcher("yet another string"), 26 | ) 27 | ).matches("test string") 28 | println(result.render()) 29 | assertThat(result.matched, equalTo(false)) 30 | } 31 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AttributeNumberGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.VertexInfo 4 | import org.hamcrest.MatcherAssert.assertThat 5 | import org.hamcrest.Matchers.equalTo 6 | import org.jgrapht.nio.DefaultAttribute 7 | import org.testng.annotations.Test 8 | 9 | class AttributeNumberGraphMatcherTest { 10 | @Test 11 | fun matchInt() { 12 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 13 | "key" to DefaultAttribute.createAttribute(1) 14 | )) 15 | val result = AttributeNumberGraphMatcher( 16 | "key", 17 | EqualToGraphMatcher(1), 18 | ).matches(attributed) 19 | println(result.render()) 20 | assertThat(result.matched, equalTo(true)) 21 | } 22 | 23 | @Test 24 | fun mismatchIntValue() { 25 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 26 | "key" to DefaultAttribute.createAttribute(1) 27 | )) 28 | val result = AttributeNumberGraphMatcher( 29 | "key", 30 | EqualToGraphMatcher(2), 31 | ).matches(attributed) 32 | println(result.render()) 33 | assertThat(result.matched, equalTo(false)) 34 | } 35 | 36 | @Test 37 | fun mismatchIntType() { 38 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 39 | "key" to DefaultAttribute.createAttribute("2") 40 | )) 41 | val result = AttributeNumberGraphMatcher( 42 | "key", 43 | EqualToGraphMatcher(2), 44 | ).matches(attributed) 45 | println(result.render()) 46 | assertThat(result.matched, equalTo(false)) 47 | } 48 | 49 | @Test 50 | fun matchLong() { 51 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 52 | "key" to DefaultAttribute.createAttribute(1L) 53 | )) 54 | val result = AttributeNumberGraphMatcher( 55 | "key", 56 | EqualToGraphMatcher(1L), 57 | ).matches(attributed) 58 | println(result.render()) 59 | assertThat(result.matched, equalTo(true)) 60 | } 61 | 62 | @Test 63 | fun mismatchLongValue() { 64 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 65 | "key" to DefaultAttribute.createAttribute(1L) 66 | )) 67 | val result = AttributeNumberGraphMatcher( 68 | "key", 69 | EqualToGraphMatcher(2L), 70 | ).matches(attributed) 71 | println(result.render()) 72 | assertThat(result.matched, equalTo(false)) 73 | } 74 | 75 | @Test 76 | fun mismatchLongType() { 77 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 78 | "key" to DefaultAttribute.createAttribute("2") 79 | )) 80 | val result = AttributeNumberGraphMatcher( 81 | "key", 82 | EqualToGraphMatcher(2), 83 | ).matches(attributed) 84 | println(result.render()) 85 | assertThat(result.matched, equalTo(false)) 86 | } 87 | 88 | @Test 89 | fun matchFloat() { 90 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 91 | "key" to DefaultAttribute.createAttribute(1.0F) 92 | )) 93 | val result = AttributeNumberGraphMatcher( 94 | "key", 95 | EqualToGraphMatcher(1.0F) 96 | ).matches(attributed) 97 | println(result.render()) 98 | assertThat(result.matched, equalTo(true)) 99 | } 100 | 101 | @Test 102 | fun mismatchFloatValue() { 103 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 104 | "key" to DefaultAttribute.createAttribute(2.0F) 105 | )) 106 | val result = AttributeNumberGraphMatcher( 107 | "key", 108 | EqualToGraphMatcher(1.0F), 109 | ).matches(attributed) 110 | println(result.render()) 111 | assertThat(result.matched, equalTo(false)) 112 | } 113 | 114 | @Test 115 | fun mismatchFloatType() { 116 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 117 | "key" to DefaultAttribute.createAttribute("2") 118 | )) 119 | val result = AttributeNumberGraphMatcher( 120 | "key", 121 | EqualToGraphMatcher(2.0F), 122 | ).matches(attributed) 123 | println(result.render()) 124 | assertThat(result.matched, equalTo(false)) 125 | } 126 | 127 | @Test 128 | fun matchDouble() { 129 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 130 | "key" to DefaultAttribute.createAttribute(1.0) 131 | )) 132 | val result = AttributeNumberGraphMatcher( 133 | "key", 134 | EqualToGraphMatcher(1.0) 135 | ).matches(attributed) 136 | println(result.render()) 137 | assertThat(result.matched, equalTo(true)) 138 | } 139 | 140 | @Test 141 | fun mismatchDoubleValue() { 142 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 143 | "key" to DefaultAttribute.createAttribute(2.0) 144 | )) 145 | val result = AttributeNumberGraphMatcher( 146 | "key", 147 | EqualToGraphMatcher(1.0), 148 | ).matches(attributed) 149 | println(result.render()) 150 | assertThat(result.matched, equalTo(false)) 151 | } 152 | 153 | @Test 154 | fun mismatchDoubleType() { 155 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 156 | "key" to DefaultAttribute.createAttribute("2") 157 | )) 158 | val result = AttributeNumberGraphMatcher( 159 | "key", 160 | EqualToGraphMatcher(2.0), 161 | ).matches(attributed) 162 | println(result.render()) 163 | assertThat(result.matched, equalTo(false)) 164 | } 165 | 166 | @Test 167 | fun notFound() { 168 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf()) 169 | val result = AttributeNumberGraphMatcher( 170 | "key", 171 | EqualToGraphMatcher(2.0), 172 | ).matches(attributed) 173 | println(result.render()) 174 | assertThat(result.matched, equalTo(false)) 175 | } 176 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/AttributeStringGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.VertexInfo 4 | import org.hamcrest.MatcherAssert.assertThat 5 | import org.hamcrest.Matchers.equalTo 6 | import org.jgrapht.nio.DefaultAttribute 7 | import org.testng.annotations.Test 8 | 9 | class AttributeStringGraphMatcherTest { 10 | @Test 11 | fun matchInt() { 12 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 13 | "key" to DefaultAttribute.createAttribute(1) 14 | )) 15 | val result = AttributeStringGraphMatcher( 16 | "key", 17 | EqualToGraphMatcher("1"), 18 | ).matches(attributed) 19 | println(result.render()) 20 | assertThat(result.matched, equalTo(true)) 21 | } 22 | 23 | @Test 24 | fun mismatchIntValue() { 25 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 26 | "key" to DefaultAttribute.createAttribute(1) 27 | )) 28 | val result = AttributeStringGraphMatcher( 29 | "key", 30 | EqualToGraphMatcher("2"), 31 | ).matches(attributed) 32 | println(result.render()) 33 | assertThat(result.matched, equalTo(false)) 34 | } 35 | 36 | @Test 37 | fun matchLong() { 38 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 39 | "key" to DefaultAttribute.createAttribute(1L) 40 | )) 41 | val result = AttributeStringGraphMatcher( 42 | "key", 43 | EqualToGraphMatcher("1"), 44 | ).matches(attributed) 45 | println(result.render()) 46 | assertThat(result.matched, equalTo(true)) 47 | } 48 | 49 | @Test 50 | fun mismatchLongValue() { 51 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 52 | "key" to DefaultAttribute.createAttribute(1L) 53 | )) 54 | val result = AttributeStringGraphMatcher( 55 | "key", 56 | EqualToGraphMatcher("2"), 57 | ).matches(attributed) 58 | println(result.render()) 59 | assertThat(result.matched, equalTo(false)) 60 | } 61 | 62 | @Test 63 | fun matchFloat() { 64 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 65 | "key" to DefaultAttribute.createAttribute(1.0F) 66 | )) 67 | val result = AttributeStringGraphMatcher( 68 | "key", 69 | EqualToGraphMatcher("1.0"), 70 | ).matches(attributed) 71 | println(result.render()) 72 | assertThat(result.matched, equalTo(true)) 73 | } 74 | 75 | @Test 76 | fun mismatchFloatValue() { 77 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 78 | "key" to DefaultAttribute.createAttribute(1.0F) 79 | )) 80 | val result = AttributeStringGraphMatcher( 81 | "key", 82 | EqualToGraphMatcher("2"), 83 | ).matches(attributed) 84 | println(result.render()) 85 | assertThat(result.matched, equalTo(false)) 86 | } 87 | 88 | @Test 89 | fun matchDouble() { 90 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 91 | "key" to DefaultAttribute.createAttribute(1.0) 92 | )) 93 | val result = AttributeStringGraphMatcher( 94 | "key", 95 | EqualToGraphMatcher("1.0"), 96 | ).matches(attributed) 97 | println(result.render()) 98 | assertThat(result.matched, equalTo(true)) 99 | } 100 | 101 | @Test 102 | fun mismatchDoubleValue() { 103 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 104 | "key" to DefaultAttribute.createAttribute(1.0) 105 | )) 106 | val result = AttributeStringGraphMatcher( 107 | "key", 108 | EqualToGraphMatcher("2"), 109 | ).matches(attributed) 110 | println(result.render()) 111 | assertThat(result.matched, equalTo(false)) 112 | } 113 | 114 | @Test 115 | fun mismatchType() { 116 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf( 117 | "key" to DefaultAttribute.NULL 118 | )) 119 | val result = AttributeStringGraphMatcher( 120 | "key", 121 | EqualToGraphMatcher("2"), 122 | ).matches(attributed) 123 | println(result.render()) 124 | assertThat(result.matched, equalTo(false)) 125 | } 126 | 127 | @Test 128 | fun notFound() { 129 | val attributed = VertexInfo(path = "key", attributes = mutableMapOf()) 130 | val result = AttributeStringGraphMatcher( 131 | "key", 132 | EqualToGraphMatcher("2"), 133 | ).matches(attributed) 134 | println(result.render()) 135 | assertThat(result.matched, equalTo(false)) 136 | } 137 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/DescribedMatchTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.testng.annotations.Test 4 | 5 | class DescribedMatchTest { 6 | 7 | @Test 8 | fun render() { 9 | val actual = DescribedMatch( 10 | actual = { "actual" }, 11 | description = "any item", 12 | matched = true, 13 | subResults = listOf( 14 | DescribedMatch( 15 | actual = { "subActual" }, 16 | description = "equal to 'nested'", 17 | matched = false, 18 | subResults = emptyList(), 19 | ), 20 | DescribedMatch( 21 | actual = { "notActual" }, 22 | description = "not", 23 | matched = true, 24 | inversion = true, 25 | subResults = listOf( 26 | DescribedMatch( 27 | actual = { "false" }, 28 | description = "equals 'true' (was: 'false')", 29 | matched = false, 30 | subResults = emptyList(), 31 | ), 32 | ), 33 | ), 34 | ), 35 | ).render(onlyMatches = true) 36 | println(actual) 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EdgeSourceGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.EdgeInfo 4 | import com.ebay.plugins.graph.analytics.VertexInfo 5 | import com.ebay.plugins.graph.analytics.validation.RootedEdge 6 | import org.hamcrest.MatcherAssert.assertThat 7 | import org.hamcrest.Matchers.equalTo 8 | import org.jgrapht.graph.DefaultDirectedGraph 9 | import org.mockito.kotlin.mock 10 | import org.testng.annotations.BeforeTest 11 | import org.testng.annotations.Test 12 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.equalTo as graphEqualTo 13 | 14 | class EdgeSourceGraphMatcherTest { 15 | private lateinit var root: VertexInfo 16 | private lateinit var source: VertexInfo 17 | private lateinit var target: VertexInfo 18 | private lateinit var graph: DefaultDirectedGraph 19 | private lateinit var edge: EdgeInfo 20 | private lateinit var rootedEdge: RootedEdge 21 | 22 | @BeforeTest 23 | fun setupUut() { 24 | root = VertexInfo(path = ":root") 25 | source = VertexInfo(path = ":source") 26 | target = VertexInfo(path = ":target") 27 | edge = EdgeInfo() 28 | graph = mock { 29 | on { getEdgeSource(edge) }.thenReturn(source) 30 | on { getEdgeTarget(edge) }.thenReturn(target) 31 | } 32 | rootedEdge = RootedEdge(graph = graph, root = root, edge = edge) 33 | } 34 | 35 | @Test 36 | fun match() { 37 | val result = EdgeSourceGraphMatcher( 38 | VertexPathGraphMatcher(graphEqualTo(":source")) 39 | ).matches(rootedEdge) 40 | println(result.render()) 41 | assertThat(result.matched, equalTo(true)) 42 | } 43 | 44 | @Test 45 | fun mismatch() { 46 | val result = EdgeSourceGraphMatcher( 47 | VertexPathGraphMatcher(graphEqualTo(":other")) 48 | ).matches(rootedEdge) 49 | println(result.render()) 50 | assertThat(result.matched, equalTo(false)) 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EdgeTargetGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.EdgeInfo 4 | import com.ebay.plugins.graph.analytics.VertexInfo 5 | import com.ebay.plugins.graph.analytics.validation.RootedEdge 6 | import org.hamcrest.MatcherAssert.assertThat 7 | import org.hamcrest.Matchers.equalTo 8 | import org.jgrapht.graph.DefaultDirectedGraph 9 | import org.mockito.kotlin.mock 10 | import org.testng.annotations.BeforeTest 11 | import org.testng.annotations.Test 12 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.equalTo as graphEqualTo 13 | 14 | class EdgeTargetGraphMatcherTest { 15 | private lateinit var root: VertexInfo 16 | private lateinit var source: VertexInfo 17 | private lateinit var target: VertexInfo 18 | private lateinit var graph: DefaultDirectedGraph 19 | private lateinit var edge: EdgeInfo 20 | private lateinit var rootedEdge: RootedEdge 21 | 22 | @BeforeTest 23 | fun setupUut() { 24 | root = VertexInfo(path = ":root") 25 | source = VertexInfo(path = ":source") 26 | target = VertexInfo(path = ":target") 27 | edge = EdgeInfo() 28 | graph = mock { 29 | on { getEdgeSource(edge) }.thenReturn(source) 30 | on { getEdgeTarget(edge) }.thenReturn(target) 31 | } 32 | rootedEdge = RootedEdge(graph = graph, root = root, edge = edge) 33 | } 34 | 35 | @Test 36 | fun match() { 37 | val result = EdgeTargetGraphMatcher( 38 | VertexPathGraphMatcher(graphEqualTo(":target")) 39 | ).matches(rootedEdge) 40 | println(result.render()) 41 | assertThat(result.matched, equalTo(true)) 42 | } 43 | 44 | @Test 45 | fun mismatch() { 46 | val result = EdgeTargetGraphMatcher( 47 | VertexPathGraphMatcher(graphEqualTo(":other")) 48 | ).matches(rootedEdge) 49 | println(result.render()) 50 | assertThat(result.matched, equalTo(false)) 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EqualToGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class EqualToGraphMatcherTest { 8 | @Test 9 | fun matchString() { 10 | val result = EqualToGraphMatcher("string").matches("string") 11 | println(result.render()) 12 | assertThat(result.matched, equalTo(true)) 13 | } 14 | 15 | @Test 16 | fun matchNumber() { 17 | val result = EqualToGraphMatcher(1).matches(1) 18 | println(result.render()) 19 | assertThat(result.matched, equalTo(true)) 20 | } 21 | 22 | @Test 23 | fun mismatch() { 24 | val result = EqualToGraphMatcher(1).matches(2) 25 | println(result.render()) 26 | assertThat(result.matched, equalTo(false)) 27 | } 28 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/EveryItemGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class EveryItemGraphMatcherTest { 8 | @Test 9 | fun match() { 10 | val result = EveryItemGraphMatcher( 11 | EqualToGraphMatcher("1") 12 | ).matches(listOf("1", "1", "1")) 13 | println(result.render()) 14 | assertThat(result.matched, equalTo(true)) 15 | } 16 | 17 | @Test 18 | fun mismatch() { 19 | val result = EveryItemGraphMatcher( 20 | EqualToGraphMatcher("1") 21 | ).matches(listOf("1", "2", "1")) 22 | println(result.render()) 23 | assertThat(result.matched, equalTo(false)) 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/GreaterThanGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class GreaterThanGraphMatcherTest { 8 | @Test 9 | fun matchNumber() { 10 | val result = GreaterThanGraphMatcher(1).matches(2) 11 | println(result.render()) 12 | assertThat(result.matched, equalTo(true)) 13 | } 14 | 15 | @Test 16 | fun mismatch() { 17 | val result = GreaterThanGraphMatcher(2).matches(1) 18 | println(result.render()) 19 | assertThat(result.matched, equalTo(false)) 20 | } 21 | 22 | @Test 23 | fun mismatchNull() { 24 | val result = GreaterThanGraphMatcher(1).matches(null) 25 | println(result.render()) 26 | assertThat(result.matched, equalTo(false)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/HasItemGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class HasItemGraphMatcherTest { 8 | @Test 9 | fun match() { 10 | val result = HasItemGraphMatcher( 11 | EqualToGraphMatcher("1") 12 | ).matches(listOf("1", "2", "3")) 13 | println(result.render()) 14 | assertThat(result.matched, equalTo(true)) 15 | } 16 | 17 | @Test 18 | fun mismatch() { 19 | val result = HasItemGraphMatcher( 20 | EqualToGraphMatcher("4") 21 | ).matches(listOf("1", "2", "3")) 22 | println(result.render()) 23 | assertThat(result.matched, equalTo(false)) 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/LessThanGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class LessThanGraphMatcherTest { 8 | @Test 9 | fun matchNumber() { 10 | val result = LessThanGraphMatcher(2).matches(1) 11 | println(result.render()) 12 | assertThat(result.matched, equalTo(true)) 13 | } 14 | 15 | @Test 16 | fun mismatch() { 17 | val result = LessThanGraphMatcher(1).matches(2) 18 | println(result.render()) 19 | assertThat(result.matched, equalTo(false)) 20 | } 21 | 22 | @Test 23 | fun mismatchNull() { 24 | val result = LessThanGraphMatcher(1).matches(null) 25 | println(result.render()) 26 | assertThat(result.matched, equalTo(false)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/MatchesPatternGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | 7 | class MatchesPatternGraphMatcherTest { 8 | @Test 9 | fun success() { 10 | val actual = MatchesPatternGraphMatcher(":sour.*").matches(":source") 11 | println(actual.render()) 12 | assertThat(actual.matched, equalTo(true)) 13 | } 14 | 15 | @Test 16 | fun failure() { 17 | val actual = MatchesPatternGraphMatcher(":foo.*").matches(":source") 18 | println(actual.render()) 19 | assertThat(actual.matched, equalTo(false)) 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/NotGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import org.hamcrest.MatcherAssert.assertThat 4 | import org.hamcrest.Matchers.equalTo 5 | import org.testng.annotations.Test 6 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.equalTo as graphEqualTo 7 | 8 | class NotGraphMatcherTest { 9 | @Test 10 | fun success() { 11 | val actual = NotGraphMatcher(graphEqualTo("1")).matches("0") 12 | println(actual.render()) 13 | assertThat(actual.matched, equalTo(true)) 14 | } 15 | 16 | @Test 17 | fun failure() { 18 | val actual = NotGraphMatcher(graphEqualTo("1")).matches("1") 19 | println(actual.render()) 20 | assertThat(actual.matched, equalTo(false)) 21 | } 22 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/ebay/plugins/graph/analytics/validation/matchers/VertexPathGraphMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.ebay.plugins.graph.analytics.validation.matchers 2 | 3 | import com.ebay.plugins.graph.analytics.EdgeInfo 4 | import com.ebay.plugins.graph.analytics.VertexInfo 5 | import com.ebay.plugins.graph.analytics.validation.RootedVertex 6 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.path 7 | import org.hamcrest.MatcherAssert.assertThat 8 | import org.hamcrest.Matchers.equalTo 9 | import org.jgrapht.graph.DefaultDirectedGraph 10 | import org.mockito.kotlin.mock 11 | import org.testng.annotations.Test 12 | import com.ebay.plugins.graph.analytics.validation.matchers.GraphMatchers.equalTo as graphEqualTo 13 | 14 | class VertexPathGraphMatcherTest { 15 | private val vertex = VertexInfo(path = ":source") 16 | private val graph: DefaultDirectedGraph = mock() 17 | private val rootedVertex = RootedVertex(graph = graph, root = vertex) 18 | 19 | @Test 20 | fun success() { 21 | val uut = path(graphEqualTo(":source")) 22 | val actual = uut.matches(rootedVertex) 23 | println(actual.render()) 24 | assertThat(actual.matched, equalTo(true)) 25 | } 26 | 27 | @Test 28 | fun failure() { 29 | val uut = path(graphEqualTo(":notSource")) 30 | val actual = uut.matches(rootedVertex) 31 | println(actual.render()) 32 | assertThat(actual.matched, equalTo(false)) 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/resources/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | id("com.ebay.graph-analytics") 4 | } 5 | 6 | dependencies { 7 | // PRODUCTION DEPENDENCIES PLACEHOLDER 8 | // TEST DEPENDENCIES PLACEHOLDER 9 | } -------------------------------------------------------------------------------- /src/test/resources/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "graph-analytics-plugin-test" 2 | 3 | // INCLUDE PLACEHOLDER --------------------------------------------------------------------------------