├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── plugin ├── src │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── jraska │ │ │ └── module │ │ │ └── graph │ │ │ ├── GraphStatistics.kt │ │ │ ├── assertion │ │ │ ├── GraphAssert.kt │ │ │ ├── tasks │ │ │ │ ├── AssertGraphTask.kt │ │ │ │ ├── GenerateModulesGraphStatisticsTask.kt │ │ │ │ └── GenerateModulesGraphTask.kt │ │ │ ├── GraphRulesExtension.kt │ │ │ ├── GradleModuleAliasExtractor.kt │ │ │ ├── Api.kt │ │ │ ├── OnlyAllowedAssert.kt │ │ │ ├── RestrictedDependenciesAssert.kt │ │ │ ├── ModuleTreeHeightAssert.kt │ │ │ ├── ModuleDependency.kt │ │ │ ├── GradleDependencyGraphFactory.kt │ │ │ └── ModuleGraphAssertionsPlugin.kt │ │ │ ├── LongestPath.kt │ │ │ ├── RegexpDependencyMatcher.kt │ │ │ ├── Parse.kt │ │ │ ├── GraphvizWriter.kt │ │ │ └── DependencyGraph.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── jraska │ │ └── module │ │ └── graph │ │ ├── assertion │ │ ├── ModuleDependencyTest.kt │ │ ├── ModuleTreeHeightAssertTest.kt │ │ ├── ConfigurationAvoidanceTest.kt │ │ ├── GradleDependencyGraphFactorySingleModuleProjectTest.kt │ │ ├── OnlyAllowedAssertTest.kt │ │ ├── FullProjectMultipleAppliedGradleTest.kt │ │ ├── RestrictedDependenciesAssertTest.kt │ │ ├── ModuleGraphAssertionsPluginTest.kt │ │ ├── FullProjectRootGradleTest.kt │ │ ├── GradleDependencyGraphFactoryTest.kt │ │ ├── OnAnyBuildAssertTest.kt │ │ └── FullProjectGradleTest.kt │ │ ├── DependencyGraphSerializationTest.kt │ │ ├── GraphvizWriterTest.kt │ │ ├── ParseTest.kt │ │ ├── DependencyGraphPerformanceTest.kt │ │ └── DependencyGraphTest.kt └── build.gradle ├── gradle.properties ├── presentation ├── REFERENCES-LONDON-2023.md ├── REFERENCES-LONDON-2022.md └── REFERENCES.md ├── .gitignore ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':plugin' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jraska/modules-graph-assert/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/GraphStatistics.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | data class GraphStatistics( 4 | val modulesCount: Int, 5 | val edgesCount: Int, 6 | val height: Int, 7 | val longestPath: LongestPath 8 | ) 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout the code 8 | uses: actions/checkout@v3 9 | - name: Run Tests 10 | run: ./gradlew check --stacktrace 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | # Default value: -Xmx1024m -XX:MaxPermSize=256m 3 | org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 4 | org.gradle.parallel=true 5 | org.gradle.daemon=true 6 | VERSION_NAME=0.1.0 7 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/GraphAssert.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import java.io.Serializable 5 | 6 | interface GraphAssert : Serializable { 7 | fun assert(dependencyGraph: DependencyGraph) 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/LongestPath.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | data class LongestPath( 4 | val nodeNames: List 5 | ) { 6 | fun pathString(): String { 7 | return nodeNames.joinToString(" -> ") 8 | } 9 | 10 | override fun toString() = "'${pathString()}'" 11 | } 12 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/RegexpDependencyMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | class RegexpDependencyMatcher( 4 | private val matchingRegex: Regex, 5 | private val divider: String 6 | ) { 7 | 8 | fun matches(dependency: Pair): Boolean { 9 | val dependencyToMatch = "${dependency.first}$divider${dependency.second}" 10 | return matchingRegex.matches(dependencyToMatch) 11 | } 12 | 13 | override fun toString(): String { 14 | return matchingRegex.toString() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release To Gradle Plugins 2 | on: 3 | release: 4 | types: [published] 5 | env: 6 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 7 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout the code 13 | uses: actions/checkout@v3 14 | - name: Publish Release 15 | run: ./gradlew publishPlugins -Pgradle.publish.key=$GRADLE_PUBLISH_KEY -Pgradle.publish.secret=$GRADLE_PUBLISH_SECRET --stacktrace 16 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/tasks/AssertGraphTask.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion.tasks 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import com.jraska.module.graph.assertion.GraphAssert 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.TaskAction 8 | 9 | open class AssertGraphTask : DefaultTask() { 10 | 11 | @Input 12 | lateinit var assertion: GraphAssert 13 | 14 | @Input 15 | lateinit var dependencyGraph: DependencyGraph.SerializableGraph 16 | 17 | @TaskAction 18 | fun run() { 19 | val modulesTree = DependencyGraph.create(dependencyGraph) 20 | 21 | assertion.assert(modulesTree) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/GraphRulesExtension.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | open class GraphRulesExtension { 4 | var maxHeight: Int = 0 5 | var restricted = emptyArray() // each restriction in format "regexp -X> regexp" e.g.: ":feature-[a-z]* -X> :forbidden-lib" 6 | var allowed = emptyArray() // each allowance in format "regexp -> regexp" e.g.: ":feature-[a-z]* -> :forbidden-lib" 7 | var configurations: Set = Api.API_IMPLEMENTATION_CONFIGURATIONS 8 | var assertOnAnyBuild: Boolean = false 9 | 10 | internal fun shouldAssertHeight() = maxHeight > 0 11 | 12 | internal fun shouldAssertRestricted() = restricted.isNotEmpty() 13 | 14 | internal fun shouldAssertAllowed() = allowed.isNotEmpty() 15 | } 16 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/GradleModuleAliasExtractor.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import org.gradle.api.Project 4 | 5 | object GradleModuleAliasExtractor { 6 | fun extractModuleAliases(project: Project): Map { 7 | val rootProject = project.rootProject 8 | return (rootProject.subprojects + rootProject) 9 | .mapNotNull { alias(it) } 10 | .toMap() 11 | } 12 | 13 | private fun alias(project: Project): Pair? { 14 | if (project.hasProperty(Api.Properties.MODULE_NAME_ALIAS)) { 15 | val moduleAlias = project.property(Api.Properties.MODULE_NAME_ALIAS) as String 16 | return project.moduleDisplayName() to moduleAlias 17 | } else { 18 | return null 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/Parse.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | object Parse { 4 | const val NO_DEPENDENCY_SIGN_DIVIDER = " -X> " 5 | const val DEPENDENCY_SIGN_DIVIDER = " -> " 6 | 7 | fun matcher(dependencyText: String): RegexpDependencyMatcher { 8 | return matcher(dependencyText, DEPENDENCY_SIGN_DIVIDER) 9 | } 10 | 11 | fun restrictiveMatcher(matcherText: String): RegexpDependencyMatcher { 12 | return matcher(matcherText, NO_DEPENDENCY_SIGN_DIVIDER) 13 | } 14 | 15 | private fun matcher(matcherText: String, divider: String): RegexpDependencyMatcher { 16 | if (!matcherText.contains(divider)) { 17 | throw IllegalArgumentException("Incorrect format. Expected: 'regexp${divider}regexp', found $matcherText") 18 | } 19 | 20 | return RegexpDependencyMatcher(matcherText.toRegex(), divider) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/ModuleDependencyTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import org.junit.Test 4 | 5 | class ModuleDependencyTest { 6 | @Test 7 | fun displayTextWorksWithAliases() { 8 | val displayText = ModuleDependency(":feature" to ":core-api", "Impl", "Api").assertDisplayText() 9 | 10 | assert(displayText == """"Impl"(':feature') -> "Api"(':core-api')""") 11 | } 12 | 13 | @Test 14 | fun displayTextWorksWithOneAlias() { 15 | val displayText = ModuleDependency(":feature" to ":core-api", null, "Api").assertDisplayText() 16 | 17 | assert(displayText == """':feature' -> "Api"(':core-api')""") 18 | } 19 | 20 | @Test 21 | fun displayTextWorksWithNoAliases() { 22 | val displayText = ModuleDependency(":feature" to ":core-api", null, null).assertDisplayText() 23 | 24 | assert(displayText == "':feature' -> ':core-api'") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/tasks/GenerateModulesGraphStatisticsTask.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion.tasks 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.tasks.Input 6 | import org.gradle.api.tasks.Optional 7 | import org.gradle.api.tasks.TaskAction 8 | 9 | open class GenerateModulesGraphStatisticsTask : DefaultTask() { 10 | 11 | @Optional 12 | @Input 13 | var onlyModuleStatistics: String? = null 14 | 15 | @Input 16 | lateinit var dependencyGraph: DependencyGraph.SerializableGraph 17 | 18 | @TaskAction 19 | fun run() { 20 | val dependencyGraph = DependencyGraph.create(dependencyGraph) 21 | 22 | if (onlyModuleStatistics == null) { 23 | println(dependencyGraph.statistics()) 24 | } else { 25 | println(dependencyGraph.subTree(onlyModuleStatistics!!).statistics()) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/Api.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | object Api { 4 | object Tasks { 5 | const val GENERATE_GRAPHVIZ = "generateModulesGraphvizText" 6 | const val GENERATE_GRAPH_STATISTICS = "generateModulesGraphStatistics" 7 | 8 | const val ASSERT_ALL = "assertModuleGraph" 9 | const val ASSERT_MAX_HEIGHT = "assertMaxHeight" 10 | const val ASSERT_RESTRICTIONS = "assertRestrictions" 11 | const val ASSERT_ALLOWED = "assertAllowedModuleDependencies" 12 | } 13 | 14 | object Parameters { 15 | const val PRINT_ONLY_MODULE = "modules.graph.of.module" 16 | const val OUTPUT_PATH = "modules.graph.output.gv" 17 | } 18 | 19 | object Properties { 20 | const val MODULE_NAME_ALIAS = "moduleNameAssertAlias" 21 | } 22 | 23 | const val EXTENSION_ROOT = "moduleGraphAssert" 24 | 25 | val API_IMPLEMENTATION_CONFIGURATIONS = setOf("api", "implementation") 26 | } 27 | 28 | -------------------------------------------------------------------------------- /presentation/REFERENCES-LONDON-2023.md: -------------------------------------------------------------------------------- 1 | ### References to support the Droidcon London 2023: Mobile Observability at scale. (*[Video link](https://www.droidcon.com/2023/11/15/mobile-observability-at-scale/)*) 2 | 3 | - [The slides of the presentataion](https://docs.google.com/presentation/d/1ZZMT4IXQQV5_fN-ESLX9egJrwSJivv7nCIYLfawTYfk/edit?usp=sharing) 4 | - [Google SRE book](https://sre.google/sre-book/table-of-contents/) - [Embracing Risk](https://sre.google/sre-book/embracing-risk/): On error budgets 5 | - [Google SRE book](https://sre.google/sre-book/table-of-contents/) - [Effective Troubleshooting](https://sre.google/sre-book/effective-troubleshooting/) 6 | - [SLA/SLO/SLI explanation](https://www.atlassian.com/incident-management/kpis/sla-vs-slo-vs-sli) 7 | - [Retrofit Error Logging `Converter.Factory`](https://github.com/jraska/github-client/blob/master/core/src/main/java/com/jraska/github/client/http/ErrorLoggingConverterFactory.kt) 8 | - [Open Telemetry](https://opentelemetry.io/) 9 | -------------------------------------------------------------------------------- /presentation/REFERENCES-LONDON-2022.md: -------------------------------------------------------------------------------- 1 | ### References to support the Droidcon London 2022 talk: Modularization – flatten your graph – get the benefits. (*[Video link](https://www.droidcon.com/2022/11/15/modularization-flatten-your-graph-and-get-the-real-benefits/)*) 2 | 3 | - [The slides of the presentataion](https://docs.google.com/presentation/d/1_c3SsyXBUbCSn9RHL7PjRfprNzxlYr_IjRbqQvEqsOk/edit?usp=sharing) 4 | - [Example app implementing the presented practices](https://github.com/jraska/github-client) 5 | - [About module rules](https://proandroiddev.com/module-rules-protect-your-build-time-and-architecture-d1194c7cc6bc) 6 | - [Modules graph assert plugin](https://github.com/jraska/modules-graph-assert) 7 | - [Modularization - Untangling the dependency graph by Siggi Jonsson](https://speakerdeck.com/siggijons/modularization-siggi-jonsson) 8 | - [Android modularization docs](https://developer.android.com/topic/modularization) 9 | - [Gephi](https://gephi.org/) 10 | - [IntelliJ IDEA/Android Studio dependency analysis](https://www.jetbrains.com/help/idea/dependencies-analysis.html#analyze-dependencies) 11 | - [Longest path](https://en.wikipedia.org/wiki/Longest_path_problem) 12 | - [Node height](https://en.wikipedia.org/wiki/Glossary_of_graph_theory#height) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | bin/ 3 | gen/ 4 | 5 | # Gradle files 6 | .gradle/ 7 | build/ 8 | 9 | # Local configuration file (sdk path, etc) 10 | local.properties 11 | 12 | # Proguard folder generated by Eclipse 13 | proguard/ 14 | 15 | # Log Files 16 | *.log 17 | 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 19 | 20 | *.iml 21 | 22 | # Directory-based project format: 23 | .idea/ 24 | # if you remove the above rule, at least ignore the following: 25 | 26 | # User-specific stuff: 27 | # .idea/workspace.xml 28 | # .idea/tasks.xml 29 | # .idea/dictionaries 30 | 31 | # Sensitive or high-churn files: 32 | # .idea/dataSources.ids 33 | # .idea/dataSources.xml 34 | # .idea/sqlDataSources.xml 35 | # .idea/dynamic.xml 36 | # .idea/uiDesigner.xml 37 | 38 | # Gradle: 39 | # .idea/gradle.xml 40 | # .idea/libraries 41 | 42 | # Mongo Explorer plugin: 43 | # .idea/mongoSettings.xml 44 | 45 | ## File-based project format: 46 | *.ipr 47 | *.iws 48 | 49 | ## Plugin-specific files: 50 | 51 | # IntelliJ 52 | /out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Crashlytics plugin (for Android Studio and IntelliJ) 61 | com_crashlytics_export_strings.xml 62 | crashlytics.properties 63 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/GraphvizWriter.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | import com.jraska.module.graph.assertion.mapAlias 4 | 5 | object GraphvizWriter { 6 | fun toGraphviz(dependencyGraph: DependencyGraph, aliases: Map = emptyMap()): String { 7 | 8 | val longestPathConnections = dependencyGraph.longestPath() 9 | .nodeNames.zipWithNext() 10 | .toSet() 11 | 12 | val stringBuilder = StringBuilder() 13 | 14 | stringBuilder.append("digraph G {\n") 15 | 16 | val dependencyPairs = dependencyGraph.dependencyPairs() 17 | if(dependencyPairs.isEmpty()) { 18 | stringBuilder.append("\"${dependencyGraph.findRoot().key}\"") 19 | .append("\n") 20 | } 21 | 22 | dependencyPairs 23 | .map { aliases.mapAlias(it) } 24 | .forEach { connection -> 25 | stringBuilder.append("\"${connection.fromDocText()}\"") 26 | .append(" -> ") 27 | .append("\"${connection.toDocText()}\"") 28 | 29 | if (longestPathConnections.contains(connection.dependencyPair)) { 30 | stringBuilder.append(" [color=red style=bold]") 31 | } 32 | 33 | stringBuilder.append("\n") 34 | } 35 | 36 | stringBuilder.append("}") 37 | 38 | return stringBuilder.toString() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/OnlyAllowedAssert.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import com.jraska.module.graph.Parse 5 | import org.gradle.api.tasks.VerificationException 6 | 7 | class OnlyAllowedAssert( 8 | private val allowedDependencies: Array, 9 | private val aliasMap: Map = emptyMap(), 10 | ) : GraphAssert { 11 | override fun assert(dependencyGraph: DependencyGraph) { 12 | val matchers = allowedDependencies.map { Parse.matcher(it) } 13 | 14 | val disallowedDependencies = dependencyGraph.dependencyPairs() 15 | .map { aliasMap.mapAlias(it) } 16 | .filterNot { dependency -> matchers.any { it.matches(dependency.pairToAssert()) } } 17 | .map { it.assertDisplayText() } 18 | 19 | if (disallowedDependencies.isNotEmpty()) { 20 | val allowedRules = allowedDependencies.joinToString(", ") { "'$it'" } 21 | throw VerificationException("$disallowedDependencies not allowed by any of [$allowedRules]") 22 | } 23 | } 24 | } 25 | 26 | fun Map.mapAlias(dependencyPair: Pair): ModuleDependency { 27 | val fromAlias = this[dependencyPair.first] 28 | val toAlias = this[dependencyPair.second] 29 | 30 | return ModuleDependency(dependencyPair, fromAlias, toAlias) 31 | } 32 | -------------------------------------------------------------------------------- /presentation/REFERENCES.md: -------------------------------------------------------------------------------- 1 | ### References to support the Droidcon Berlin 2021 talk: Nail your Gradle build time. ([Video Link](https://www.droidcon.com/2021/11/10/nail-your-gradle-build-time/)) 2 | 3 | - [Presentation slides](https://docs.google.com/presentation/d/1NXNFT26CaxXYy3GUcBA594ZcCEv-BEongH3TMZai1Tw/edit?usp=sharing) 4 | - [About module rules](https://proandroiddev.com/module-rules-protect-your-build-time-and-architecture-d1194c7cc6bc) 5 | - [Why not to use buildSrc](https://proandroiddev.com/stop-using-gradle-buildsrc-use-composite-builds-instead-3c38ac7a2ab3) 6 | - [Modules graph assert plugin](https://github.com/jraska/modules-graph-assert) 7 | - [Build time plugin example](https://github.com/jraska/github-client/tree/master/plugins/src/main/java/com/jraska/gradle/buildtime) 8 | - [Collecting build Data from Gradle](https://git.io/JKjUB) 9 | - [Gradle Profiling](https://developer.android.com/studio/build/profile-your-build#gradle-profile-option) 10 | - [About Gradle scans](https://scans.gradle.com/) 11 | - [Example Gradle scan to play around](https://scans.gradle.com/s/ydg7r3qbmoieq/timeline?details=odfxjrm7pa5ps) 12 | - [Example Gradle scan with Remote cache hits](https://scans.gradle.com/s/owqoklhlsd7ds/performance/build-cache?anchor=eyJpZCI6InJlbW90ZS1oaXQifQ&cacheDetails=remote-hit) 13 | - [Display a dialog from Gradle](https://stackoverflow.com/a/42225803/3080924) 14 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.gradle.plugin-publish" version "1.3.1" 3 | id "java-gradle-plugin" 4 | } 5 | 6 | apply plugin: 'kotlin' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation gradleApi() 14 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 15 | 16 | testImplementation 'junit:junit:4.13.2' 17 | } 18 | 19 | compileKotlin { 20 | kotlinOptions { 21 | jvmTarget = JavaVersion.VERSION_11 22 | } 23 | } 24 | compileTestKotlin { 25 | kotlinOptions { 26 | jvmTarget = JavaVersion.VERSION_11 27 | } 28 | } 29 | java { 30 | sourceCompatibility JavaVersion.VERSION_11 31 | targetCompatibility JavaVersion.VERSION_11 32 | } 33 | 34 | group = 'com.jraska.module.graph.assertion' 35 | 36 | gradlePlugin { 37 | website = 'https://github.com/jraska/modules-graph-assert' 38 | vcsUrl = 'https://github.com/jraska/modules-graph-assert' 39 | 40 | plugins { 41 | modulesGraphAssert { 42 | id = 'com.jraska.module.graph.assertion' 43 | version = '2.9.0' 44 | displayName = 'Modules Graph Assert' 45 | description = 'Gradle plugin to keep your modules graph healthy and lean.' 46 | implementationClass = 'com.jraska.module.graph.assertion.ModuleGraphAssertionsPlugin' 47 | tags.addAll('graph', 'assert', 'build speed', 'android', 'java', 'kotlin', 'quality', 'multiprojects', 'module') 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/RestrictedDependenciesAssert.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import com.jraska.module.graph.Parse 5 | import com.jraska.module.graph.RegexpDependencyMatcher 6 | import org.gradle.api.tasks.VerificationException 7 | 8 | class RestrictedDependenciesAssert( 9 | private val errorMatchers: Array, 10 | private val aliasMap: Map = emptyMap() 11 | ) : GraphAssert { 12 | override fun assert(dependencyGraph: DependencyGraph) { 13 | val matchers = errorMatchers.map { Parse.restrictiveMatcher(it) } 14 | 15 | val failedDependencies = dependencyGraph.dependencyPairs() 16 | .map { aliasMap.mapAlias(it) } 17 | .map { dependency -> 18 | val violations = matchers.filter { it.matches(dependency.pairToAssert()) }.toList() 19 | dependency to violations 20 | }.filter { it.second.isNotEmpty() } 21 | 22 | if (failedDependencies.isNotEmpty()) { 23 | throw VerificationException(buildErrorMessage(failedDependencies)) 24 | } 25 | } 26 | 27 | private fun buildErrorMessage(failedDependencies: List>>): String { 28 | return failedDependencies.map { 29 | val violatedRules = it.second.map { "'$it'" }.joinToString(", ") 30 | "Dependency '${it.first.assertDisplayText()} violates: $violatedRules" 31 | }.joinToString("\n") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/DependencyGraphSerializationTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | import org.junit.Test 4 | import java.io.ByteArrayInputStream 5 | import java.io.ByteArrayOutputStream 6 | import java.io.ObjectInputStream 7 | import java.io.ObjectOutputStream 8 | 9 | class DependencyGraphSerializationTest { 10 | @Test 11 | fun singularGraphIsSerializable() { 12 | val originalGraph = DependencyGraph.createSingular(":app") 13 | val deserializedGraph = serializeAndDeserializeGraph(originalGraph) 14 | 15 | assert(deserializedGraph.statistics() == originalGraph.statistics()) 16 | } 17 | 18 | @Test 19 | fun graphIsSerializable() { 20 | val originalGraph = DependencyGraph.create( 21 | "feature" to "lib", 22 | "lib" to "core", 23 | "app" to "feature", 24 | "feature" to "core", 25 | "app" to "core" 26 | ) 27 | 28 | val deserializedGraph = serializeAndDeserializeGraph(originalGraph) 29 | 30 | assert(deserializedGraph.statistics() == originalGraph.statistics()) 31 | } 32 | 33 | private fun serializeAndDeserializeGraph(originalGraph: DependencyGraph): DependencyGraph { 34 | val byteArray = ByteArrayOutputStream() 35 | ObjectOutputStream(byteArray).writeObject(originalGraph.serializableGraph()) 36 | 37 | val deserialized = ObjectInputStream(ByteArrayInputStream(byteArray.toByteArray())).readObject() 38 | return DependencyGraph.create(deserialized as DependencyGraph.SerializableGraph) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/ModuleTreeHeightAssert.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import org.gradle.api.tasks.VerificationException 5 | import java.io.Serializable 6 | 7 | class ModuleTreeHeightAssert( 8 | private val moduleName: String?, 9 | private val maxHeight: Int 10 | ) : GraphAssert, Serializable { 11 | override fun assert(dependencyGraph: DependencyGraph) { 12 | if (moduleName == null) { 13 | assertWholeGraphHeight(dependencyGraph) 14 | } else { 15 | assertModuleHeight(dependencyGraph, moduleName) 16 | } 17 | } 18 | 19 | private fun assertModuleHeight(dependencyGraph: DependencyGraph, moduleName: String) { 20 | val height = dependencyGraph.heightOf(moduleName) 21 | if (height > maxHeight) { 22 | val longestPath = dependencyGraph.longestPath(moduleName) 23 | throw VerificationException("Module $moduleName is allowed to have maximum height of $maxHeight, but has $height, problematic dependencies: ${longestPath.pathString()}") 24 | } 25 | } 26 | 27 | private fun assertWholeGraphHeight(dependencyGraph: DependencyGraph) { 28 | val height = dependencyGraph.height() 29 | if (height > maxHeight) { 30 | val longestPath = dependencyGraph.longestPath() 31 | throw VerificationException("Module Graph is allowed to have maximum height of $maxHeight, but has $height, problematic dependencies: ${longestPath.pathString()}") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/ModuleDependency.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | class ModuleDependency( 4 | val dependencyPair: Pair, 5 | private val fromAlias: String?, 6 | private val toAlias: String? 7 | ) { 8 | private val from get() = dependencyPair.first 9 | private val to get() = dependencyPair.second 10 | 11 | fun pairToAssert(): Pair { 12 | if (fromAlias != null || toAlias != null) { 13 | return (fromAlias ?: from) to (toAlias ?: to) 14 | } else { 15 | return dependencyPair 16 | } 17 | } 18 | 19 | fun assertDisplayText(): String { 20 | val stringBuilder = StringBuilder() 21 | 22 | if (fromAlias != null) { 23 | stringBuilder.append("\"$fromAlias\"") 24 | stringBuilder.append("('${from}')") 25 | } else { 26 | stringBuilder.append("'${from}'") 27 | } 28 | 29 | stringBuilder.append(" -> ") 30 | 31 | if (toAlias != null) { 32 | stringBuilder.append("\"$toAlias\"") 33 | stringBuilder.append("('${to}')") 34 | } else { 35 | stringBuilder.append("'${to}'") 36 | } 37 | 38 | return stringBuilder.toString() 39 | } 40 | 41 | fun fromDocText(): String { 42 | return if (fromAlias != null) { 43 | "$from('$fromAlias')" 44 | } else { 45 | from 46 | } 47 | } 48 | 49 | fun toDocText() : String { 50 | return if (toAlias != null) { 51 | "$to('$toAlias')" 52 | } else { 53 | to 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/ModuleTreeHeightAssertTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import org.gradle.api.tasks.VerificationException 5 | import org.junit.Test 6 | 7 | class ModuleTreeHeightAssertTest { 8 | @Test 9 | fun passesOnCorrectTree() { 10 | val dependencyGraph = testGraph() 11 | 12 | ModuleTreeHeightAssert("app", 3).assert(dependencyGraph) 13 | } 14 | 15 | @Test(expected = VerificationException::class) 16 | fun failsOnTooLargeHeight() { 17 | val dependencyGraph = testGraph() 18 | 19 | ModuleTreeHeightAssert("app", 2).assert(dependencyGraph) 20 | } 21 | 22 | @Test 23 | fun passesOnCorrectRoot() { 24 | val dependencyGraph = testGraph() 25 | 26 | ModuleTreeHeightAssert(null, 3).assert(dependencyGraph) 27 | } 28 | 29 | @Test(expected = VerificationException::class) 30 | fun failsOnTooHighGraphHeight() { 31 | val dependencyGraph = testGraph() 32 | 33 | ModuleTreeHeightAssert(null, 2).assert(dependencyGraph) 34 | } 35 | 36 | @Test(expected = NoSuchElementException::class) 37 | fun failsOnUnknownModule() { 38 | val dependencyGraph = testGraph() 39 | 40 | ModuleTreeHeightAssert("appx", 2).assert(dependencyGraph) 41 | } 42 | 43 | private fun testGraph(): DependencyGraph { 44 | return DependencyGraph.create( 45 | "app" to "feature", 46 | "app" to "lib", 47 | "feature" to "lib", 48 | "lib" to "core" 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/ConfigurationAvoidanceTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.GradleRunner 5 | import org.junit.Before 6 | import org.junit.Rule 7 | import org.junit.Test 8 | import org.junit.rules.TemporaryFolder 9 | import java.io.File 10 | 11 | class ConfigurationAvoidanceTest { 12 | @get:Rule 13 | val testProjectDir: TemporaryFolder = TemporaryFolder() 14 | 15 | @Before 16 | fun setup() { 17 | testProjectDir.newFile("build.gradle").writeText( 18 | """ 19 | plugins { 20 | id 'com.jraska.module.graph.assertion' 21 | } 22 | 23 | moduleGraphAssert { 24 | maxHeight = 3 25 | allowed = [':app -> .*', ':feature-\\S* -> :lib\\S*', '.* -> :core', ':feature-one -> :feature-exclusion-test', ':feature-one -> :feature-one:nested'] 26 | restricted = [':feature-[a-z]* -X> :feature-[a-z]*'] 27 | } 28 | 29 | ext.moduleNameAssertAlias = "Alias" 30 | """ 31 | ) 32 | } 33 | 34 | @Test 35 | fun tasksAreUpToDate() { 36 | runGradleAssertModuleGraph(testProjectDir.root) 37 | val secondRunResult = runGradleAssertModuleGraph(testProjectDir.root) 38 | 39 | val findAll = Regex("UP-TO-DATE").findAll(secondRunResult.output) 40 | assert(findAll.count() == 4) 41 | } 42 | } 43 | 44 | fun runGradleAssertModuleGraph(dir: File, vararg arguments: String = arrayOf("--configuration-cache", "assertModuleGraph")): BuildResult { 45 | return GradleRunner.create() 46 | .withProjectDir(dir) 47 | .withPluginClasspath() 48 | .withArguments(arguments.asList()) 49 | .build() 50 | } 51 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/GraphvizWriterTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | import org.junit.Test 4 | 5 | class GraphvizWriterTest { 6 | 7 | @Test 8 | fun testPrintsProperly() { 9 | val graphvizText = GraphvizWriter.toGraphviz(testGraph()) 10 | 11 | assert(graphvizText == EXPECTED_OUTPUT) 12 | } 13 | 14 | @Test 15 | fun testPrintsProperlyWithAliases() { 16 | val aliases = mapOf( 17 | "app" to "App", 18 | "lib" to "Api", 19 | "feature-about" to "Implementation" 20 | ) 21 | 22 | val graphvizText = GraphvizWriter.toGraphviz(testGraph(), aliases) 23 | 24 | assert(graphvizText == EXPECTED_OUTPUT_WITH_ALISASES) 25 | } 26 | 27 | private fun testGraph(): DependencyGraph { 28 | return DependencyGraph.create( 29 | "app" to "feature", 30 | "app" to "feature-about", 31 | "feature-about" to "lib", 32 | "feature-about" to "core", 33 | "app" to "lib", 34 | "feature" to "lib", 35 | "lib" to "core" 36 | ) 37 | } 38 | } 39 | 40 | private const val EXPECTED_OUTPUT = """digraph G { 41 | "app" -> "feature" [color=red style=bold] 42 | "app" -> "feature-about" 43 | "app" -> "lib" 44 | "feature" -> "lib" [color=red style=bold] 45 | "feature-about" -> "lib" 46 | "feature-about" -> "core" 47 | "lib" -> "core" [color=red style=bold] 48 | }""" 49 | 50 | private const val EXPECTED_OUTPUT_WITH_ALISASES = """digraph G { 51 | "app('App')" -> "feature" [color=red style=bold] 52 | "app('App')" -> "feature-about('Implementation')" 53 | "app('App')" -> "lib('Api')" 54 | "feature" -> "lib('Api')" [color=red style=bold] 55 | "feature-about('Implementation')" -> "lib('Api')" 56 | "feature-about('Implementation')" -> "core" 57 | "lib('Api')" -> "core" [color=red style=bold] 58 | }""" 59 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/GradleDependencyGraphFactory.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import org.gradle.api.artifacts.ProjectDependency 5 | import org.gradle.api.Project 6 | 7 | object GradleDependencyGraphFactory { 8 | 9 | fun create(project: Project, configurationsToLook: Set): DependencyGraph { 10 | val modulesWithDependencies = project.listAllDependencies(configurationsToLook) 11 | val dependencies = modulesWithDependencies.flatMap { module -> 12 | module.second.map { module.first to it } 13 | } 14 | 15 | val moduleDisplayName = project.moduleDisplayName() 16 | if(dependencies.isEmpty()) { 17 | return DependencyGraph.createSingular(moduleDisplayName) 18 | } 19 | 20 | val fullDependencyGraph = DependencyGraph.create(dependencies) 21 | 22 | if (project == project.rootProject) { 23 | return fullDependencyGraph 24 | } 25 | 26 | modulesWithDependencies.find { it.first == moduleDisplayName && it.second.isNotEmpty() } 27 | ?: return DependencyGraph.createSingular(moduleDisplayName) 28 | 29 | return fullDependencyGraph.subTree(moduleDisplayName) 30 | } 31 | 32 | private fun Project.listAllDependencies(configurationsToLook: Set): List>> { 33 | return (rootProject.subprojects + rootProject) 34 | .map { project -> 35 | project.moduleDisplayName() to project.configurations 36 | .filter { configurationsToLook.contains(it.name) } 37 | .flatMap { configuration -> 38 | configuration.dependencies.filterIsInstance() 39 | .map { project.project(it.path) } 40 | } 41 | .map { it.moduleDisplayName() } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/GradleDependencyGraphFactorySingleModuleProjectTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.GraphvizWriter 4 | import org.gradle.api.internal.project.DefaultProject 5 | import org.gradle.api.plugins.JavaLibraryPlugin 6 | import org.gradle.testfixtures.ProjectBuilder 7 | import org.junit.Before 8 | import org.junit.Test 9 | 10 | class GradleDependencyGraphFactorySingleModuleProjectTest { 11 | 12 | private val EXPECTED_SINGLE_MODULE = """digraph G { 13 | ":app" 14 | }""" 15 | 16 | private val EXPECTED_ROOT_MODULE = """digraph G { 17 | "root some-root" 18 | }""" 19 | 20 | private lateinit var singleModule: DefaultProject 21 | private var rootProject: DefaultProject? = null 22 | 23 | @Before 24 | fun setUp() { 25 | rootProject = createProject("some-root") 26 | singleModule = createProject("app") 27 | } 28 | 29 | @Test 30 | fun generatesProperGraph() { 31 | val dependencyGraph = GradleDependencyGraphFactory.create(singleModule, Api.API_IMPLEMENTATION_CONFIGURATIONS) 32 | 33 | val graphvizText = GraphvizWriter.toGraphviz(dependencyGraph) 34 | assert(EXPECTED_SINGLE_MODULE == graphvizText) 35 | } 36 | 37 | @Test 38 | fun generatesProperGraphOnRootModule() { 39 | val dependencyGraph = GradleDependencyGraphFactory.create(rootProject!!, Api.API_IMPLEMENTATION_CONFIGURATIONS) 40 | 41 | val graphvizText = GraphvizWriter.toGraphviz(dependencyGraph) 42 | assert(EXPECTED_ROOT_MODULE == graphvizText) 43 | } 44 | 45 | private fun createProject(name: String): DefaultProject { 46 | val project = ProjectBuilder.builder() 47 | .withName(name) 48 | .withParent(rootProject) 49 | .build() as DefaultProject 50 | 51 | project.plugins.apply(JavaLibraryPlugin::class.java) 52 | return project 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/ParseTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | import org.junit.Test 4 | 5 | class ParseTest { 6 | @Test 7 | fun parsesProperly() { 8 | val matcher = Parse.matcher(":one -> two") 9 | 10 | assert(matcher.matches(":one" to "two")) 11 | } 12 | 13 | @Test(expected = IllegalArgumentException::class) 14 | fun failsOnWrongFormat() { 15 | Parse.matcher(":one - > two") 16 | } 17 | 18 | @Test(expected = IllegalArgumentException::class) 19 | fun failsOnEmptyString() { 20 | Parse.matcher("") 21 | } 22 | 23 | @Test 24 | fun parsesMatcherProperly() { 25 | val matcher = Parse.restrictiveMatcher(":feature:[a-zA-Z]* -X> :lib") 26 | 27 | assert(matcher.matches(":feature:about" to ":lib")) 28 | assert(matcher.matches(":feature:aboutX" to ":lib")) 29 | assert(!matcher.matches(":feature:aboutX" to ":libx")) 30 | assert(!matcher.matches(":feature-about" to ":lib")) 31 | } 32 | 33 | @Test 34 | fun groupsMatchingIsSupported() { 35 | val groupingRegex = ":features:(\\S*):\\S* -> :features:\\1:\\S*" 36 | 37 | val matcher = Parse.matcher(groupingRegex) 38 | 39 | assert(matcher.matches(":features:X:Y" to ":features:X:Z")) 40 | assert(matcher.matches(":features:data:Y" to ":features:data:Z")) 41 | assert(!matcher.matches(":features:data:Y" to ":features:dat:Z")) 42 | } 43 | 44 | @Test 45 | fun groupsMatchingIsSupportedForRestricted() { 46 | val groupingRegex = ":features:(\\S*):(\\S*) -X> :features:\\1:\\2:\\1" 47 | 48 | val restrictiveMatcher = Parse.restrictiveMatcher(groupingRegex) 49 | 50 | assert(restrictiveMatcher.matches(":features:X:Y" to ":features:X:Y:X")) 51 | assert(restrictiveMatcher.matches(":features:data:core" to ":features:data:core:data")) 52 | assert(!restrictiveMatcher.matches(":features:X:Y" to ":features:X:Z")) 53 | assert(!restrictiveMatcher.matches(":features:data:Y" to ":features:dat:Z")) 54 | } 55 | 56 | @Test(expected = IllegalArgumentException::class) 57 | fun failsOnWrongDefinition() { 58 | Parse.restrictiveMatcher(":feature -> :lib") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/tasks/GenerateModulesGraphTask.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion.tasks 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import com.jraska.module.graph.GraphvizWriter 5 | import com.jraska.module.graph.assertion.Api 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.Project 8 | import org.gradle.api.tasks.Input 9 | import org.gradle.api.tasks.Optional 10 | import org.gradle.api.tasks.OutputFile 11 | import org.gradle.api.tasks.TaskAction 12 | import java.io.File 13 | 14 | open class GenerateModulesGraphTask : DefaultTask() { 15 | 16 | @Input 17 | lateinit var aliases: Map 18 | 19 | @Optional 20 | @Input 21 | var onlyModuleToPrint: String? = null 22 | 23 | @Input 24 | lateinit var dependencyGraph: DependencyGraph.SerializableGraph 25 | 26 | @Optional 27 | @Input 28 | var outputFilePath: String? = null 29 | 30 | @Optional 31 | @OutputFile 32 | var outputFile: File? = null 33 | 34 | @TaskAction 35 | fun run() { 36 | val dependencyGraph = DependencyGraph.create(dependencyGraph).let { 37 | if (onlyModuleToPrint == null) { 38 | it 39 | } else { 40 | it.subTree(onlyModuleToPrint!!) 41 | } 42 | } 43 | 44 | val graphviz = GraphvizWriter.toGraphviz(dependencyGraph, aliases) 45 | 46 | if (outputFilePath != null) { 47 | val file = File(outputFilePath!!) 48 | file.writeText(graphviz) 49 | outputFile = file 50 | println("GraphViz saved to $path") 51 | } else { 52 | println(graphviz) 53 | } 54 | } 55 | 56 | companion object { 57 | internal fun outputFilePath(project: Project): String? { 58 | if (project.hasProperty(Api.Parameters.OUTPUT_PATH)) { 59 | return project.property(Api.Parameters.OUTPUT_PATH).toString() 60 | } else { 61 | return null 62 | } 63 | } 64 | 65 | internal fun onlyModule(project: Project): String? { 66 | if (project.hasProperty(Api.Parameters.PRINT_ONLY_MODULE)) { 67 | return project.property(Api.Parameters.PRINT_ONLY_MODULE) as String? 68 | } else { 69 | return null 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/DependencyGraphPerformanceTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | import org.junit.Before 4 | import org.junit.Test 5 | import java.io.File 6 | 7 | class DependencyGraphPerformanceTest { 8 | lateinit var dependencyGraph: DependencyGraph 9 | 10 | @Before 11 | fun setUp() { 12 | val uri = javaClass.classLoader.getResource("graph/large-graph.txt") 13 | val file = File(uri?.path!!) 14 | val dependencyPairs = file.readLines().map { 15 | val parts = it.split(" -> ") 16 | parts[0] to parts[1] 17 | } 18 | 19 | dependencyGraph = DependencyGraph.create(dependencyPairs) 20 | } 21 | 22 | @Test(timeout = 1000) // 1000 ms is more than enough - it was taking hours before optimisations 23 | fun whenTheGraphIsLarge_statisticsCalculatedFast() { 24 | val statistics = dependencyGraph.statistics() 25 | 26 | assert(statistics.height == 59) 27 | assert(statistics.modulesCount == 1000) 28 | assert(statistics.edgesCount == 15259) 29 | assert(statistics.longestPath.pathString().startsWith("23 -> 31 -> 36 -> 57 -> 61 -> 72 -> 74 -> 75")) 30 | } 31 | 32 | @Test(timeout = 1_000) 33 | fun whenTheGraphIsLarge_statisticsOfSubgraphMatchFast() { 34 | val subGraphStatistics = dependencyGraph.subTree("31").statistics() 35 | 36 | assert(subGraphStatistics.height == 58) 37 | assert(subGraphStatistics.longestPath.pathString().startsWith("31 -> 36 -> 57 -> 61 -> 72 -> 74 -> 75")) 38 | } 39 | 40 | @Test(timeout = 1_000) 41 | fun whenTheGraphIsLarge_statisticsCreatedFast() { 42 | val subGraphStatistics = dependencyGraph.subTree("500").statistics() 43 | assert(subGraphStatistics.modulesCount == 281) 44 | } 45 | 46 | @Test(timeout = 1_000) // was running out of heap before optimisation 47 | fun whenTheGraphIsLarge_statisticsLargeCreatedFast() { 48 | val subGraphStatistics = dependencyGraph.subTree("2").statistics() 49 | 50 | assert(subGraphStatistics.modulesCount == 870) 51 | assert(subGraphStatistics.edgesCount == 11650) 52 | assert(subGraphStatistics.height == 55) 53 | assert(subGraphStatistics.longestPath.pathString().startsWith("2 -> 30 -> 76 -> 105 -> 119 -> ")) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/OnlyAllowedAssertTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import org.gradle.api.tasks.VerificationException 5 | import org.junit.Test 6 | 7 | class OnlyAllowedAssertTest { 8 | @Test(expected = VerificationException::class) 9 | fun failsWithNoMatchingMatchers() { 10 | val dependencyGraph = testGraph() 11 | 12 | OnlyAllowedAssert(emptyArray()).assert(dependencyGraph) 13 | } 14 | 15 | @Test 16 | fun passesWhenAllAllowed() { 17 | val dependencyGraph = testGraph() 18 | 19 | OnlyAllowedAssert(arrayOf(".* -> .*")).assert(dependencyGraph) 20 | } 21 | 22 | @Test 23 | fun passesWhenAllowed() { 24 | val dependencyGraph = testGraph() 25 | 26 | val allowedDependencies = arrayOf( 27 | "app -> .*", 28 | "feature[a-z]* -> lib[0-9]*", 29 | "feature[a-z]* -> api[0-9]*", 30 | "api[0-9]* -> lib", 31 | ) 32 | 33 | OnlyAllowedAssert(allowedDependencies).assert(dependencyGraph) 34 | } 35 | 36 | @Test(expected = VerificationException::class) 37 | fun failsWhenOneNotAllowed() { 38 | val dependencies = testGraph().dependencyPairs().toMutableList().apply { add("api" to "lib2") } 39 | val dependencyGraph = DependencyGraph.create(dependencies) 40 | 41 | val allowedDependencies = arrayOf( 42 | "app -> .*", 43 | "feature[a-z]* -> lib[0-9]*", 44 | "feature[a-z]* -> api[0-9]*", 45 | "api[0-9]* -> lib", 46 | ) 47 | 48 | OnlyAllowedAssert(allowedDependencies).assert(dependencyGraph) 49 | } 50 | 51 | @Test 52 | fun passesWhenAllowedWithAlias() { 53 | val dependencyGraph = testGraph() 54 | 55 | val allowedDependencies = arrayOf( 56 | "App -> .*", 57 | "Impl -> Api", 58 | "Api -> Api" 59 | ) 60 | val aliases = mapOf( 61 | "app" to "App", 62 | "feature" to "Impl", 63 | "feature2" to "Impl", 64 | "api" to "Api", 65 | "api2" to "Api", 66 | "lib" to "Api", 67 | "lib2" to "Api", 68 | ) 69 | 70 | OnlyAllowedAssert(allowedDependencies, aliases).assert(dependencyGraph) 71 | } 72 | 73 | private fun testGraph(): DependencyGraph { 74 | return DependencyGraph.create( 75 | "app" to "feature", 76 | "app" to "feature2", 77 | "app" to "api", 78 | "feature" to "api", 79 | "feature" to "api2", 80 | "api" to "lib", 81 | "api2" to "lib", 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/FullProjectMultipleAppliedGradleTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import org.junit.Before 4 | import org.junit.Rule 5 | import org.junit.Test 6 | import org.junit.rules.TemporaryFolder 7 | import java.io.File 8 | 9 | class FullProjectMultipleAppliedGradleTest { 10 | @get:Rule 11 | val testProjectDir: TemporaryFolder = TemporaryFolder() 12 | 13 | @Before 14 | fun setup() { 15 | testProjectDir.newFile("settings.gradle") 16 | .writeText("include ':app', ':core', 'core-api', 'no-dependencies'") 17 | 18 | createModule( 19 | "core-api", content = """ 20 | apply plugin: 'java-library' 21 | """ 22 | ) 23 | 24 | createModule( 25 | "core", content = """ 26 | apply plugin: 'java-library' 27 | 28 | dependencies { 29 | implementation project(":core-api") 30 | } 31 | """ 32 | ) 33 | 34 | createModule( 35 | "app", content = """ 36 | plugins { 37 | id 'com.jraska.module.graph.assertion' 38 | } 39 | apply plugin: 'java-library' 40 | 41 | moduleGraphAssert { 42 | maxHeight = 2 43 | } 44 | 45 | dependencies { 46 | implementation project(":core-api") 47 | implementation project(":core") 48 | } 49 | """ 50 | ) 51 | 52 | createModule( 53 | "no-dependencies", content = """ 54 | plugins { 55 | id 'com.jraska.module.graph.assertion' 56 | } 57 | apply plugin: 'java-library' 58 | 59 | moduleGraphAssert { 60 | maxHeight = 0 61 | } 62 | """ 63 | ) 64 | } 65 | 66 | @Test 67 | fun printsBothCorrectStatistics() { 68 | val output = 69 | runGradleAssertModuleGraph(testProjectDir.root, "generateModulesGraphStatistics").output 70 | 71 | println(output) 72 | assert(output.contains("> Task :app:generateModulesGraphStatistics\n" + 73 | "GraphStatistics(modulesCount=3, edgesCount=3, height=2, longestPath=':app -> :core -> :core-api')\n" + 74 | "\n" + 75 | "> Task :no-dependencies:generateModulesGraphStatistics\n" + 76 | "GraphStatistics(modulesCount=1, edgesCount=0, height=0, longestPath=':no-dependencies')")) 77 | } 78 | 79 | private fun createModule(dir: String, content: String) { 80 | val newFolder = testProjectDir.newFolder(dir) 81 | File(newFolder, "build.gradle").writeText(content) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/RestrictedDependenciesAssertTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import org.gradle.api.tasks.VerificationException 5 | import org.junit.Test 6 | 7 | class RestrictedDependenciesAssertTest { 8 | @Test 9 | fun passesWithNoMatchingMatchers() { 10 | val dependencyGraph = testGraph() 11 | 12 | RestrictedDependenciesAssert(emptyArray()).assert(dependencyGraph) 13 | } 14 | 15 | @Test(expected = VerificationException::class) 16 | fun failsWhenFeatureCannotDependOnLib() { 17 | val dependencyGraph = testGraph() 18 | 19 | RestrictedDependenciesAssert(arrayOf("feature -X> lib2")).assert(dependencyGraph) 20 | } 21 | 22 | @Test(expected = VerificationException::class) 23 | fun failsWhenLibCannotDependOnAndroid() { 24 | val dependencyGraph = testGraph() 25 | 26 | RestrictedDependenciesAssert(arrayOf("lib[0-9]* -X> [a-z]*-android")).assert(dependencyGraph) 27 | } 28 | 29 | @Test 30 | fun passesWithNoMatchersToAlias() { 31 | val dependencyGraph = DependencyGraph.create( 32 | "app" to "feature", 33 | "app" to "feature2", 34 | "feature2" to "core", 35 | "feature" to "core" 36 | ) 37 | 38 | val aliases = mapOf( 39 | "app" to "App", 40 | "feature2" to "Impl", 41 | "feature" to "Impl", 42 | "core" to "Api" 43 | ) 44 | 45 | RestrictedDependenciesAssert( 46 | arrayOf("Api -X> Impl", "Impl -X> Impl"), 47 | aliases 48 | ).assert(dependencyGraph) 49 | } 50 | 51 | @Test(expected = VerificationException::class) 52 | fun failsWithMatchersToAlias() { 53 | val dependencyGraph = DependencyGraph.create( 54 | "app" to "feature", 55 | "app" to "feature2", 56 | "feature2" to "core", 57 | "feature" to "core", 58 | "feature" to "feature2", 59 | ) 60 | 61 | val aliases = mapOf( 62 | "app" to "App", 63 | "feature2" to "Impl", 64 | "feature" to "Impl", 65 | "core" to "Api" 66 | ) 67 | 68 | RestrictedDependenciesAssert( 69 | arrayOf("Api -X> Impl", "Impl -X> Impl"), 70 | aliases 71 | ).assert(dependencyGraph) 72 | } 73 | 74 | private fun testGraph(): DependencyGraph { 75 | return DependencyGraph.create( 76 | "app" to "feature", 77 | "app" to "feature2", 78 | "app" to "lib", 79 | "feature" to "lib", 80 | "feature" to "lib2", 81 | "feature" to "feature2", 82 | "lib" to "core", 83 | "lib" to "core-android", 84 | "lib2" to "core-android", 85 | "core-android" to "core" 86 | ) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/ModuleGraphAssertionsPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.assertion.tasks.AssertGraphTask 4 | import org.gradle.api.internal.project.DefaultProject 5 | import org.gradle.api.plugins.JavaLibraryPlugin 6 | import org.gradle.language.base.plugins.LifecycleBasePlugin.CHECK_TASK_NAME 7 | import org.gradle.testfixtures.ProjectBuilder 8 | import org.junit.Before 9 | import org.junit.Test 10 | 11 | class ModuleGraphAssertionsPluginTest { 12 | private lateinit var project: DefaultProject 13 | 14 | @Before 15 | fun setUp() { 16 | project = ProjectBuilder.builder().withName("app").build() as DefaultProject 17 | project.plugins.apply(JavaLibraryPlugin::class.java) 18 | } 19 | 20 | @Test 21 | fun testAddsOnlyOneTaskWhenApplied() { 22 | val checkDependsOnSize = project.tasks.findByName(CHECK_TASK_NAME)!!.dependsOn.size 23 | 24 | val plugin = ModuleGraphAssertionsPlugin() 25 | plugin.addModulesAssertions(project, GraphRulesExtension()) 26 | 27 | assert(project.tasks.findByName(Api.Tasks.ASSERT_ALL) != null) 28 | assert(project.tasks.findByName(Api.Tasks.ASSERT_ALL)!!.dependsOn.isEmpty()) 29 | assert(project.tasks.findByName(CHECK_TASK_NAME)!!.dependsOn.size == checkDependsOnSize + 1) 30 | } 31 | 32 | @Test 33 | fun testAddsOnlyOneTaskWhenApplied2() { 34 | val plugin = ModuleGraphAssertionsPlugin() 35 | 36 | val extension = GraphRulesExtension().apply { 37 | maxHeight = 3 38 | allowed = arrayOf(":feature-\\S* -> :lib\\S*", ".* -> :core") 39 | restricted = arrayOf(":feature-one -X> :feature-two") 40 | } 41 | 42 | plugin.addModulesAssertions(project, extension) 43 | 44 | assert(project.tasks.findByName(Api.Tasks.ASSERT_ALL) != null) 45 | assert(project.tasks.findByName(Api.Tasks.ASSERT_ALL)!!.dependsOn.size == 3) 46 | 47 | setOf( 48 | project.tasks.findByName(Api.Tasks.ASSERT_MAX_HEIGHT) as AssertGraphTask, 49 | project.tasks.findByName(Api.Tasks.ASSERT_ALLOWED) as AssertGraphTask, 50 | project.tasks.findByName(Api.Tasks.ASSERT_RESTRICTIONS) as AssertGraphTask 51 | ) 52 | } 53 | 54 | @Test 55 | fun testAddsOnlyOneTaskWhenApplied3() { 56 | val plugin = ModuleGraphAssertionsPlugin() 57 | 58 | val extension = GraphRulesExtension().apply { 59 | maxHeight = 3 60 | allowed = arrayOf(":feature-one -> :lib") 61 | } 62 | 63 | plugin.addModulesAssertions(project, extension) 64 | 65 | assert(project.tasks.findByName(Api.Tasks.ASSERT_ALL) != null) 66 | assert(project.tasks.findByName(Api.Tasks.ASSERT_ALL)!!.dependsOn.size == 2) 67 | 68 | setOf( 69 | project.tasks.findByName(Api.Tasks.ASSERT_MAX_HEIGHT) as AssertGraphTask, 70 | project.tasks.findByName(Api.Tasks.ASSERT_ALLOWED) as AssertGraphTask, 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/FullProjectRootGradleTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import org.junit.Rule 4 | import org.junit.Test 5 | import org.junit.rules.TemporaryFolder 6 | import java.io.File 7 | 8 | class FullProjectRootGradleTest { 9 | @get:Rule 10 | val testProjectDir: TemporaryFolder = TemporaryFolder() 11 | 12 | @Test 13 | fun printsCorrectStatisticsForRootProjectWithDependency() { 14 | testProjectDir.newFile("settings.gradle") 15 | .writeText("include ':core'") 16 | 17 | createRoot(content = """ 18 | plugins { 19 | id 'com.jraska.module.graph.assertion' 20 | } 21 | apply plugin: 'java-library' 22 | 23 | moduleGraphAssert { 24 | maxHeight = 1 25 | } 26 | dependencies { 27 | implementation project(":core") 28 | } 29 | """.trimIndent()) 30 | 31 | createModule( 32 | "core", content = """ 33 | apply plugin: 'java-library' 34 | """ 35 | ) 36 | 37 | val output = 38 | runGradleAssertModuleGraph(testProjectDir.root, "generateModulesGraphStatistics").output 39 | 40 | assert(output.contains(("> Task :generateModulesGraphStatistics\n.*" + 41 | "GraphStatistics\\(modulesCount=2, edgesCount=1, height=1, longestPath=\'root.* -> :core\'\\)").toRegex())) 42 | } 43 | 44 | @Test 45 | fun printsCorrectStatisticsForIndependentRootProject() { 46 | testProjectDir.newFile("settings.gradle") 47 | .writeText("include ':app'") 48 | 49 | createRoot(content = """ 50 | plugins { 51 | id 'com.jraska.module.graph.assertion' 52 | } 53 | apply plugin: 'java-library' 54 | 55 | moduleGraphAssert { 56 | maxHeight = 0 57 | } 58 | """.trimIndent()) 59 | 60 | createModule( 61 | "app", content = """ 62 | plugins { 63 | id 'com.jraska.module.graph.assertion' 64 | } 65 | apply plugin: 'java-library' 66 | moduleGraphAssert { 67 | maxHeight = 0 68 | } 69 | """ 70 | ) 71 | 72 | val output = 73 | runGradleAssertModuleGraph(testProjectDir.root, "generateModulesGraphStatistics").output 74 | 75 | assert(output.contains(("> Task :generateModulesGraphStatistics\n.*" + 76 | "GraphStatistics\\(modulesCount=1, edgesCount=0, height=0, longestPath=\'root.*\'\\)\n\n" + 77 | "> Task :app:generateModulesGraphStatistics\n+" + 78 | "GraphStatistics\\(modulesCount=1, edgesCount=0, height=0, longestPath=\':app\'\\)").toRegex())) 79 | } 80 | 81 | private fun createRoot(content: String) { 82 | File(testProjectDir.root, "build.gradle").writeText(content) 83 | } 84 | 85 | private fun createModule(dir: String, content: String) { 86 | val newFolder = testProjectDir.newFolder(dir) 87 | File(newFolder, "build.gradle").writeText(content) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/GradleDependencyGraphFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.GraphvizWriter 4 | import org.gradle.api.internal.project.DefaultProject 5 | import org.gradle.api.plugins.JavaLibraryPlugin 6 | import org.gradle.testfixtures.ProjectBuilder 7 | import org.junit.Before 8 | import org.junit.Test 9 | 10 | class GradleDependencyGraphFactoryTest { 11 | 12 | private val EXPECTED_SINGLE_MODULE = """digraph G { 13 | ":core" 14 | }""" 15 | 16 | private val EXPECTED_GRAPHVIZ = """digraph G { 17 | ":app" -> ":lib" 18 | ":app" -> ":feature" [color=red style=bold] 19 | ":lib" -> ":core" [color=red style=bold] 20 | ":feature" -> ":core" 21 | ":feature" -> ":lib" [color=red style=bold] 22 | }""" 23 | 24 | private val EXPECTED_TEST_IMPLEMENTATION = """digraph G { 25 | ":app" -> ":feature" [color=red style=bold] 26 | ":feature" -> ":lib" [color=red style=bold] 27 | ":feature" -> ":core-testing" 28 | ":lib" -> ":core" [color=red style=bold] 29 | ":core-testing" -> ":core" 30 | }""" 31 | 32 | private lateinit var appProject: DefaultProject 33 | private lateinit var coreProject: DefaultProject 34 | private var rootProject: DefaultProject? = null 35 | 36 | @Before 37 | fun setUp() { 38 | rootProject = createProject("root") 39 | appProject = createProject("app") 40 | 41 | val libProject = createProject("lib") 42 | appProject.dependencies.add("api", libProject) 43 | 44 | val featureProject = createProject("feature") 45 | appProject.dependencies.add("implementation", featureProject) 46 | featureProject.dependencies.add("implementation", libProject) 47 | 48 | coreProject = createProject("core") 49 | featureProject.dependencies.add("api", coreProject) 50 | libProject.dependencies.add("implementation", coreProject) 51 | 52 | val coreTestingProject = createProject("core-testing") 53 | coreTestingProject.dependencies.add("implementation", coreProject) 54 | featureProject.dependencies.add("testImplementation", coreTestingProject) 55 | } 56 | 57 | @Test 58 | fun generatesProperGraph() { 59 | val dependencyGraph = GradleDependencyGraphFactory.create(appProject, Api.API_IMPLEMENTATION_CONFIGURATIONS) 60 | 61 | val graphvizText = GraphvizWriter.toGraphviz(dependencyGraph) 62 | assert(EXPECTED_GRAPHVIZ == graphvizText) 63 | } 64 | 65 | @Test 66 | fun generatesWithTestImplementatinoGraph() { 67 | val dependencyGraph = GradleDependencyGraphFactory.create(appProject, setOf("implementation", "testImplementation")) 68 | 69 | val graphvizText = GraphvizWriter.toGraphviz(dependencyGraph) 70 | assert(EXPECTED_TEST_IMPLEMENTATION == graphvizText) 71 | } 72 | 73 | @Test 74 | fun generatesSingleModuleGraphOnNoDependencyModule() { 75 | val dependencyGraph = GradleDependencyGraphFactory.create(coreProject, Api.API_IMPLEMENTATION_CONFIGURATIONS) 76 | 77 | val graphvizText = GraphvizWriter.toGraphviz(dependencyGraph) 78 | assert(EXPECTED_SINGLE_MODULE == graphvizText) 79 | } 80 | 81 | private fun createProject(name: String): DefaultProject { 82 | val project = ProjectBuilder.builder() 83 | .withName(name) 84 | .withParent(rootProject) 85 | .build() as DefaultProject 86 | 87 | project.plugins.apply(JavaLibraryPlugin::class.java) 88 | return project 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/OnAnyBuildAssertTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import org.gradle.testkit.runner.GradleRunner 4 | import org.junit.Before 5 | import org.junit.Rule 6 | import org.junit.Test 7 | import org.junit.rules.TemporaryFolder 8 | import java.io.File 9 | 10 | class OnAnyBuildAssertTest { 11 | @get:Rule 12 | val testProjectDir: TemporaryFolder = TemporaryFolder() 13 | 14 | @Before 15 | fun setup() { 16 | testProjectDir.newFile("settings.gradle") 17 | .writeText("include ':app', ':core', ':feature', 'core-api'") 18 | 19 | createModule( 20 | "core-api", content = """ 21 | apply plugin: 'java-library' 22 | 23 | ext.moduleNameAssertAlias = "Api" 24 | """ 25 | ) 26 | 27 | createModule( 28 | "core", content = """ 29 | apply plugin: 'java-library' 30 | 31 | dependencies { 32 | implementation project(":core-api") 33 | } 34 | ext.moduleNameAssertAlias = "Implementation" 35 | """ 36 | ) 37 | 38 | createModule( 39 | "feature", content = """ 40 | apply plugin: 'java-library' 41 | 42 | dependencies { 43 | implementation project(":core-api") 44 | } 45 | 46 | ext.moduleNameAssertAlias = "Implementation" 47 | """ 48 | ) 49 | } 50 | 51 | private fun createAppModule(moduleGraphAssertConfiguration: String) { 52 | createModule( 53 | "app", content = """ 54 | plugins { 55 | id 'com.jraska.module.graph.assertion' 56 | } 57 | apply plugin: 'java-library' 58 | 59 | $moduleGraphAssertConfiguration 60 | 61 | dependencies { 62 | implementation project(":core-api") 63 | implementation project(":core") 64 | implementation project(":feature") 65 | } 66 | 67 | ext.moduleNameAssertAlias = "App" 68 | """ 69 | ) 70 | } 71 | 72 | @Test 73 | fun failsOnEvaluationCheckMaxHeight() { 74 | createAppModule( 75 | """ 76 | moduleGraphAssert { 77 | maxHeight = 1 78 | assertOnAnyBuild = true 79 | } 80 | """ 81 | ) 82 | 83 | val output = setupGradle(testProjectDir.root, "help").buildAndFail().output 84 | 85 | assert(output.contains("Module :app is allowed to have maximum height of 1, but has 2, problematic dependencies: :app -> :core -> :core-api")) 86 | } 87 | 88 | @Test 89 | fun whenNotAssertOnEachEvaluation_succeedsOnEvaluationCheckMaxHeight() { 90 | createAppModule( 91 | """ 92 | moduleGraphAssert { 93 | maxHeight = 1 94 | assertOnAnyBuild = false 95 | } 96 | """ 97 | ) 98 | 99 | val output = setupGradle(testProjectDir.root, "help").build().output 100 | assert(output.contains("BUILD SUCCESS")) 101 | 102 | setupGradle(testProjectDir.root, "assertModuleGraph").buildAndFail() 103 | } 104 | 105 | @Test 106 | fun failsOnEvaluationCheckAllowed() { 107 | createAppModule( 108 | """ 109 | moduleGraphAssert { 110 | allowed = ['Implementation -> Api', 'App -> Api'] 111 | assertOnAnyBuild = true 112 | } 113 | """ 114 | ) 115 | 116 | val output = setupGradle(testProjectDir.root, "help").buildAndFail().output 117 | 118 | assert(output.contains("""["App"(':app') -> "Implementation"(':core'), "App"(':app') -> "Implementation"(':feature')] not allowed by any of ['Implementation -> Api', 'App -> Api']""")) 119 | } 120 | 121 | @Test 122 | fun whenNotAssertOnEachEvaluationDefault_succeedsOnEvaluationCheckAllowed() { 123 | createAppModule( 124 | """ 125 | moduleGraphAssert { 126 | allowed = ['Implementation -> Api', 'App -> Api'] 127 | } 128 | """ 129 | ) 130 | 131 | val output = setupGradle(testProjectDir.root, "help").build().output 132 | assert(output.contains("BUILD SUCCESS")) 133 | 134 | setupGradle(testProjectDir.root, "assertModuleGraph").buildAndFail() 135 | } 136 | 137 | @Test 138 | fun failsOnEvaluationCheckRestricted() { 139 | createAppModule( 140 | """ 141 | moduleGraphAssert { 142 | restricted = ['App -X> Api', 'Implementation -X> Implementation'] 143 | assertOnAnyBuild = true 144 | } 145 | """ 146 | ) 147 | 148 | val output = setupGradle(testProjectDir.root, "help").buildAndFail().output 149 | 150 | assert(output.contains("""Dependency '"App"(':app') -> "Api"(':core-api') violates: 'App -X> Api'""")) 151 | } 152 | 153 | private fun createModule(dir: String, content: String) { 154 | val newFolder = testProjectDir.newFolder(dir) 155 | File(newFolder, "build.gradle").writeText(content) 156 | } 157 | 158 | private fun setupGradle( 159 | dir: File, 160 | vararg arguments: String 161 | ): GradleRunner { 162 | return GradleRunner.create() 163 | .withProjectDir(dir) 164 | .withPluginClasspath() 165 | .withArguments(arguments.asList()) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/assertion/FullProjectGradleTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import org.junit.Before 4 | import org.junit.Rule 5 | import org.junit.Test 6 | import org.junit.rules.TemporaryFolder 7 | import java.io.File 8 | 9 | class FullProjectGradleTest { 10 | @get:Rule 11 | val testProjectDir: TemporaryFolder = TemporaryFolder() 12 | 13 | @Before 14 | fun setup() { 15 | testProjectDir.newFile("settings.gradle").writeText("include ':app', ':core', ':feature', 'core-api'") 16 | 17 | createModule( 18 | "core-api", content = """ 19 | apply plugin: 'java-library' 20 | 21 | ext.moduleNameAssertAlias = "Api" 22 | """ 23 | ) 24 | 25 | createModule( 26 | "core", content = """ 27 | apply plugin: 'java-library' 28 | 29 | dependencies { 30 | implementation project(":core-api") 31 | } 32 | ext.moduleNameAssertAlias = "Implementation" 33 | """ 34 | ) 35 | 36 | createModule( 37 | "feature", content = """ 38 | apply plugin: 'java-library' 39 | 40 | dependencies { 41 | implementation project(":core-api") 42 | } 43 | 44 | ext.moduleNameAssertAlias = "Implementation" 45 | """ 46 | ) 47 | 48 | createModule( 49 | "app", content = """ 50 | plugins { 51 | id 'com.jraska.module.graph.assertion' 52 | } 53 | apply plugin: 'java-library' 54 | 55 | moduleGraphAssert { 56 | maxHeight = 2 57 | allowed = ['Implementation -> Api', 'App -> .*'] 58 | restricted = ['Api -X> Api', 'Implementation -X> Implementation'] 59 | } 60 | 61 | dependencies { 62 | implementation project(":core-api") 63 | implementation project(":core") 64 | implementation project(":feature") 65 | } 66 | 67 | ext.moduleNameAssertAlias = "App" 68 | """ 69 | ) 70 | } 71 | 72 | @Test 73 | fun supportsConfigurationCache() { 74 | runGradleAssertModuleGraph(testProjectDir.root) 75 | val secondRunResult = runGradleAssertModuleGraph(testProjectDir.root) 76 | 77 | assert(secondRunResult.output.contains("Reusing configuration cache.")) 78 | } 79 | 80 | @Test 81 | fun statisticsSupportConfigurationCache() { 82 | runGradleAssertModuleGraph(testProjectDir.root, "--configuration-cache", "generateModulesGraphStatistics") 83 | val secondRunResult = runGradleAssertModuleGraph(testProjectDir.root, "--configuration-cache", "generateModulesGraphStatistics") 84 | 85 | assert(secondRunResult.output.contains("Reusing configuration cache.")) 86 | } 87 | 88 | @Test 89 | fun moduleGraphSupportConfigurationCache() { 90 | runGradleAssertModuleGraph(testProjectDir.root, "--configuration-cache", "generateModulesGraphvizText") 91 | val secondRunResult = runGradleAssertModuleGraph(testProjectDir.root, "--configuration-cache", "generateModulesGraphvizText") 92 | 93 | assert(secondRunResult.output.contains("Reusing configuration cache.")) 94 | } 95 | 96 | @Test 97 | fun printsCorrectStatistics() { 98 | val output = runGradleAssertModuleGraph(testProjectDir.root, "generateModulesGraphStatistics").output 99 | 100 | assert(output.contains("GraphStatistics(modulesCount=4, edgesCount=5, height=2, longestPath=':app -> :core -> :core-api')")) 101 | } 102 | 103 | @Test 104 | fun printsOnlyModule() { 105 | val output = runGradleAssertModuleGraph(testProjectDir.root, "generateModulesGraphvizText", "-Pmodules.graph.of.module=:feature").output 106 | 107 | assert(output.contains("digraph G {\n" + 108 | "\":feature('Implementation')\" -> \":core-api('Api')\" [color=red style=bold]\n" + 109 | "}")) 110 | } 111 | 112 | @Test 113 | fun printsOnlyModuleStatistics() { 114 | val output = runGradleAssertModuleGraph(testProjectDir.root, "generateModulesGraphStatistics", "-Pmodules.graph.of.module=:feature").output 115 | 116 | assert(output.contains("GraphStatistics(modulesCount=2, edgesCount=1, height=1, longestPath=':feature -> :core-api')")) 117 | } 118 | 119 | @Test 120 | fun savesGraphIntoFile() { 121 | val outputFile = File(testProjectDir.root, "all_modules.dot") 122 | val output = runGradleAssertModuleGraph(testProjectDir.root, "generateModulesGraphvizText", "-Pmodules.graph.output.gv=${outputFile.absolutePath}").output 123 | 124 | assert(output.contains("GraphViz saved to")) 125 | assert(outputFile.readText() == EXPECTED_GRAPHVIZ_TEXT) 126 | } 127 | 128 | private fun createModule(dir: String, content: String) { 129 | val newFolder = testProjectDir.newFolder(dir) 130 | File(newFolder, "build.gradle").writeText(content) 131 | } 132 | 133 | companion object { 134 | const val EXPECTED_GRAPHVIZ_TEXT = """digraph G { 135 | ":app('App')" -> ":core-api('Api')" 136 | ":app('App')" -> ":core('Implementation')" [color=red style=bold] 137 | ":app('App')" -> ":feature('Implementation')" 138 | ":core('Implementation')" -> ":core-api('Api')" [color=red style=bold] 139 | ":feature('Implementation')" -> ":core-api('Api')" 140 | }""" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/DependencyGraph.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | import java.io.Serializable 4 | 5 | class DependencyGraph private constructor() { 6 | private val nodes = mutableMapOf() 7 | 8 | fun findRoot(): Node { 9 | require(nodes.isNotEmpty()) { "Dependency Tree is empty" } 10 | 11 | val rootCandidates = nodes().toMutableSet() 12 | 13 | nodes().flatMap { it.dependsOn } 14 | .forEach { rootCandidates.remove(it) } 15 | 16 | return rootCandidates.associateBy { heightOf(it.key) } 17 | .maxByOrNull { it.key }!!.value 18 | } 19 | 20 | fun nodes(): Collection = nodes.values 21 | 22 | fun dependencyPairs(): List> { 23 | return nodes() 24 | .flatMap { parent -> 25 | parent.dependsOn.map { dependency -> parent to dependency } 26 | } 27 | .map { it.first.key to it.second.key } 28 | } 29 | 30 | fun longestPath(): LongestPath { 31 | return longestPath(findRoot().key) 32 | } 33 | 34 | fun longestPath(key: String): LongestPath { 35 | val nodeNames = nodes.getValue(key) 36 | .longestPath() 37 | .map { it.key } 38 | 39 | return LongestPath(nodeNames) 40 | } 41 | 42 | fun height(): Int { 43 | return heightOf(findRoot().key) 44 | } 45 | 46 | fun heightOf(key: String): Int { 47 | return nodes.getValue(key).height() 48 | } 49 | 50 | fun statistics(): GraphStatistics { 51 | val height = height() 52 | val edgesCount = countEdges() 53 | return GraphStatistics( 54 | modulesCount = nodes.size, 55 | edgesCount = edgesCount, 56 | height = height, 57 | longestPath = longestPath() 58 | ) 59 | } 60 | 61 | fun subTree(key: String): DependencyGraph { 62 | require(nodes.contains(key)) { "Dependency Tree doesn't contain module: $key" } 63 | 64 | val connections = mutableListOf>() 65 | addConnections(nodes.getValue(key), connections, mutableSetOf(), mutableSetOf()) 66 | 67 | return if (connections.isEmpty()) { 68 | createSingular(key) 69 | } else { 70 | create(connections) 71 | } 72 | } 73 | 74 | fun serializableGraph(): SerializableGraph { 75 | return SerializableGraph( 76 | ArrayList(dependencyPairs()), 77 | nodes.keys.first() 78 | ) 79 | } 80 | 81 | private fun addConnections( 82 | node: Node, 83 | into: MutableList>, 84 | path: MutableSet, 85 | visited: MutableSet, 86 | ) { 87 | if (visited.contains(node)) { 88 | return 89 | } else { 90 | visited.add(node) 91 | } 92 | 93 | path.add(node) 94 | node.dependsOn.forEach { dependant -> 95 | into.add(node.key to dependant.key) 96 | 97 | val nodeInCurrentPath = path.contains(dependant) 98 | if (nodeInCurrentPath) { 99 | val pathText = path.joinToString(separator = ", ") { it.key } 100 | throw IllegalStateException("Dependency cycle detected! Cycle in nodes: '${pathText}'.") 101 | } 102 | addConnections(dependant, into, path, visited) 103 | } 104 | 105 | path.remove(node) 106 | } 107 | 108 | private fun countEdges(): Int { 109 | return nodes().flatMap { node -> node.dependsOn }.count() 110 | } 111 | 112 | class SerializableGraph( 113 | val dependencyPairs: ArrayList>, 114 | val firstModule: String 115 | ) : Serializable 116 | 117 | class Node(val key: String) { 118 | val dependsOn = mutableSetOf() 119 | 120 | private val calculatedHeight by lazy { 121 | if (isLeaf()) { 122 | 0 123 | } else { 124 | 1 + dependsOn.maxOf { it.height() } 125 | } 126 | } 127 | 128 | private fun isLeaf() = dependsOn.isEmpty() 129 | 130 | fun height(): Int { 131 | return calculatedHeight 132 | } 133 | 134 | internal fun longestPath(): List { 135 | if (isLeaf()) { 136 | return listOf(this) 137 | } else { 138 | val path = mutableListOf(this) 139 | 140 | val maxHeightNode = dependsOn.maxByOrNull { it.height() }!! 141 | path.addAll(maxHeightNode.longestPath()) 142 | 143 | return path 144 | } 145 | } 146 | } 147 | 148 | companion object { 149 | fun createSingular(singleModule: String): DependencyGraph { 150 | val dependencyGraph = DependencyGraph() 151 | 152 | dependencyGraph.getOrCreate(singleModule) 153 | 154 | return dependencyGraph 155 | } 156 | 157 | fun create(dependencies: List>): DependencyGraph { 158 | if (dependencies.isEmpty()) { 159 | throw IllegalArgumentException("Graph cannot be empty. Use createSingular for cases with no dependencies") 160 | } 161 | 162 | val graph = DependencyGraph() 163 | dependencies.forEach { graph.addEdge(it.first, it.second) } 164 | return graph 165 | } 166 | 167 | fun create(vararg dependencies: Pair): DependencyGraph { 168 | return create(dependencies.asList()) 169 | } 170 | 171 | fun create(graph: SerializableGraph): DependencyGraph { 172 | if (graph.dependencyPairs.isEmpty()) { 173 | return createSingular(graph.firstModule) 174 | } else { 175 | return create(graph.dependencyPairs) 176 | } 177 | } 178 | 179 | private fun DependencyGraph.addEdge(from: String, to: String) { 180 | getOrCreate(from).dependsOn.add(getOrCreate(to)) 181 | } 182 | 183 | private fun DependencyGraph.getOrCreate(key: String): Node { 184 | return nodes[key] ?: Node(key).also { nodes[key] = it } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/jraska/module/graph/assertion/ModuleGraphAssertionsPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph.assertion 2 | 3 | import com.jraska.module.graph.DependencyGraph 4 | import com.jraska.module.graph.assertion.Api.Tasks 5 | import com.jraska.module.graph.assertion.tasks.AssertGraphTask 6 | import com.jraska.module.graph.assertion.tasks.GenerateModulesGraphStatisticsTask 7 | import com.jraska.module.graph.assertion.tasks.GenerateModulesGraphTask 8 | import org.gradle.api.Plugin 9 | import org.gradle.api.Project 10 | import org.gradle.api.UnknownTaskException 11 | import org.gradle.api.tasks.TaskProvider 12 | import org.gradle.language.base.plugins.LifecycleBasePlugin.CHECK_TASK_NAME 13 | import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP 14 | 15 | @Suppress("unused") // Used as plugin 16 | class ModuleGraphAssertionsPlugin : Plugin { 17 | 18 | private val moduleGraph by lazy { 19 | GradleDependencyGraphFactory.create(evaluatedProject, configurationsToLook).serializableGraph() 20 | } 21 | 22 | private val aliases by lazy { 23 | GradleModuleAliasExtractor.extractModuleAliases(evaluatedProject) 24 | } 25 | 26 | private lateinit var evaluatedProject: Project 27 | private lateinit var configurationsToLook: Set 28 | 29 | override fun apply(project: Project) { 30 | val graphRules = project.extensions.create(GraphRulesExtension::class.java, Api.EXTENSION_ROOT, GraphRulesExtension::class.java) 31 | 32 | project.afterEvaluate { 33 | addModulesAssertions(project, graphRules) 34 | 35 | if (graphRules.assertOnAnyBuild) { 36 | project.gradle.projectsEvaluated { 37 | project.runAssertionsDirectly(graphRules) 38 | } 39 | } 40 | } 41 | } 42 | 43 | internal fun addModulesAssertions(project: Project, graphRules: GraphRulesExtension) { 44 | evaluatedProject = project 45 | configurationsToLook = graphRules.configurations 46 | 47 | project.addModuleGraphGeneration() 48 | project.addModuleGraphStatisticsGeneration() 49 | 50 | val allAssertionsTask = project.tasks.register(Tasks.ASSERT_ALL) { it.group = VERIFICATION_GROUP } 51 | 52 | try { 53 | project.tasks.named(CHECK_TASK_NAME).configure { it.dependsOn(allAssertionsTask) } 54 | } catch (checkNotFound: UnknownTaskException) { 55 | // We register other tasks, but we don't add a dependency to 'check' task 56 | } 57 | 58 | val childTasks = mutableListOf>() 59 | project.addMaxHeightTask(graphRules)?.also { childTasks.add(it) } 60 | project.addModuleRestrictionsTask(graphRules)?.also { childTasks.add(it) } 61 | project.addModuleAllowedRulesTask(graphRules)?.also { childTasks.add(it) } 62 | 63 | allAssertionsTask.configure { allTask -> 64 | childTasks.forEach { 65 | allTask.dependsOn(it) 66 | } 67 | } 68 | } 69 | 70 | private fun Project.runAssertionsDirectly(graphRules: GraphRulesExtension) { 71 | val dependencyGraph = DependencyGraph.create(moduleGraph) 72 | 73 | if (graphRules.shouldAssertHeight()) { 74 | moduleTreeHeightAssert(graphRules).assert(dependencyGraph) 75 | } 76 | 77 | if (graphRules.shouldAssertRestricted()) { 78 | restrictedDependenciesAssert(graphRules).assert(dependencyGraph) 79 | } 80 | 81 | if (graphRules.shouldAssertAllowed()) { 82 | onlyAllowedAssert(graphRules).assert(dependencyGraph) 83 | } 84 | } 85 | 86 | private fun Project.addModuleGraphGeneration() { 87 | tasks.register(Tasks.GENERATE_GRAPHVIZ, GenerateModulesGraphTask::class.java) { 88 | it.dependencyGraph = moduleGraph 89 | it.aliases = aliases 90 | it.outputFilePath = GenerateModulesGraphTask.outputFilePath(this) 91 | it.onlyModuleToPrint = GenerateModulesGraphTask.onlyModule(this) 92 | } 93 | } 94 | 95 | private fun Project.addModuleGraphStatisticsGeneration() { 96 | tasks.register( 97 | Tasks.GENERATE_GRAPH_STATISTICS, 98 | GenerateModulesGraphStatisticsTask::class.java 99 | ) { 100 | it.dependencyGraph = moduleGraph 101 | it.onlyModuleStatistics = GenerateModulesGraphTask.onlyModule(this) 102 | } 103 | } 104 | 105 | private fun Project.addMaxHeightTask(graphRules: GraphRulesExtension): TaskProvider? { 106 | if (graphRules.shouldAssertHeight()) { 107 | return tasks.register(Tasks.ASSERT_MAX_HEIGHT, AssertGraphTask::class.java) { 108 | it.assertion = moduleTreeHeightAssert(graphRules) 109 | it.dependencyGraph = moduleGraph 110 | it.outputs.upToDateWhen { true } 111 | it.group = VERIFICATION_GROUP 112 | } 113 | } else { 114 | return null 115 | } 116 | } 117 | 118 | private fun Project.moduleTreeHeightAssert(graphRules: GraphRulesExtension) = 119 | ModuleTreeHeightAssert(moduleNameForHeightAssert(), graphRules.maxHeight) 120 | 121 | private fun Project.addModuleRestrictionsTask(graphRules: GraphRulesExtension): TaskProvider? { 122 | if (graphRules.shouldAssertRestricted()) { 123 | return tasks.register(Tasks.ASSERT_RESTRICTIONS, AssertGraphTask::class.java) { 124 | it.assertion = restrictedDependenciesAssert(graphRules) 125 | it.dependencyGraph = moduleGraph 126 | it.outputs.upToDateWhen { true } 127 | it.group = VERIFICATION_GROUP 128 | } 129 | } else { 130 | return null 131 | } 132 | } 133 | 134 | private fun restrictedDependenciesAssert(graphRules: GraphRulesExtension) = 135 | RestrictedDependenciesAssert(graphRules.restricted, aliases) 136 | 137 | private fun Project.addModuleAllowedRulesTask(graphRules: GraphRulesExtension): TaskProvider? { 138 | if (graphRules.shouldAssertAllowed()) { 139 | return tasks.register(Tasks.ASSERT_ALLOWED, AssertGraphTask::class.java) { 140 | it.assertion = onlyAllowedAssert(graphRules) 141 | it.dependencyGraph = moduleGraph 142 | it.outputs.upToDateWhen { true } 143 | it.group = VERIFICATION_GROUP 144 | } 145 | } else { 146 | return null 147 | } 148 | } 149 | 150 | private fun onlyAllowedAssert(graphRules: GraphRulesExtension) = 151 | OnlyAllowedAssert(graphRules.allowed, aliases) 152 | } 153 | 154 | private fun Project.moduleNameForHeightAssert(): String? { 155 | if (this == rootProject) { 156 | return null 157 | } else { 158 | return moduleDisplayName() 159 | } 160 | } 161 | 162 | fun Project.moduleDisplayName(): String { 163 | return displayName.replace("project", "") 164 | .replace("'", "") 165 | .trim() 166 | } 167 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/jraska/module/graph/DependencyGraphTest.kt: -------------------------------------------------------------------------------- 1 | package com.jraska.module.graph 2 | 3 | import org.junit.Test 4 | 5 | class DependencyGraphTest { 6 | @Test 7 | fun correctHeightIsMaintained() { 8 | val dependencyTree = DependencyGraph.create( 9 | "app" to "feature", 10 | "app" to "lib", 11 | "feature" to "lib", 12 | "lib" to "core" 13 | ) 14 | 15 | assert(dependencyTree.heightOf("app") == 3) 16 | } 17 | 18 | @Test 19 | fun findsProperLongestPath() { 20 | val dependencyTree = DependencyGraph.create( 21 | "app" to "feature", 22 | "app" to "lib", 23 | "app" to "core", 24 | "feature" to "lib", 25 | "lib" to "core" 26 | ) 27 | 28 | assert(dependencyTree.longestPath("app").nodeNames == listOf("app", "feature", "lib", "core")) 29 | } 30 | 31 | @Test 32 | fun findsProperRoot() { 33 | val dependencyTree = DependencyGraph.create( 34 | "feature" to "lib", 35 | "lib" to "core", 36 | "app" to "feature", 37 | "app" to "lib", 38 | "app" to "core" 39 | ) 40 | 41 | assert(dependencyTree.findRoot().key == "app") 42 | } 43 | 44 | @Test 45 | fun createsSubtreeProperly() { 46 | val dependencyTree = DependencyGraph.create( 47 | "feature" to "lib", 48 | "lib" to "core", 49 | "app" to "feature", 50 | "feature" to "core", 51 | "app" to "core" 52 | ) 53 | 54 | val subTree = dependencyTree.subTree("feature") 55 | 56 | assert(subTree.findRoot().key == "feature") 57 | assert(subTree.heightOf("feature") == 2) 58 | assert(subTree.longestPath("feature").nodeNames == listOf("feature", "lib", "core")) 59 | } 60 | 61 | @Test 62 | fun subtreeOfLeafModuleIsNotEmpty() { 63 | val dependencyTree = DependencyGraph.create( 64 | "feature" to "lib", 65 | "lib" to "core", 66 | "app" to "feature", 67 | "feature" to "core", 68 | "app" to "core" 69 | ) 70 | 71 | assert(dependencyTree.subTree("core").findRoot().key == "core") 72 | } 73 | 74 | @Test 75 | fun singleModuleTreeWorks() { 76 | val dependencyGraph = DependencyGraph.createSingular(":app") 77 | 78 | assert(dependencyGraph.dependencyPairs().isEmpty()) 79 | assert(dependencyGraph.height() == 0) 80 | assert(dependencyGraph.statistics().modulesCount == 1) 81 | assert(dependencyGraph.statistics().edgesCount == 0) 82 | assert(dependencyGraph.statistics().height == 0) 83 | assert(dependencyGraph.statistics().height == 0) 84 | assert(dependencyGraph.findRoot().key == ":app") 85 | assert(dependencyGraph.heightOf(":app") == 0) 86 | assert(dependencyGraph.longestPath().nodeNames == listOf(":app")) 87 | assert(dependencyGraph.longestPath().nodeNames == dependencyGraph.statistics().longestPath.nodeNames) 88 | } 89 | 90 | @Test(expected = IllegalArgumentException::class) 91 | fun cannotCreateEmptyGraph() { 92 | DependencyGraph.create() 93 | } 94 | 95 | @Test 96 | fun countsStatisticsWell() { 97 | val dependencyTree = DependencyGraph.create( 98 | ":app" to ":core", 99 | ":app" to ":core-android", 100 | ":app" to ":navigation", 101 | ":app" to ":lib:navigation-deeplink", 102 | ":app" to ":lib:identity", 103 | ":app" to ":lib:dynamic-features", 104 | ":app" to ":lib:network-status", 105 | ":app" to ":feature:push", 106 | ":app" to ":feature:users", 107 | ":app" to ":feature:settings_entrance", 108 | ":app" to ":feature:about_entrance", 109 | ":app" to ":feature:shortcuts", 110 | ":core-android" to ":core", 111 | ":lib:navigation-deeplink" to ":navigation", 112 | ":lib:navigation-deeplink" to ":core", 113 | ":lib:identity" to ":core", 114 | ":lib:dynamic-features" to ":core", 115 | ":lib:dynamic-features" to ":core-android", 116 | ":lib:network-status" to ":core", 117 | ":lib:network-status" to ":core-android", 118 | ":feature:push" to ":core", 119 | ":feature:push" to ":core-android", 120 | ":feature:push" to ":lib:identity", 121 | ":feature:users" to ":core", 122 | ":feature:users" to ":core-android", 123 | ":feature:users" to ":navigation", 124 | ":feature:settings_entrance" to ":core", 125 | ":feature:settings_entrance" to ":core-android", 126 | ":feature:settings_entrance" to ":lib:dynamic-features", 127 | ":feature:about_entrance" to ":core", 128 | ":feature:about_entrance" to ":core-android", 129 | ":feature:about_entrance" to ":lib:dynamic-features", 130 | ":feature:shortcuts" to ":core", 131 | ":feature:shortcuts" to ":core-android", 132 | ":core-testing" to ":core", 133 | ":feature:about" to ":app", 134 | ":feature:about" to ":core", 135 | ":feature:about" to ":core-android", 136 | ":feature:about" to ":navigation", 137 | ":feature:about" to ":lib:identity", 138 | ":feature:about" to ":lib:dynamic-features", 139 | ":feature:settings" to ":core", 140 | ":feature:settings" to ":core-android", 141 | ":feature:settings" to ":lib:dynamic-features", 142 | ":feature:settings" to ":app" 143 | ) 144 | 145 | val statistics = dependencyTree.statistics() 146 | 147 | assert(statistics.height == 5) 148 | assert(statistics.modulesCount == 16) 149 | assert(statistics.edgesCount == 45) 150 | assert( 151 | statistics.longestPath.nodeNames == listOf( 152 | ":feature:settings", 153 | ":app", 154 | ":feature:settings_entrance", 155 | ":lib:dynamic-features", 156 | ":core-android", 157 | ":core" 158 | ) 159 | ) 160 | } 161 | 162 | @Test(expected = IllegalStateException::class) 163 | fun cyclesDoNotOverflow() { 164 | val dependencyTree = DependencyGraph.create( 165 | "feature" to "lib", 166 | "lib" to "core", 167 | "lib" to "feature", 168 | "feature" to "core", 169 | "app" to "feature" 170 | ) 171 | 172 | dependencyTree.subTree("app") 173 | } 174 | 175 | @Test 176 | fun doesNotDetectCycleOnMultiEntry() { 177 | val dependencyTree = DependencyGraph.create( 178 | "feature" to "lib", 179 | "app" to "lib", 180 | "app" to "feature" 181 | ) 182 | 183 | assert(dependencyTree.subTree("app").findRoot().key == "app") 184 | } 185 | 186 | @Test(expected = IllegalStateException::class) 187 | fun detectsTwoNodesCycle() { 188 | val dependencyTree = DependencyGraph.create( 189 | "feature" to "app", 190 | "app" to "feature" 191 | ) 192 | 193 | dependencyTree.subTree("app") 194 | } 195 | 196 | @Test(expected = IllegalStateException::class) 197 | fun detectsThreeNodesCycle() { 198 | val dependencyTree = DependencyGraph.create( 199 | "feature" to "app", 200 | "app" to "lib", 201 | "lib" to "feature" 202 | ) 203 | 204 | dependencyTree.subTree("app") 205 | } 206 | 207 | @Test(expected = IllegalStateException::class) 208 | fun detectsSingleNodeCycle() { 209 | val dependencyTree = DependencyGraph.create( 210 | "app" to "app", 211 | ) 212 | 213 | dependencyTree.subTree("app") 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Module Graph Assert 2 | A Gradle plugin that helps keep your module graph healthy and lean. 3 | 4 | - [Medium Article](https://proandroiddev.com/module-rules-protect-your-build-time-and-architecture-d1194c7cc6bc) with complete context. 5 | - [Talk about module graph and why it matters](https://www.droidcon.com/2022/11/15/modularization-flatten-your-graph-and-get-the-real-benefits/) 6 | - [Changelog](https://github.com/jraska/modules-graph-assert/releases) 7 | 8 | [![Build](https://github.com/jraska/modules-graph-assert/actions/workflows/build.yml/badge.svg)](https://github.com/jraska/modules-graph-assert/actions/workflows/build.yml) 9 | [![Gradle Plugin](https://img.shields.io/badge/Gradle-Plugin-green)](https://plugins.gradle.org/plugin/com.jraska.module.graph.assertion) 10 | 11 | example_graph 12 | 13 | ## Why the module dependency structure matters 14 | - Build speeds can be very dependent on the structure of your module graph. 15 | - Modules separate logical units and enforce proper dependencies. 16 | - The module graph can silently degenerate into a list-like structure. 17 | - Breaking problematic module dependencies can be very difficult, it is cheaper to prevent them. 18 | - If not enforced, undesirable module dependencies will appear. Murphy's law of dependencies: "Whatever they can access, they will access." 19 | 20 | ## What we can enforce 21 | - The plugin provides a simple way for defining rules, which will be verified with the task `assertModuleGraph` as part of the `check` task. 22 | - Match module names using regular expressions. 23 | - Define the only allowed dependencies between modules 24 | - `allowed = [':feature-one -> :feature-[a-z-:]*', ':.* -> :core', ':feature.* -> :lib.*']` define rules by using `regex -> regex` signature. 25 | - Dependency, which will not match any of those rules will fail the assertion. 26 | - `restricted [':feature-[a-z]* -X> :forbidden-to-depend-on']` helps us to define custom rules by using `regex -X> regex` signature. 27 | - `maxHeight = 4` can verify that the [height of the modules tree](https://stackoverflow.com/questions/2603692/what-is-the-difference-between-tree-depth-and-height) with a root in the module will not exceed 4. Tree height is a good metric to prevent module tree degeneration into a list. 28 | 29 | ## Usage 30 | Apply the plugin to a module, which dependencies graph you want to assert. 31 | ```groovy 32 | plugins { 33 | id "com.jraska.module.graph.assertion" version "2.9.0" 34 | } 35 | ``` 36 | 37 | - You can run `./gradlew assertModuleGraph` to execute configured checks or `./gradlew check` where `assertModuleGraph` will be included. 38 | - Alternative option is using `assertOnAnyBuild = true` configuration to run the checks on every single Gradle build without need for running explicit tasks - see https://github.com/jraska/modules-graph-assert/pull/184 for more details. 39 | - Hint: Gradle [Configuration On Demand](https://docs.gradle.org/current/userguide/multi_project_configuration_and_execution.html) may hide some modules from the plugin visibility. If you notice some modules are missing, try the `--no-configure-on-demand` flag. 40 | 41 | ### Configuration 42 | Rules are applied on the Gradle module and its `api` and `implementation` dependencies by default. Typically you would want to apply this in your final app module, however configuration for any module is possible. [Example](https://github.com/jraska/github-client/blob/master/app/build.gradle#L141) 43 | 44 | ```groovy 45 | moduleGraphAssert { 46 | maxHeight = 4 47 | allowed = [':.* -> :core', ':feature.* -> :lib.*'] // regex to match module names 48 | restricted = [':feature-[a-z]* -X> :forbidden-to-depend-on'] // regex to match module names 49 | configurations = ['api', 'implementation'] // Dependency configurations to look. ['api', 'implementation'] is the default 50 | assertOnAnyBuild = false // true value will run the assertions as part of any build without need to run the assert* tasks, false is default 51 | } 52 | ``` 53 | 54 | #### Kotlin Multiplatform (KMP) 55 | ```groovy 56 | configurations += setOf("commonMainImplementation", "commonMainApi") // different sourceSets defaults 57 | ``` 58 | - Please see [this issue](https://github.com/jraska/modules-graph-assert/issues/249#issuecomment-2437075587) for details and comment if you face issues. 59 | 60 | ### Module name alias 61 | - You don't have to rely on module names and set a property `ext.moduleNameAssertAlias = "ThisWillBeAssertedOn"` 62 | - This can be set on any module and the `allowed`/`restricted` rules would use the alias instead of module name for asserting. 63 | - This is useful for example if you want to use "module types" where each module has a type regardless the name and you want to manage only dependnecies of different types. 64 | - It is recommended to use either module names or `moduleNameAssertAlias` everywhere. Mixing both is not recommended. 65 | - Example of module rules you could implement for a flat module graph: 66 | 67 | ` 68 | - Each module would have set `ext.moduleNameAssertAlias = "Api|Implementation|App"` 69 | - Module rules example for such case: `allowed = ['Implementation -> Api', 'App -> Implementation', 'App -> Api']` 70 | - In case you want to migrate to this structure incrementally, you can set a separate module type like `ext.moduleNameAssertAlias = "NeedsMigration"` and setting 71 | ``` 72 | allowed = [ 73 | 'Implementation -> Api', 74 | 'App -> Implementation', 75 | 'App -> Api', 76 | 'NeedsMigration -> .*', 77 | '.* -> NeedsMigration' 78 | ] 79 | ``` 80 | 81 | - `"NeedsMigration"` modules can be then tackled one by one to move them into `Implementation` or `Api` type. Example of app with this structure [can be seen here](https://github.com/jraska/github-client). 82 | 83 | ### Graphviz Graph Export 84 | - Visualising the graph could be useful to help find your dependency issues, therefore a helper `generateModulesGraphvizText` task is included. 85 | - This generates a graph of dependent modules when the plugin is applied. 86 | - The longest path of the project is in red. 87 | - If you utilise [Configuration on demand](https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:configuration_on_demand) Gradle feature, please use `--no-configure-on-demand` flag along the `generateModulesGraphvizText` task. 88 | - You can set the `modules.graph.of.module` parameter if you are only interested in a sub-graph of the module graph. 89 | ``` 90 | ./gradlew generateModulesGraphvizText -Pmodules.graph.of.module=:feature-one 91 | ``` 92 | - Adding the parameter `modules.graph.output.gv` saves the graphViz file to the specified path 93 | ``` 94 | ./gradlew generateModulesGraphvizText -Pmodules.graph.output.gv=all_modules 95 | ``` 96 | 97 | ### Graph statistics 98 | - Executing the task `generateModulesGraphStatistics` prints the information about the graph. 99 | - Statistics printed: Modules Count, [Edges Count](https://en.wikipedia.org/wiki/Glossary_of_graph_theory_terms#edge), [Height](https://en.wikipedia.org/wiki/Glossary_of_graph_theory_terms#height) and [Longest Path](https://en.wikipedia.org/wiki/Longest_path_problem) 100 | - Parameter `-Pmodules.graph.of.module` is supported as well. 101 | ``` 102 | ./gradlew generateModulesGraphStatistics -Pmodules.graph.of.module=:feature-one 103 | ``` 104 | 105 | ## Contributing 106 | 107 | Please feel free to create PR or issue with any suggestions or ideas. No special format required, just common sense. 108 | 109 | ### Debugging 110 | 111 | **Setting up a composite build** 112 | 113 | [Composite builds](https://docs.gradle.org/current/userguide/composite_builds.html#settings_defined_composite) are consumed directly without publishing a version. 114 | 115 | settings.gradle: 116 | ```groovy 117 | includeBuild("path/to/modules-graph-assert") 118 | ``` 119 | 120 | Root build.gradle: 121 | ```groovy 122 | plugins { 123 | id('com.jraska.module.graph.assertion') 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 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 | --------------------------------------------------------------------------------