├── .bazelrc ├── .bazelversion ├── .gitignore ├── BUILD.bazel ├── CONTRIBUTING.md ├── DESIGN.md ├── LICENSE ├── README.md ├── WORKSPACE ├── bin ├── .bazel-5.1.1.pkg ├── README.hermit.md ├── activate-hermit ├── bazel ├── hermit ├── hermit.hcl └── release.sh ├── build.gradle ├── core ├── BUILD.bazel ├── README.md ├── build.gradle ├── module.spice.yml └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── squareup │ │ └── spice │ │ └── serialization │ │ ├── FileWorkspace.kt │ │ ├── PathIndex.kt │ │ ├── Serializer.kt │ │ └── dependency_serialization.kt │ └── test │ ├── kotlin │ └── com │ │ └── squareup │ │ └── spice │ │ └── serialization │ │ ├── BUILD.bazel │ │ ├── FileWorkspaceTest.kt │ │ ├── IndexingTest.kt │ │ ├── ScaleTest.kt │ │ └── SerializerTest.kt │ └── resources │ ├── BUILD.bazel │ └── file_workspaces │ ├── basic │ ├── a │ │ └── module.spice.yml │ ├── b │ │ └── module.spice.yml │ ├── c │ │ └── module.spice.yml │ ├── group1 │ │ ├── e │ │ │ └── module.spice.yml │ │ └── f │ │ │ └── module.spice.yml │ └── workspace.spice.yml │ ├── child_modules │ ├── a │ │ └── module.spice.yml │ ├── b │ │ └── module.spice.yml │ ├── c │ │ ├── d │ │ │ └── module.spice.yml │ │ └── module.spice.yml │ └── workspace.spice.yml │ ├── cyclic │ ├── a │ │ └── module.spice.yml │ ├── b │ │ └── module.spice.yml │ ├── c │ │ └── module.spice.yml │ ├── d │ │ └── module.spice.yml │ ├── e │ │ └── module.spice.yml │ ├── f │ │ └── module.spice.yml │ ├── g │ │ └── module.spice.yml │ ├── h │ │ └── module.spice.yml │ ├── i │ │ └── module.spice.yml │ └── workspace.spice.yml │ ├── dangling │ ├── a │ │ └── module.spice.yml │ ├── b │ │ └── module.spice.yml │ ├── c │ │ └── module.spice.yml │ └── workspace.spice.yml │ ├── nested_workspaces │ ├── a │ │ └── module.spice.yml │ ├── b │ │ └── module.spice.yml │ ├── nested │ │ ├── c │ │ │ └── module.spice.yml │ │ ├── d │ │ │ └── module.spice.yml │ │ └── workspace.spice.yml │ └── workspace.spice.yml │ ├── no_workspace_file │ ├── a │ │ └── module.spice.yml │ └── b │ │ └── module.spice.yml │ ├── with_symlinks │ └── with_symlinks.tgz │ └── with_tests │ ├── a │ └── module.spice.yml │ ├── b │ └── module.spice.yml │ ├── c │ └── module.spice.yml │ └── workspace.spice.yml ├── examples ├── README.md ├── complete │ └── workspace.spice.yml └── minimal │ └── workspace.spice.yml ├── gradle.properties ├── gradlew ├── libs.versions.toml ├── model ├── BUILD.bazel ├── README.md ├── build.gradle ├── module.spice.yml └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── squareup │ │ └── spice │ │ ├── builders │ │ ├── DeclarationScope.kt │ │ ├── ExternalWorkspaceDocumentBuilder.kt │ │ ├── ModuleDocumentBuilder.kt │ │ ├── SpiceBuilderScope.kt │ │ ├── ToolDefinitionSet.kt │ │ └── WorkspaceDocumentBuilder.kt │ │ ├── collections │ │ ├── DeterministicMap.kt │ │ └── DeterministicSet.kt │ │ ├── model │ │ ├── Edge.kt │ │ ├── ExternalWorkspaceDocument.kt │ │ ├── GradleSerializable.kt │ │ ├── Model.puml │ │ ├── ModuleDocument.kt │ │ ├── Node.kt │ │ ├── ToolDefinition.kt │ │ ├── WholeGraph.kt │ │ ├── Workspace.kt │ │ ├── WorkspaceDocument.kt │ │ ├── errors.kt │ │ ├── traversal │ │ │ ├── BreadthFirstDependencyVisitor.kt │ │ │ ├── BreadthFirstNodeVisitor.kt │ │ │ ├── BreadthFirstReverseDependencyVisitor.kt │ │ │ ├── DepsCollector.kt │ │ │ └── NodeVisitor.kt │ │ ├── util │ │ │ └── collections.kt │ │ └── validation │ │ │ ├── DependencyCycleValidator.kt │ │ │ ├── GraphCompletenessValidator.kt │ │ │ ├── TestLeafValidator.kt │ │ │ └── Validator.kt │ │ └── test │ │ └── FakeWorkspace.kt │ └── test │ └── kotlin │ └── com │ └── squareup │ └── spice │ ├── collections │ └── DeterministicTest.kt │ └── model │ ├── BUILD.bazel │ ├── FakeWorkspaceTest.kt │ ├── MergeTest.kt │ ├── TestNodeUnitTest.kt │ ├── traversal │ ├── BUILD.bazel │ └── BreadthFirstDependencyVisitorTest.kt │ └── validation │ ├── BUILD.bazel │ ├── DependencyCycleValidatorTest.kt │ ├── GraphCompletenessValidatorTest.kt │ └── TestLeafValidatorTest.kt ├── query ├── BUILD.bazel ├── README.md ├── build.gradle └── module.spice.yml ├── settings.gradle ├── templating ├── BUILD.bazel ├── README.md └── module.spice.yml ├── tools ├── BUILD.bazel └── junit5.bzl └── workspace.spice.yml /.bazelrc: -------------------------------------------------------------------------------- 1 | # Constrain environment within actions, in order to avoid environment pollution 2 | # which can restrict cache hits. 3 | build --incompatible_strict_action_env=true 4 | 5 | 6 | # Kochiku specific settings 7 | build:kochiku --notrack_incremental_state 8 | build:kochiku --discard_analysis_cache 9 | build:kochiku --nokeep_state_after_build 10 | build:kochiku --experimental_repository_cache_hardlinks 11 | build:kochiku --local_ram_resources=HOST_RAM*.4 12 | build:kochiku --disk_cache=/mnt/nfs/shared-cache/RF-1462-bazel-builds-research-spike/ 13 | build:kochiku --sandbox_debug 14 | build:kochiku --output_filter=UNMATCHABLE_REGEXP 15 | build:kochiku --fat_apk_cpu=x86_64 16 | build:kochiku --verbose_failures 17 | build:kochiku --build_event_json_file=bazel.ci.bep.json 18 | build:kochiku --announce_rc 19 | build:kochiku --keep_going 20 | build:kochiku --jobs=HOST_CPUS*.6 21 | test:kochiku --test_timeout=180,450,1350,4800 22 | test:kochiku --test_output=errors 23 | 24 | # This will permissively try to load a file "user.bazelrc" from the project workspace 25 | # if it exists, in case of per-user+per-project flags. Needing this should be rare, 26 | # but is a good place to put things you want active for you, but not globally for all 27 | # bazel projects. 28 | try-import %workspace%/user.bazelrc 29 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 4.1.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gradlew.* 2 | gradle/ 3 | .idea/ 4 | .aswb/ 5 | .ijwb/ 6 | bazel-* 7 | build/ 8 | # Compiled class file 9 | *.class 10 | 11 | # Log file 12 | *.log 13 | 14 | # BlueJ files 15 | *.ctxt 16 | 17 | # Mobile Tools for Java (J2ME) 18 | .mtj.tmp/ 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/BUILD.bazel -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Keeping the project small and stable limits our ability to accept new contributors. We are not 4 | seeking new committers at this time, but some small contributions are welcome. 5 | 6 | If you've found a bug, please contribute a failing test case so we can study and fix it. 7 | 8 | If you have a new feature idea, the library should be extensible enough to allow you to build it 9 | as an add-on (or application of spice) in another project - if it's awesome, we might list it here. 10 | If you need API changes to do so, file an issue and let's talk about it. 11 | 12 | Before code can be accepted all contributors must complete our 13 | [Individual Contributor License Agreement (CLA)][cla]. 14 | 15 | # Code Contributions 16 | 17 | Get working code on a personal branch with tests passing before you submit a PR: 18 | 19 | 1. Install bazelisk (which will act as a version-aware shell for bazel) 20 | - Package managers: 21 | - MacOS: `brew install bazelisk` 22 | - Windows: `choco install bazelisk` 23 | - Or install directly from [bazelisk release artifacts] 24 | 25 | 2. build and test: `bazel test //...` 26 | 27 | Please make every effort to follow existing conventions and style in order to keep the code as 28 | readable as possible. 29 | 30 | Contribute code changes through GitHub by forking the repository and sending a pull request. We 31 | squash pull requests on merge. 32 | 33 | [cla]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 34 | [bazelisk release artifacts]: https://github.com/bazelbuild/bazelisk/releases -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spice - a metadata system for software projects 2 | 3 | 4 | 5 | *“He who controls the spice, controls the universe”* - Frank Herbert 6 | 7 | # Overview 8 | 9 | Spice seeks to provide consistent build metadata in a way that can be somewhat neutral to tools (gradle, maven, bazel), 10 | but can provide high-performance, thin representation of their build graphs for a variety of use-cases, including 11 | (but not limited to): 12 | - build system migration, 13 | - multi-build-system scenarios, 14 | - consistency across a large codebase, 15 | - declarative build metadata 16 | - fast graph-aware tooling divorced from specific build systems 17 | 18 | More detail can be found in the [Design Doc](DESIGN.md) 19 | 20 | # Using 21 | 22 | TBD 23 | 24 | # Building Spice 25 | 26 | TBD - also see [CONTRIBUTING](CONTRIBUTING.md) 27 | 28 | # Examples 29 | 30 | TBD 31 | 32 | # License 33 | ```text 34 | Copyright 2020 Square, Inc. 35 | 36 | Licensed under the Apache License, Version 2.0 (the "License"); 37 | you may not use this file except in compliance with the License. 38 | You may obtain a copy of the License at 39 | 40 | http://www.apache.org/licenses/LICENSE-2.0 41 | 42 | Unless required by applicable law or agreed to in writing, software 43 | distributed under the License is distributed on an "AS IS" BASIS, 44 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 45 | See the License for the specific language governing permissions and 46 | limitations under the License. 47 | ``` 48 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "spice") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | # Rules and tooling versions 6 | RULES_KOTLIN_VERSION = "v1.5.0-beta-4" 7 | 8 | RULES_KOTLIN_SHA = "6cbd4e5768bdfae1598662e40272729ec9ece8b7bded8f0d2c81c8ff96dc139d" 9 | 10 | MAVEN_REPOSITORY_RULES_VERSION = "2.0.0-alpha-4" 11 | 12 | MAVEN_REPOSITORY_RULES_SHA = "a6484fec8d1aebd4affff7ae1ee9b59141858b2c636222bdb619526ccd8b3358" 13 | 14 | KOTLIN_VERSION = "1.5.30" 15 | 16 | KOTLINC_ROOT = "https://github.com/JetBrains/kotlin/releases/download" 17 | 18 | KOTLINC_URL = "{root}/v{v}/kotlin-compiler-{v}.zip".format( 19 | root = KOTLINC_ROOT, 20 | v = KOTLIN_VERSION, 21 | ) 22 | 23 | KOTLINC_SHA = "ccd0db87981f1c0e3f209a1a4acb6778f14e63fe3e561a98948b5317e526cc6c" 24 | 25 | RETROFIT_VERSION = "2.7.2" 26 | 27 | # Rules and tools repositories 28 | http_archive( 29 | name = "io_bazel_rules_kotlin", 30 | sha256 = RULES_KOTLIN_SHA, 31 | urls = ["https://github.com/bazelbuild/rules_kotlin/releases/download/%s/rules_kotlin_release.tgz" % RULES_KOTLIN_VERSION], 32 | ) 33 | 34 | http_archive( 35 | name = "maven_repository_rules", 36 | sha256 = MAVEN_REPOSITORY_RULES_SHA, 37 | strip_prefix = "bazel_maven_repository-%s" % MAVEN_REPOSITORY_RULES_VERSION, 38 | type = "zip", 39 | urls = ["https://github.com/square/bazel_maven_repository/archive/%s.zip" % MAVEN_REPOSITORY_RULES_VERSION], 40 | ) 41 | 42 | # Setup Kotlin 43 | load("@io_bazel_rules_kotlin//kotlin:repositories.bzl", "kotlin_repositories") 44 | 45 | kotlin_repositories() 46 | 47 | load("@io_bazel_rules_kotlin//kotlin:core.bzl", "kt_register_toolchains") 48 | 49 | kt_register_toolchains() 50 | 51 | # Setup maven repository handling. 52 | load("@maven_repository_rules//maven:maven.bzl", "maven_repository_specification") 53 | 54 | maven_repository_specification( 55 | name = "maven", 56 | artifacts = { 57 | "com.fasterxml.jackson.core:jackson-annotations:2.11.0": {"insecure": True}, 58 | "com.fasterxml.jackson.core:jackson-core:2.11.0": {"insecure": True}, 59 | "com.fasterxml.jackson.core:jackson-databind:2.11.0": {"insecure": True}, 60 | "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0": {"insecure": True}, 61 | "com.fasterxml.jackson.module:jackson-module-kotlin:2.11.0": {"insecure": True}, 62 | "com.google.auto.value:auto-value-annotations:1.6.3": {"insecure": True}, 63 | "com.google.code.findbugs:jsr305:3.0.2": {"insecure": True}, 64 | "com.google.errorprone:error_prone_annotations:2.3.1": {"insecure": True}, 65 | "com.google.guava:failureaccess:1.0.1": {"insecure": True}, 66 | "com.google.guava:guava:27.0.1-android": { 67 | "insecure": True, 68 | "exclude": ["com.google.guava:listenablefuture"], 69 | }, 70 | "com.google.j2objc:j2objc-annotations:1.1": {"insecure": True}, 71 | "com.google.truth:truth:1.1.3": { 72 | "insecure": True, 73 | "exclude": ["junit:junit"], 74 | }, 75 | "com.googlecode.java-diff-utils:diffutils:1.3.0": {"insecure": True}, 76 | "com.squareup.okhttp3:okhttp:4.5.0": {"insecure": True}, 77 | "com.squareup.okio:okio:2.4.1": {"insecure": True}, 78 | "com.squareup.retrofit2:converter-jackson:%s" % RETROFIT_VERSION: {"insecure": True}, 79 | "com.squareup.retrofit2:converter-wire:%s" % RETROFIT_VERSION: {"insecure": True}, 80 | "com.squareup.retrofit2:retrofit:%s" % RETROFIT_VERSION: {"insecure": True}, 81 | "com.squareup.wire:wire-runtime:3.0.0": {"insecure": True}, 82 | "com.xenomachina:kotlin-argparser:2.0.7": {"insecure": True}, 83 | "com.xenomachina:xenocom:0.0.7": {"insecure": True}, 84 | "org.apiguardian:apiguardian-api:1.1.2": {"insecure": True}, 85 | "org.checkerframework:checker-compat-qual:2.5.5": {"insecure": True}, 86 | "org.codehaus.mojo:animal-sniffer-annotations:1.17": {"insecure": True}, 87 | "org.jetbrains.kotlin:kotlin-reflect:%s" % KOTLIN_VERSION: {"insecure": True}, 88 | "org.jetbrains.kotlin:kotlin-stdlib-common:%s" % KOTLIN_VERSION: {"insecure": True}, 89 | "org.jetbrains.kotlin:kotlin-stdlib:%s" % KOTLIN_VERSION: {"insecure": True}, 90 | "org.jetbrains.kotlin:kotlin-stdlib-jdk8:%s" % KOTLIN_VERSION: {"insecure": True}, 91 | "org.jetbrains.kotlin:kotlin-stdlib-jdk7:%s" % KOTLIN_VERSION: {"insecure": True}, 92 | "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2": {"insecure": True}, 93 | "org.jetbrains:annotations:13.0": {"insecure": True}, 94 | "org.junit.jupiter:junit-jupiter-api:5.8.2": {"insecure": True}, 95 | "org.junit.jupiter:junit-jupiter-engine:5.8.2": {"insecure": True}, 96 | "org.junit.platform:junit-platform-commons:1.8.2": {"insecure": True}, 97 | "org.junit.platform:junit-platform-console:1.8.2": {"insecure": True}, 98 | "org.junit.platform:junit-platform-reporting:1.8.2": {"insecure": True}, 99 | "org.junit.platform:junit-platform-launcher:1.8.2": {"insecure": True}, 100 | "org.junit.platform:junit-platform-engine:1.8.2": {"insecure": True}, 101 | "org.opentest4j:opentest4j:1.2.0": {"insecure": True}, 102 | "org.yaml:snakeyaml:1.26": {"insecure": True}, 103 | "org.checkerframework:checker-qual:3.13.0": {"insecure": True}, 104 | "org.ow2.asm:asm:9.1": {"insecure": True}, 105 | }, 106 | ) 107 | -------------------------------------------------------------------------------- /bin/.bazel-5.1.1.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/README.hermit.md: -------------------------------------------------------------------------------- 1 | # Hermit environment 2 | 3 | This is a [Hermit](https://github.com/cashapp/hermit) bin directory. 4 | 5 | The symlinks in this directory are managed by Hermit and will automatically 6 | download and install Hermit itself as well as packages. These packages are 7 | local to this environment. 8 | -------------------------------------------------------------------------------- /bin/activate-hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file must be used with "source bin/activate-hermit" from bash or zsh. 3 | # You cannot run it directly 4 | 5 | if [ "${BASH_SOURCE-}" = "$0" ]; then 6 | echo "You must source this script: \$ source $0" >&2 7 | exit 33 8 | fi 9 | 10 | BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" 11 | if "${BIN_DIR}/hermit" noop > /dev/null; then 12 | eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" 13 | 14 | if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then 15 | hash -r 2>/dev/null 16 | fi 17 | 18 | echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" 19 | fi 20 | -------------------------------------------------------------------------------- /bin/bazel: -------------------------------------------------------------------------------- 1 | .bazel-5.1.1.pkg -------------------------------------------------------------------------------- /bin/hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | if [ -z "${HERMIT_STATE_DIR}" ]; then 6 | case "$(uname -s)" in 7 | Darwin) 8 | export HERMIT_STATE_DIR="${HOME}/Library/Caches/hermit" 9 | ;; 10 | Linux) 11 | export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/hermit" 12 | ;; 13 | esac 14 | fi 15 | 16 | export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" 17 | HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" 18 | export HERMIT_CHANNEL 19 | export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} 20 | 21 | if [ ! -x "${HERMIT_EXE}" ]; then 22 | echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 23 | curl -fsSL "${HERMIT_DIST_URL}/install.sh" | /bin/bash 1>&2 24 | fi 25 | 26 | exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" 27 | -------------------------------------------------------------------------------- /bin/hermit.hcl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/bin/hermit.hcl -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | (( $# == 1 )) || (echo "usage: release.sh " ; exit 1) 5 | 6 | NAME="$1" 7 | GIT_ROOT="$(git rev-parse --show-toplevel)" 8 | ROOT="${GIT_ROOT}"/tools/spice 9 | 10 | # keep in sync with third_party/rules/release/bazel_maven_repository/BUILD.bazel 11 | ARTIFACT="${NAME}.jar" 12 | ( 13 | # build and test 14 | cd "$ROOT" 15 | echo "build and test" 16 | bazel test //... && bazel build //apps/"${NAME}" 17 | echo "Copy jar" 18 | install "bazel-bin/apps/${NAME}/${NAME}" "${ROOT}/bin/${ARTIFACT}" 19 | (cd "${ROOT}/bin" && ln -s -f "${ARTIFACT}" "${NAME}" ) 20 | ) 21 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | //buildscript { 2 | // apply from: '../../dependencies.gradle' 3 | //} 4 | plugins { 5 | id 'org.jetbrains.kotlin.jvm' version '1.5.31' apply true 6 | } 7 | 8 | rootProject.ext { 9 | group = 'com.squareup.spice' 10 | artifact = 'root' 11 | version = '0.2' 12 | } 13 | -------------------------------------------------------------------------------- /core/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 2 | 3 | kt_jvm_library( 4 | name = "core", 5 | srcs = glob(["src/main/**/*.kt"]), 6 | visibility = ["//:__subpackages__"], 7 | deps = [ 8 | "//model", 9 | "@maven//com/fasterxml/jackson/core:jackson-databind", 10 | "@maven//com/fasterxml/jackson/dataformat:jackson-dataformat-yaml", 11 | "@maven//com/fasterxml/jackson/module:jackson-module-kotlin", 12 | "@maven//org/jetbrains/kotlinx:kotlinx-coroutines-core-jvm", 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # Spice Core 2 | 3 | The most common parts of spice other than `spice-model`. This contains additional capabilities 4 | on top of the model, such as serialization, validation, etc. 5 | 6 | > TODO: should validation be in model? 7 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | // id 'build-tool-convention' 3 | } 4 | apply { plugin "kotlin" } 5 | 6 | dependencies { 7 | implementation project(':model') 8 | 9 | implementation(libs.jackson.annotations) 10 | implementation(libs.jackson.databind) 11 | implementation(libs.jackson.dataformat.yaml) 12 | implementation(libs.jackson.module.kotlin) 13 | implementation(libs.kotlinx.coroutines.core) 14 | implementation(libs.snakeyaml) 15 | 16 | testImplementation(libs.junit.api) 17 | testImplementation(libs.google.truth) 18 | testRuntimeOnly(libs.junit.engine) 19 | } 20 | -------------------------------------------------------------------------------- /core/module.spice.yml: -------------------------------------------------------------------------------- 1 | # For illustration - may not be correct (yet) until we dogfood 2 | name: core 3 | tools: [ kotlin ] 4 | variants: 5 | main: 6 | deps: 7 | - /model 8 | - maven://com.fasterxml.jackson.core:jackson-databind 9 | - maven://com.fasterxml.jackson.dataformat:jackson-dataformat-yaml 10 | - maven://com.fasterxml.jackson.module:jackson-module-kotlin 11 | - maven://org.jetbrains.kotlinx:kotlinx-coroutines-core 12 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/squareup/spice/serialization/PathIndex.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.serialization 2 | 3 | import com.squareup.spice.model.FindResult 4 | import com.squareup.spice.model.FindResult.GeneralFindResult 5 | import com.squareup.spice.model.FindResult.TestFindResult 6 | import com.squareup.spice.model.FindResult.VariantFindResult 7 | import com.squareup.spice.model.InvalidAddress 8 | import com.squareup.spice.model.Node.ModuleNode 9 | import com.squareup.spice.model.Node.TestNode 10 | import com.squareup.spice.model.util.toSetMultimap 11 | import java.util.concurrent.ConcurrentHashMap 12 | 13 | /** 14 | * A simple tree index for fast lookups of addresses for module membership. Since addresses are in 15 | * effect a hierarchical filesystem, and since modules cannot be defined "above" other modules, 16 | * this creates a nice tree structure. Variant lookups and Test node lookups are all leaves off 17 | * the module, and are left to a secondary lookup once the module is found. 18 | */ 19 | class PathIndex { 20 | private data class Branch( 21 | val children: MutableMap = ConcurrentHashMap() 22 | ) { 23 | var leaf: Leaf? = null 24 | } 25 | 26 | private data class Leaf( 27 | val module: GeneralFindResult, 28 | val variants: Map>, 29 | val tests: Map> 30 | ) 31 | 32 | private val root = Branch() 33 | 34 | fun addNode(node: ModuleNode, variants: Map>, tests: List) { 35 | var current: Branch = root 36 | for (element in node.path) { 37 | with(current.leaf) { 38 | if (this != null) 39 | throw InvalidAddress( 40 | node.address, 41 | "Path is below existing address ${this.module.node.address}" 42 | ) 43 | } 44 | current = current.children.getOrPut(element) { Branch() } 45 | } 46 | if (current.children.isNotEmpty()) 47 | throw InvalidAddress( 48 | node.address, "Path is above existing addresses ${current.children.map { it.key }}" 49 | ) 50 | when { 51 | current.leaf == null -> { 52 | current.leaf = Leaf( 53 | GeneralFindResult(node), 54 | variants.mapValues { (_, variantSet) -> 55 | variantSet.map { VariantFindResult(node, it) }.toSet() 56 | }, 57 | tests.flatMap { testNode -> testNode.config.srcs.map { it to TestFindResult(testNode) } } 58 | .toSetMultimap() 59 | ) 60 | } 61 | current.leaf!!.module.node == node -> { 62 | /* found a node at this address, but they're equal, so let it pass. */ 63 | } 64 | else -> throw IllegalArgumentException("Node already registered at address ${node.address}") 65 | } 66 | } 67 | 68 | fun nodeForPath(path: String): Sequence> { 69 | val leaf = leafForPath(path) ?: return sequenceOf() 70 | val module = leaf.module.node 71 | val variants = leaf.variants.flatMap { (src, result) -> 72 | if (path.startsWith("${module.address}/$src")) result else setOf() 73 | }.toSet() 74 | val tests = leaf.tests.flatMap { (src, result) -> 75 | if (path.startsWith("${module.address}/$src")) result else setOf() 76 | }.toSet() 77 | return (variants + tests).ifEmpty { setOf(leaf.module) }.asSequence() 78 | } 79 | 80 | /** 81 | * Returns a module for a given path, if that path is contained within a module 82 | */ 83 | private fun leafForPath(path: String): Leaf? { 84 | // TODO: Validate path and reject bad (incoherent, not just wrong) paths. 85 | var current: Branch? = root 86 | val localPath = if (path.startsWith('/')) path.substring(1) else path 87 | val elements = localPath.split("/").toMutableList() 88 | for (element in elements) { 89 | current = current?.children?.get(element) 90 | if (current == null) return null 91 | if (current.leaf != null) return current.leaf 92 | } 93 | return null 94 | } 95 | 96 | /** 97 | * Returns a module for a given path, if that path is contained within a module 98 | */ 99 | fun moduleForPath(path: String): ModuleNode? { 100 | return leafForPath(path)?.module?.node 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/squareup/spice/serialization/Serializer.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.serialization 2 | 3 | import com.fasterxml.jackson.core.ObjectCodec 4 | import com.fasterxml.jackson.core.io.IOContext 5 | import com.fasterxml.jackson.databind.module.SimpleModule 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory 7 | import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator 8 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper 9 | import com.fasterxml.jackson.module.kotlin.KotlinModule 10 | import com.squareup.spice.collections.DeterministicMap 11 | import com.squareup.spice.collections.DeterministicSet 12 | import com.squareup.spice.collections.MutableDeterministicMap 13 | import com.squareup.spice.collections.MutableDeterministicSet 14 | import com.squareup.spice.model.Dependency 15 | import org.yaml.snakeyaml.DumperOptions 16 | import org.yaml.snakeyaml.DumperOptions.FlowStyle 17 | import org.yaml.snakeyaml.DumperOptions.NonPrintableStyle.ESCAPE 18 | import org.yaml.snakeyaml.DumperOptions.ScalarStyle 19 | import java.io.IOException 20 | import java.io.Writer 21 | import java.nio.charset.StandardCharsets.UTF_8 22 | 23 | class Serializer( 24 | private val mapper: YAMLMapper = YAMLMapper(CustomYAMLFactory()).apply { 25 | registerModule( 26 | KotlinModule().apply { 27 | addAbstractTypeMapping( 28 | DeterministicMap::class.java, 29 | MutableDeterministicMap.ByHash::class.java 30 | ) 31 | addAbstractTypeMapping( 32 | MutableDeterministicMap::class.java, 33 | MutableDeterministicMap.ByHash::class.java 34 | ) 35 | addAbstractTypeMapping( 36 | DeterministicSet::class.java, 37 | MutableDeterministicSet.ByHash::class.java 38 | ) 39 | addAbstractTypeMapping( 40 | MutableDeterministicSet::class.java, 41 | MutableDeterministicSet.ByHash::class.java 42 | ) 43 | } 44 | ) 45 | // Want to use this, but can't, as it results in map entries with null values not being written. 46 | // configOverride(List::class.java).setterInfo = JsonSetter.Value.forContentNulls(Nulls.AS_EMPTY) 47 | registerModule( 48 | SimpleModule().apply { 49 | addDeserializer(Dependency::class.java, DependencyDeserializer()) 50 | addSerializer(Dependency::class.java, DependencySerializer()) 51 | } 52 | ) 53 | } 54 | ) { 55 | fun marshall(dataObject: T): String { 56 | return mapper.writeValueAsString(dataObject) 57 | } 58 | 59 | fun unmarshall(contents: String, type: Class): T { 60 | return mapper.readValue(contents, type) 61 | } 62 | 63 | inline fun unmarshall(bytes: ByteArray): T { 64 | return unmarshall(bytes.toString(UTF_8), T::class.java) 65 | } 66 | 67 | class CustomYAMLGenerator( 68 | ctx: IOContext, 69 | jsonFeatures: Int, 70 | yamlFeatures: Int, 71 | codec: ObjectCodec, 72 | out: Writer, 73 | version: DumperOptions.Version? 74 | ) : YAMLGenerator(ctx, jsonFeatures, yamlFeatures, codec, out, version) { 75 | override fun buildDumperOptions( 76 | jsonFeatures: Int, 77 | yamlFeatures: Int, 78 | version: DumperOptions.Version? 79 | ): DumperOptions { 80 | return super.buildDumperOptions(jsonFeatures, yamlFeatures, version).apply { 81 | defaultScalarStyle = ScalarStyle.LITERAL 82 | defaultFlowStyle = FlowStyle.BLOCK 83 | indicatorIndent = 2 84 | nonPrintableStyle = ESCAPE 85 | indent = 4 86 | isPrettyFlow = true 87 | width = 100 88 | this.version = version 89 | } 90 | } 91 | } 92 | 93 | class CustomYAMLFactory : YAMLFactory() { 94 | @Throws(IOException::class) 95 | override fun _createGenerator(out: Writer, ctxt: IOContext): YAMLGenerator { 96 | val feats = _yamlGeneratorFeatures 97 | return CustomYAMLGenerator(ctxt, _generatorFeatures, feats, _objectCodec, out, _version) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/squareup/spice/serialization/dependency_serialization.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.serialization 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator 4 | import com.fasterxml.jackson.core.JsonParser 5 | import com.fasterxml.jackson.databind.DeserializationContext 6 | import com.fasterxml.jackson.databind.JsonNode 7 | import com.fasterxml.jackson.databind.SerializerProvider 8 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer 9 | import com.fasterxml.jackson.databind.ser.std.StdSerializer 10 | import com.squareup.spice.model.Dependency 11 | 12 | /** 13 | * Custom deserializer/serializer for Dependency, allowing users to specify deps in the yaml with 14 | * metadata, but not have to put map/field metadata markers if there isn't any. e.g. both of these 15 | * are supported, and write out the same way. 16 | * 17 | * ``` 18 | * --- 19 | * variants: 20 | * debug: 21 | * deps: 22 | * - /foo/bar 23 | * - /bar/baz: [exported_all] 24 | * - maven://blah.foo:bar 25 | * - maven://blah.baz:baz: [exported_children] 26 | * ``` 27 | * 28 | * See [Dependency.tags] for details on the semantics. The parsing system simply treats them as 29 | * strings. 30 | */ 31 | class DependencyDeserializer : StdDeserializer(Dependency::class.java) { 32 | override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Dependency { 33 | with(p.codec) { 34 | val node: JsonNode = readTree(p) 35 | return if (node.isTextual) { 36 | // Handle simple strings without ":" 37 | Dependency(node.asText()) 38 | } else { 39 | // Handle "- /foo/bar: [blah]" treating the target as the field key. 40 | val field = node.fieldNames().next() 41 | val tags = node[field].asIterable().map { it.asText() }.toList() 42 | Dependency(target = field, tags = tags) 43 | } 44 | } 45 | } 46 | } 47 | 48 | class DependencySerializer : StdSerializer(Dependency::class.java) { 49 | override fun serialize(dep: Dependency, gen: JsonGenerator, provider: SerializerProvider) { 50 | gen.writeStartObject() 51 | gen.writeArrayFieldStart(dep.target) 52 | dep.tags.forEach { gen.writeObject(it) } 53 | gen.writeEndArray() 54 | gen.writeEndObject() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/squareup/spice/serialization/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 2 | load("//tools:junit5.bzl", "kt_jvm_test") 3 | 4 | kt_jvm_test( 5 | name = "FileWorkspaceTest", 6 | srcs = ["FileWorkspaceTest.kt"], 7 | data = ["//core/src/test/resources:file_workspaces"], 8 | friends = ["//core"], 9 | test_class = "com.squareup.spice.serialization.FileWorkspaceTest", 10 | deps = [ 11 | "//model", 12 | "@maven//com/google/truth", 13 | "@maven//org/junit/jupiter:junit-jupiter-api", 14 | ], 15 | ) 16 | 17 | kt_jvm_test( 18 | name = "IndexingTest", 19 | srcs = ["IndexingTest.kt"], 20 | friends = ["//core"], 21 | test_class = "com.squareup.spice.serialization.IndexingTest", 22 | deps = [ 23 | "//model", 24 | "@maven//com/google/truth", 25 | "@maven//org/junit/jupiter:junit-jupiter-api", 26 | ], 27 | ) 28 | 29 | kt_jvm_test( 30 | name = "ScaleTest", 31 | size = "medium", 32 | srcs = ["ScaleTest.kt"], 33 | data = ["//core/src/test/resources:file_workspaces"], 34 | friends = ["//core"], 35 | test_class = "com.squareup.spice.serialization.ScaleTest", 36 | deps = [ 37 | "//model", 38 | "@maven//com/google/truth", 39 | "@maven//org/junit/jupiter:junit-jupiter-api", 40 | ], 41 | ) 42 | 43 | kt_jvm_test( 44 | name = "SerializerTest", 45 | srcs = ["SerializerTest.kt"], 46 | friends = ["//core"], 47 | test_class = "com.squareup.spice.serialization.SerializerTest", 48 | deps = [ 49 | "//model", 50 | "@maven//com/google/truth", 51 | "@maven//org/junit/jupiter:junit-jupiter-api", 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/squareup/spice/serialization/FileWorkspaceTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.serialization 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.google.common.truth.Truth.assertWithMessage 5 | import com.squareup.spice.model.CyclicReferenceError 6 | import com.squareup.spice.model.FindResult.GeneralFindResult 7 | import com.squareup.spice.model.FindResult.TestFindResult 8 | import com.squareup.spice.model.FindResult.VariantFindResult 9 | import com.squareup.spice.model.InvalidAddress 10 | import com.squareup.spice.model.NoSuchAddress 11 | import com.squareup.spice.model.traversal.DepsCollector 12 | import com.squareup.spice.model.validation.DependencyCycleValidator 13 | import com.squareup.spice.model.validation.STANDARD_VALIDATORS 14 | import kotlinx.coroutines.ExperimentalCoroutinesApi 15 | import kotlinx.coroutines.FlowPreview 16 | import org.junit.jupiter.api.Assertions.assertThrows 17 | import org.junit.jupiter.api.Test 18 | import java.io.File 19 | import java.io.FileNotFoundException 20 | 21 | @FlowPreview 22 | @ExperimentalCoroutinesApi 23 | class FileWorkspaceTest { 24 | 25 | private val runfiles by lazy { 26 | File("${System.getenv("PWD")}/core/src/test/resources/file_workspaces").takeIf { 27 | it.exists() 28 | } 29 | } 30 | 31 | private val resources by lazy { 32 | javaClass.classLoader?.getResource("file_workspaces")?.toURI()?.let { 33 | File(it) 34 | }?.takeIf { 35 | it.exists() 36 | } 37 | } 38 | 39 | private val workspaces: File by lazy { 40 | runfiles ?: resources ?: error("Unable to find workspace") 41 | } 42 | 43 | @Test fun noWorkspaceDirectory() { 44 | assertThrows(FileNotFoundException::class.java) { workspace("no_such_workspace_dir").loadAll() } 45 | } 46 | 47 | @Test fun noWorkspaceFile() { 48 | assertThrows(FileNotFoundException::class.java) { workspace("no_workspace_file").loadAll() } 49 | } 50 | 51 | @Test fun basicNodeLoading() { 52 | val ws = workspace("basic") 53 | // Preconditions 54 | assertThat(ws.document.definitions) 55 | assertThat(ws.variants).hasSize(2) 56 | 57 | val a = ws.nodeAt("/a") 58 | assertThat(a.address).isEqualTo("/a") 59 | val c = ws.nodeAt("/c") 60 | assertThat(c.address).isEqualTo("/c") 61 | val f = ws.nodeAt("/group1/f") 62 | assertThat(f.address).isEqualTo("/group1/f") 63 | 64 | assertThrows(NoSuchAddress::class.java) { 65 | ws.nodeAt("/q") 66 | } 67 | } 68 | 69 | @Test fun forwardDeps() { 70 | val ws = workspace("basic") 71 | // Preconditions 72 | assertThat(ws.document.definitions) 73 | assertThat(ws.variants).hasSize(2) 74 | 75 | with(ws.slice("debug")) { 76 | val cDeps = dependenciesOf("/c") 77 | assertThat(cDeps).hasSize(1) 78 | val b = cDeps.first() 79 | assertThat(b.target).isEqualTo("/b") 80 | val bDeps = dependenciesOf(b.target) 81 | assertThat(bDeps).hasSize(1) 82 | val a = bDeps.first() 83 | assertThat(a.target).isEqualTo("/a") 84 | assertThat(dependenciesOf(a.target)).isEmpty() 85 | } 86 | 87 | with(ws.slice("release")) { 88 | val cDeps = dependenciesOf("/c") 89 | assertThat(cDeps).hasSize(1) 90 | val a = cDeps.first() 91 | assertThat(a.target).isEqualTo("/a") 92 | assertThat(dependenciesOf(a.target)).isEmpty() 93 | } 94 | } 95 | 96 | @Test fun reverseDeps() { 97 | val ws = workspace("basic").apply { loadAll() } 98 | // Preconditions 99 | assertThat(ws.document.definitions) 100 | assertThat(ws.variants).hasSize(2) 101 | 102 | with(ws.slice("debug")) { 103 | val aRDeps = dependenciesOn("/a") 104 | assertThat(aRDeps).hasSize(2) 105 | assertThat(aRDeps.map { it.target }.toSet()).isEqualTo(setOf("/b", "/group1/f")) 106 | 107 | val cRDeps = dependenciesOn("/c") 108 | assertThat(cRDeps).hasSize(1) 109 | assertThat(cRDeps.first().target).isEqualTo("/group1/e") 110 | 111 | val fRDeps = dependenciesOn("/group1/f") 112 | assertThat(fRDeps).isEmpty() 113 | } 114 | 115 | with(ws.slice("release")) { 116 | val aRDeps = dependenciesOn("/a") 117 | assertThat(aRDeps).hasSize(2) 118 | assertThat(aRDeps.map { it.target }.toSet()).isEqualTo(setOf("/b", "/c")) 119 | 120 | val cRDeps = dependenciesOn("/c") 121 | assertThat(cRDeps).hasSize(1) 122 | assertThat(cRDeps.first().target).isEqualTo("/group1/f") 123 | 124 | val fRDeps = dependenciesOn("/group1/f") 125 | assertThat(fRDeps).isEmpty() 126 | } 127 | } 128 | 129 | @Test fun loadAll() { 130 | val ws = workspace("basic") 131 | assertThat(ws.nodeIndex).isEmpty() 132 | ws.loadAll() 133 | ws.validate(STANDARD_VALIDATORS) 134 | assertThat(ws.nodeIndex.size).isEqualTo(10) 135 | } 136 | 137 | @Test fun loadAllWithCycles() { 138 | val ws = workspace("cyclic") 139 | assertThat(ws.nodeIndex).isEmpty() 140 | assertThrows(CyclicReferenceError::class.java) { 141 | ws.loadAll() 142 | ws.validate(STANDARD_VALIDATORS) 143 | } 144 | } 145 | 146 | /** 147 | * bazel copies data directories and resolves (not copies) symlinks, so extract this one from 148 | * a tarball, to preserve it. 149 | */ 150 | fun extractWorkspace(tag: String) { 151 | val dir = File("$workspaces/$tag") 152 | val tarball = dir.resolve("$tag.tgz") 153 | require(tarball.exists()) { 154 | "No such tarball $tarball" 155 | } 156 | val builder = ProcessBuilder().apply { 157 | command("sh", "-c", "tar xvfz $tarball -C $dir") 158 | directory(File("/tmp")) 159 | } 160 | val exitCode = builder.start().waitFor() 161 | assertThat(exitCode).isEqualTo(0) 162 | } 163 | 164 | @Test fun loadAllWithSymlinksEnabled() { 165 | extractWorkspace("with_symlinks") 166 | val ws = workspace("with_symlinks", followSymlinks = true) 167 | assertThat(ws.nodeIndex).isEmpty() 168 | ws.loadAll() 169 | assertThat(ws.nodeIndex.size).isEqualTo(6) 170 | } 171 | 172 | @Test fun loadAllWithSymlinksDisabled() { 173 | extractWorkspace("with_symlinks") 174 | val ws = workspace("with_symlinks") 175 | assertThat(ws.nodeIndex).isEmpty() 176 | ws.loadAll() 177 | assertThat(ws.nodeIndex.size).isEqualTo(2) 178 | } 179 | 180 | @Test fun loadAllWithTests() { 181 | val ws = workspace("with_tests") 182 | assertThat(ws.nodeIndex).isEmpty() 183 | ws.loadAll() 184 | assertThat(ws.nodeIndex.size).isEqualTo(6) 185 | ws.validate(STANDARD_VALIDATORS) 186 | val slice = ws.slice("debug") 187 | with(DepsCollector()) { 188 | this.visit(slice, slice.nodeAt("/b:debug:unit")) 189 | assertThat(this.deps).containsExactly("/a", "/b", "/b:debug:unit") 190 | } 191 | with(DepsCollector()) { 192 | this.visit(slice, slice.nodeAt("/a:debug:unit")) 193 | assertThat(this.deps).containsExactly("/a", "/b", "/a:debug:unit") 194 | } 195 | } 196 | 197 | @Test fun loadAllWithDanglingReferences() { 198 | val ws = workspace("dangling") 199 | assertThat(ws.nodeIndex).isEmpty() 200 | // Because the file workspace does a full load which implicitly tries to visit all nodes, its 201 | // graph walk trips over the missing reference during loadAll(). 202 | val e = assertThrows(NoSuchAddress::class.java) { 203 | ws.loadAll() 204 | ws.validate(STANDARD_VALIDATORS) 205 | } 206 | assertThat(e).hasMessageThat().contains("No such address found in graph: /q") 207 | } 208 | 209 | // Permit self-cycles for now. 210 | @Test fun cycleSelf() { 211 | workspace("cyclic").slice("main").validate(listOf(DependencyCycleValidator), "/c") 212 | } 213 | 214 | @Test fun cyclePair() { 215 | val e = assertThrows(CyclicReferenceError::class.java) { 216 | workspace("cyclic").slice("main").validate(listOf(DependencyCycleValidator), "/a") 217 | } 218 | assertThat(e).hasMessageThat().contains("Cycle detected in 'main' at \"/a\":") 219 | } 220 | 221 | @Test fun cycleChain() { 222 | val e = assertThrows(CyclicReferenceError::class.java) { 223 | workspace("cyclic").slice("main").validate(listOf(DependencyCycleValidator), "/i") 224 | } 225 | assertThat(e).hasMessageThat().contains("Cycle detected in 'main' at \"/g\" from /i:") 226 | } 227 | 228 | @Test fun badVariant() { 229 | val ws = workspace("basic") 230 | val error = assertThrows(IllegalArgumentException::class.java) { 231 | ws.slice("nope") 232 | } 233 | assertThat(error).hasMessageThat().contains("No such variant \"nope\"") 234 | } 235 | 236 | @Test fun lookupModuleItself() { 237 | val ws = workspace("basic") 238 | val actual = requireNotNull(ws.findModule("/a")) 239 | assertThat(actual.module.name).isEqualTo("a") 240 | } 241 | 242 | @Test fun lookupNodeModuleItself() { 243 | val ws = workspace("basic") 244 | val actual = ws.findNode("/a").only() 245 | actual as GeneralFindResult 246 | assertThat(actual.node.module.name).isEqualTo("a") 247 | } 248 | 249 | @Test fun lookupFileInModule() { 250 | val ws = workspace("basic") 251 | val actual = requireNotNull(ws.findModule("/a/src/main/java/blah.kt")) 252 | assertThat(actual.module.name).isEqualTo("a") 253 | } 254 | 255 | @Test fun lookupNodeForFileInModule() { 256 | val ws = workspace("basic") 257 | val actual = ws.findNode("/a/blah.txt").only() 258 | actual as GeneralFindResult 259 | assertThat(actual.node.module.name).isEqualTo("a") 260 | } 261 | 262 | @Test fun lookupNodeForFileInBothVariants() { 263 | val ws = workspace("basic") 264 | val actual = 265 | (ws.findNode("/a/src/main/java/blah.kt").filterIsInstance()).toList() 266 | assertThat(actual).hasSize(2) 267 | assertThat(actual.map { it.variant }).containsExactly("debug", "release") 268 | assertThat(actual.map { it.node.module.name }.toSet()).containsExactly("a") 269 | } 270 | 271 | @Test fun lookupNodeForFileInOneVariant() { 272 | val ws = workspace("basic") 273 | val actual = ws.findNode("/a/src/debug/java/blah.kt").only() 274 | actual as VariantFindResult 275 | assertThat(actual.variant).isEqualTo("debug") 276 | assertThat(actual.node.module.name).isEqualTo("a") 277 | } 278 | 279 | @Test fun lookupNodeForFileInTest() { 280 | val ws = workspace("basic") 281 | val actual = ws.findNode("/a/src/test/java/blah.kt").only() 282 | actual as TestFindResult 283 | assertThat(actual.node.address).isEqualTo("/a:debug:unit") 284 | assertThat(actual.node.module).isEqualTo("/a") 285 | assertThat(actual.node.variant).isEqualTo("debug") 286 | assertThat(actual.node.name).isEqualTo("unit") 287 | } 288 | 289 | @Test fun lookupFileInDeeperModule() { 290 | val ws = workspace("basic") 291 | val actual = requireNotNull(ws.findModule("/group1/e/src/main/java/blah.kt")) 292 | assertThat(actual.module.name).isEqualTo("e") 293 | } 294 | 295 | @Test fun lookupFileNotInModule() { 296 | val ws = workspace("basic") 297 | val actual = ws.findModule("/q/src/main/java/blah.kt") 298 | assertThat(actual).isNull() 299 | } 300 | 301 | @Test fun lookupNodeFileNotInModule() { 302 | val ws = workspace("basic") 303 | val actual = ws.findNode("/q/src/main/java/blah.kt") 304 | assertThat(actual.toList()).isEmpty() 305 | } 306 | 307 | @Test fun lookupBadAddress() { 308 | val ws = workspace("basic") 309 | assertThrows(IllegalArgumentException::class.java) { 310 | ws.findModule("blah://blah.foo:bar") 311 | } 312 | } 313 | 314 | @Test fun lookupNodeBadAddress() { 315 | val ws = workspace("basic") 316 | assertThrows(IllegalArgumentException::class.java) { 317 | ws.findNode("blah://blah.foo:bar") 318 | } 319 | } 320 | 321 | @Test fun childModules() { 322 | val e = assertThrows(InvalidAddress::class.java) { workspace("child_modules").loadAll() } 323 | assertThat(e).hasMessageThat().contains("Unsupported address: /c") 324 | assertThat(e).hasMessageThat().isNotNull() 325 | assertThat(e).hasMessageThat().isNotEmpty() 326 | with(e.message!!) { 327 | // loading order is not guaranteed, so the error may come from above or below for nested 328 | // children. This structure avoids the flakiness inherent in that indeterminacy. 329 | assertWithMessage( 330 | "Address should be invalidated either from above or below, depending on load order" 331 | ).that( 332 | this.contains("/c/d (Path is below existing address /c)") || 333 | this.contains("/c (Path is above existing addresses [d])") 334 | ).isTrue() 335 | } 336 | } 337 | 338 | @Test fun nestedModules() { 339 | val ws = workspace("nested_workspaces").loadAll() 340 | assertThat(ws.findNode("/a").only().node.address).isEqualTo("/a") 341 | assertThat(ws.findNode("/b").only().node.address).isEqualTo("/b") 342 | assertThat(ws.findNode("/nested/c").toList()).isEmpty() 343 | assertThat(ws.findNode("/nested/d").toList()).isEmpty() 344 | } 345 | 346 | private fun workspace( 347 | tag: String, 348 | loadOnLookup: Boolean = true, 349 | followSymlinks: Boolean = false 350 | ): FileWorkspace = 351 | FileWorkspace.fromFile( 352 | workspaces.resolve(tag).resolve("workspace.spice.yml"), 353 | dynamicallyLoadModulesAlongPath = loadOnLookup, 354 | followSymlinks = followSymlinks 355 | ) 356 | 357 | private fun Sequence.only(): T { 358 | if (this.count() != 1) throw AssertionError("Expected iterable to have 1 element: $this") 359 | return first() 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/squareup/spice/serialization/IndexingTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.serialization 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.collections.deterministicMapOf 5 | import com.squareup.spice.model.Dependency 6 | import com.squareup.spice.model.FindResult.GeneralFindResult 7 | import com.squareup.spice.model.FindResult.TestFindResult 8 | import com.squareup.spice.model.FindResult.VariantFindResult 9 | import com.squareup.spice.model.InvalidAddress 10 | import com.squareup.spice.model.ModuleDocument 11 | import com.squareup.spice.model.Node.ModuleNode 12 | import com.squareup.spice.model.Node.TestNode 13 | import com.squareup.spice.model.TestConfiguration 14 | import com.squareup.spice.model.VariantConfiguration 15 | import org.junit.jupiter.api.Assertions.assertThrows 16 | import org.junit.jupiter.api.Test 17 | 18 | class IndexingTest { 19 | 20 | private val index = PathIndex() 21 | 22 | private val main = mapOf("src/main/java" to setOf("main")) 23 | private val testConfig = TestConfiguration(srcs = listOf("src/test/java"), tools = listOf("java")) 24 | 25 | @Test fun smokeTest() { 26 | index.addNode(ModuleNode("/foo/bar/baz/a", ModuleDocument(name = "a")), main, listOf()) 27 | index.addNode(ModuleNode("/foo/bar/baz/b", ModuleDocument(name = "b")), main, listOf()) 28 | index.addNode(ModuleNode("/foo/c", ModuleDocument(name = "c")), main, listOf()) 29 | index.addNode(ModuleNode("/foo/bar/d", ModuleDocument(name = "d")), main, listOf()) 30 | val testNode = TestNode("/e:main:unit", testConfig.copy(deps = listOf(Dependency("/foo/c")))) 31 | index.addNode( 32 | ModuleNode( 33 | "/e", 34 | ModuleDocument( 35 | name = "e", 36 | variants = deterministicMapOf( 37 | "main" to VariantConfiguration( 38 | srcs = listOf("src/main/java"), 39 | tests = deterministicMapOf("unit" to testNode.config) 40 | ) 41 | ) 42 | ) 43 | ), 44 | main, 45 | tests = listOf(testNode) 46 | ) 47 | assertThat(index.moduleForPath("/foo/c")?.module?.name).isEqualTo("c") 48 | assertThat(index.moduleForPath("/e")?.module?.name).isEqualTo("e") 49 | assertThat(index.moduleForPath("/foo/bar/baz/b")?.module?.name).isEqualTo("b") 50 | assertThat(index.moduleForPath("/foo/bar/d")?.module?.name).isEqualTo("d") 51 | assertThat(index.moduleForPath("/foo/bar/baz/a")?.module?.name).isEqualTo("a") 52 | index.nodeForPath("/e").only().let { actual -> 53 | actual as GeneralFindResult 54 | assertThat(actual.node.module.name).isEqualTo("e") 55 | } 56 | index.nodeForPath("/e/src/main/java/foo.bar").only().let { actual -> 57 | actual as VariantFindResult 58 | assertThat(actual.variant).isEqualTo("main") 59 | assertThat(actual.node.module.name).isEqualTo("e") 60 | } 61 | index.nodeForPath("/e/src/test/java/foo.bar").only().let { actual -> 62 | actual as TestFindResult 63 | assertThat(actual.node.address).isEqualTo("/e:main:unit") 64 | } 65 | } 66 | 67 | @Test fun notFound() { 68 | assertThat(index.moduleForPath("/b")).isNull() 69 | index.addNode(ModuleNode("/a", ModuleDocument(name = "a")), main, listOf()) 70 | assertThat(index.moduleForPath("/b")).isNull() 71 | assertThat(index.nodeForPath("/b").toList()).isEmpty() 72 | index.addNode(ModuleNode("/b", ModuleDocument(name = "b")), main, listOf()) 73 | assertThat(index.moduleForPath("/b")!!.module!!.name).isEqualTo("b") 74 | index.nodeForPath("/b").only().let { actual -> 75 | actual as GeneralFindResult 76 | assertThat(actual.node.module.name).isEqualTo("b") 77 | } 78 | } 79 | 80 | @Test fun findModuleForFile() { 81 | index.addNode(ModuleNode("/foo/bar/baz/a", ModuleDocument(name = "a")), main, listOf()) 82 | index.addNode(ModuleNode("/foo/bar/baz/b", ModuleDocument(name = "b")), main, listOf()) 83 | index.addNode(ModuleNode("/foo/c", ModuleDocument(name = "c")), main, listOf()) 84 | index.addNode(ModuleNode("/foo/bar/d", ModuleDocument(name = "d")), main, listOf()) 85 | index.addNode(ModuleNode("/e", ModuleDocument(name = "e")), main, listOf()) 86 | val actual = index.moduleForPath("/foo/bar/d/src/main/java/com/squareup/Something.java") 87 | assertThat(actual?.module?.name).isEqualTo("d") 88 | } 89 | 90 | @Test fun overspecifiedPaths() { 91 | index.addNode(ModuleNode("/a", ModuleDocument(name = "a")), main, listOf()) 92 | assertThat(index.moduleForPath("/a/")?.module?.name).isEqualTo("a") 93 | index.addNode(ModuleNode("/b/", ModuleDocument(name = "b")), main, listOf()) 94 | assertThat(index.moduleForPath("/b")?.module?.name).isEqualTo("b") 95 | } 96 | 97 | @Test fun addNodeCollisionWithEqualNodes() { 98 | val node1 = ModuleNode("/foo/bar/baz/bash", ModuleDocument(name = "bar")) 99 | val node2 = ModuleNode("/foo/bar/baz/bash", ModuleDocument(name = "bar")) 100 | index.addNode(node1, main, listOf()) 101 | index.addNode(node2, main, listOf()) 102 | // should not error by the end. 103 | } 104 | 105 | @Test fun addNodeCollisionWithUnequalNodes() { 106 | val node1 = ModuleNode("/foo/bar/baz/bash", ModuleDocument(name = "bar")) 107 | val node2 = ModuleNode("/foo/bar/baz/bash", ModuleDocument(name = "foo")) 108 | assertThrows(IllegalArgumentException::class.java) { 109 | index.addNode(node1, main, listOf()) 110 | index.addNode(node2, main, listOf()) 111 | } 112 | } 113 | 114 | @Test fun addNodeCollisionWithSubpath() { 115 | val node1 = ModuleNode("/foo/bar/baz/bash", ModuleDocument(name = "bar")) 116 | val node2 = ModuleNode("/foo/bar", ModuleDocument(name = "foo")) 117 | assertThrows(InvalidAddress::class.java) { 118 | index.addNode(node1, main, listOf()) 119 | index.addNode(node2, main, listOf()) 120 | } 121 | } 122 | 123 | @Test fun addNodeCollisionWithSuperpath() { 124 | val node1 = ModuleNode("/foo/bar", ModuleDocument(name = "foo")) 125 | val node2 = ModuleNode("/foo/bar/baz/bash", ModuleDocument(name = "bar")) 126 | assertThrows(InvalidAddress::class.java) { 127 | index.addNode(node1, main, listOf()) 128 | index.addNode(node2, main, listOf()) 129 | } 130 | } 131 | 132 | private fun Sequence.only(): T { 133 | if (this.count() != 1) throw AssertionError("Expected iterable to have 1 element: $this") 134 | return first() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/squareup/spice/serialization/ScaleTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.serialization 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.model.validation.DependencyCycleValidator 5 | import com.squareup.spice.model.validation.GraphCompletenessValidator 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import kotlinx.coroutines.FlowPreview 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import java.io.File 11 | import java.nio.file.Files 12 | import kotlin.system.measureTimeMillis 13 | 14 | // TODO(MDX-3298) turn this into a caliper microbenchmark or something. 15 | @FlowPreview 16 | @ExperimentalCoroutinesApi 17 | class ScaleTest { 18 | /** Creates a temp dir either in bazel's test dir, or system temp, and cleans up after the test */ 19 | private val tmpDir by lazy { 20 | Files.createTempDirectory(ScaleTest::class.simpleName).toFile() 21 | } 22 | 23 | private val wsText = """ 24 | --- 25 | name: scale 26 | definitions: 27 | tools: [kotlin] 28 | variants: 29 | main: 30 | srcs: [src/main] 31 | 32 | declared_tools: 33 | - name: kotlin 34 | 35 | """.trimIndent() 36 | 37 | // These start/end nodes are abstracted mostly to ease tweaking them while debugging the test. 38 | private val start = 'a' 39 | private val end = 'j' 40 | 41 | private lateinit var wsFile: File 42 | 43 | /** Prepare a ginormous spice graph */ 44 | @kotlin.ExperimentalStdlibApi 45 | @BeforeEach fun prepareGraph() { 46 | val wsDir = tmpDir 47 | wsFile = wsDir.resolve("workspace.spice.yml") 48 | wsFile.writeText(wsText) 49 | val range = start..end 50 | val outer = range.toMutableList() 51 | while (outer.isNotEmpty()) { 52 | val w = outer.removeLast() 53 | for (x in range) { 54 | for (y in range) { 55 | for (z in range) { 56 | val moduleDir = wsDir.resolve("$w/$x/$y/$z").apply { mkdirs() } 57 | val deps = when { 58 | setOf(x, y, z).all { it == end } -> { 59 | val wSet = outer.reversed() + w 60 | wSet.flatMap { w1 -> 61 | (start..end).flatMap { x1 -> 62 | (start..end).flatMap { y1 -> 63 | (start..end).flatMap { z1 -> 64 | if (listOf(w1, x1, y1, z1) == listOf(w, x, y, z)) listOf() 65 | else listOf("/$w1/$x1/$y1/$z1") 66 | } 67 | } 68 | } 69 | } 70 | } 71 | z == start -> { 72 | if (outer.isEmpty()) listOf() else listOf("/${w - 1}/$end/$end/$end") 73 | } 74 | else -> listOf("/$w/$x/$y/${z - 1}") 75 | } 76 | moduleDir.resolve("module.spice.yml").apply { 77 | val moduleText = """ 78 | --- # Module for /$w/$x/$y/$z 79 | name: $z 80 | variants: 81 | main: 82 | deps: 83 | 84 | """.trimIndent() + 85 | deps.joinToString("") { dep -> " - $dep\n" } 86 | writeText(moduleText) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | @Test fun bulkLoadWorkspaceWithFileWalk() { 95 | val ws = FileWorkspace.fromFile(wsFile) 96 | val slice = ws.slice("main") 97 | assertThat(ws.nodeIndex.size).isEqualTo( 98 | 0 99 | ) // omitting hasSize to avoid loading giant error strings 100 | assertThat(slice.depsIndex.size).isEqualTo(0) 101 | assertThat(slice.rdepsIndex.size).isEqualTo(0) 102 | 103 | val loadTime = measureTimeMillis { 104 | ws.loadAll() 105 | ws.validate(listOf(GraphCompletenessValidator, DependencyCycleValidator)) 106 | } 107 | val forwardEdges = slice.depsIndex.entries.flatMap { (_, v) -> v }.count() 108 | val reverseEdges = slice.rdepsIndex.entries.flatMap { (_, v) -> v }.count() 109 | System.err.println( 110 | "\n" + 111 | "TRACE: Universe of ${ws.nodeIndex.size} nodes, $forwardEdges forward edges, " + 112 | " and $reverseEdges reverse edges bulk-loaded and validated in $loadTime ms" 113 | ) 114 | assertThat(ws.nodeIndex.size).isEqualTo(10000) 115 | // omitting hasSize to avoid loading giant strings 116 | assertThat(forwardEdges).isEqualTo(64880) 117 | assertThat(reverseEdges).isEqualTo(64880) 118 | } 119 | 120 | @Test fun validateGinormousWorkspace() { 121 | 122 | val ws = FileWorkspace.fromFile(wsFile) 123 | val slice = ws.slice("main") 124 | val loadTime = measureTimeMillis { 125 | slice.validate( 126 | listOf(GraphCompletenessValidator, DependencyCycleValidator), 127 | "/$end/$end/$end/$end" 128 | ) 129 | } 130 | val edgeCount = ws.nodeIndex.values.flatMap { slice.dependenciesOf(it).map { Unit } }.count() 131 | System.err.println( 132 | "\n" + 133 | "TRACE: Universe of ${ws.nodeIndex.size} nodes and $edgeCount edges " + 134 | "loaded and cycle-checked in $loadTime ms" 135 | ) 136 | var lookupCount = 0 137 | val lookupTime = measureTimeMillis { 138 | for (x in 1..2) { 139 | for (i in start..end) { 140 | for (j in start..end) { 141 | for (k in start..end) { 142 | for (l in start..end) { 143 | val pathToLookup = "/$j/$i/$l/$k/src/main/whatever.kt" 144 | val node = ws.findModule(pathToLookup) 145 | requireNotNull(node) { "Could not find module for $pathToLookup in workspace" } 146 | lookupCount++ 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | val lookupAvg = lookupTime * 1000.0 / lookupCount 154 | System.err.println("TRACE: Looked up $lookupCount nodes in $lookupTime ms (avg: $lookupAvg µs)") 155 | 156 | var noHitCount = 0 157 | val noHitTime = measureTimeMillis { 158 | for (i in start..end) { 159 | for (j in start..end) { 160 | for (k in start..end step 2) { 161 | for (l in start..end step 2) { 162 | val node = ws.findModule("/no/such/path/$i$j$k$l/src/main/whatever.kt") 163 | assertThat(node).isNull() 164 | noHitCount++ 165 | } 166 | } 167 | } 168 | } 169 | } 170 | val noHitAvg = noHitTime * 1000.0 / noHitCount 171 | System.err.println( 172 | "TRACE: Looked up $noHitCount non-existant nodes in $noHitTime ms (avg: $noHitAvg µs)" 173 | ) 174 | // omitting hasSize to avoid loading giant strings 175 | assertThat(ws.nodeIndex.size).isEqualTo(10000) 176 | assertThat(edgeCount).isEqualTo(64880) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/squareup/spice/serialization/SerializerTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.serialization 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.collections.deterministicMapOf 5 | import com.squareup.spice.model.Dependency 6 | import com.squareup.spice.model.ExternalNodeConfiguration 7 | import com.squareup.spice.model.ExternalWorkspaceDocument 8 | import com.squareup.spice.model.ModuleDocument 9 | import com.squareup.spice.model.ToolDefinition 10 | import com.squareup.spice.model.VariantConfiguration 11 | import com.squareup.spice.model.WorkspaceDocument 12 | import org.junit.jupiter.api.Test 13 | 14 | class SerializerTest { 15 | 16 | @Test fun roundTripFromWorkspaceObject() { 17 | val serializer = Serializer() 18 | val initial = WorkspaceDocument( 19 | name = "foo", 20 | tools = listOf( 21 | ToolDefinition("java"), 22 | ToolDefinition("kotlin"), 23 | ToolDefinition("anvil"), 24 | ToolDefinition("dagger"), 25 | ToolDefinition("anvil_library", not = listOf("dagger")) 26 | ), 27 | definitions = ModuleDocument( 28 | variants = deterministicMapOf( 29 | "main" to VariantConfiguration( 30 | deps = listOf( 31 | Dependency("foo/bar", listOf("a", "b")), 32 | Dependency("foo/baz") 33 | ) 34 | ) 35 | ) 36 | ), 37 | external = listOf( 38 | ExternalWorkspaceDocument( 39 | "maven", 40 | "maven", 41 | deterministicMapOf(), 42 | deterministicMapOf( 43 | "com.google.guava:guava:25.0" to ExternalNodeConfiguration( 44 | exclude = listOf("blah:foo") 45 | ), 46 | "com.google.truth:truth:1.0" to null, 47 | "junit:junit:4.13" to null 48 | ) 49 | ) 50 | ) 51 | ) 52 | val yaml = serializer.marshall(initial) 53 | val actual = serializer.unmarshall(yaml, WorkspaceDocument::class.java) 54 | assertThat(actual).isEqualTo(initial) 55 | } 56 | 57 | @Test fun parseWorkspaceYaml() { 58 | // Yes this is brittle, yes it's testing parsing. But it's making sure the data objects are what 59 | // we want 60 | val serializer = Serializer() 61 | val initial = """ 62 | --- 63 | name: "foo" 64 | 65 | definitions: 66 | namespace: "com.squareup.spice" 67 | tools: ["kotlin"] 68 | variants: 69 | debug: 70 | srcs: ["src/main", "src/debug"] 71 | tests: 72 | "unit": 73 | srcs: ["src/test"] 74 | deps: 75 | - "blah://com.google.truth" 76 | - "blah://junit" 77 | release: 78 | srcs: ["src/main", "src/release"] 79 | tests: 80 | ui: { srcs: ["src/androidTest"] } 81 | beta: 82 | srcs: ["src/main", "src/release", "src/beta"] # uses release sources AND beta sources 83 | tests: 84 | ui: { srcs: ["src/androidTest"] } 85 | 86 | 87 | 88 | declared_tools: 89 | - name: "java" 90 | - name: "kotlin" 91 | - name: "anvil" 92 | - name: "dagger" 93 | - name: "anvil_library" 94 | not: 95 | - "dagger" 96 | external: 97 | - name: "blah" 98 | type: "maven" 99 | properties: 100 | repositories: 101 | - central::http://repo1.maven.org 102 | artifacts: 103 | com.google.truth:truth:1.0: 104 | exclude: ["foo:bar"] 105 | include: ["bar:foo"] 106 | deps: ["blah:foo"] # mutually exclusive to exclude/include 107 | junit:junit:4.13: 108 | "com.foo.bar:bar:1234": 109 | """.trimIndent() 110 | val actual = serializer.unmarshall(initial, WorkspaceDocument::class.java) 111 | assertThat(actual.name).isEqualTo("foo") 112 | assertThat(actual.tools).hasSize(5) 113 | assertThat(actual.tools.last().not).isEqualTo(listOf("dagger")) 114 | assertThat(actual.tools.last().not).isEqualTo(listOf("dagger")) 115 | assertThat(actual.external).hasSize(1) 116 | // TODO - allow for a find-by-type or other better apis than a cast. 117 | val mavenWorkspace = actual.external.first() 118 | assertThat(mavenWorkspace.name).isEqualTo("blah") 119 | assertThat(mavenWorkspace.type).isEqualTo("maven") 120 | with(mavenWorkspace.properties["repositories"]) { 121 | requireNotNull(this) 122 | assertThat(this).isEqualTo(listOf("central::http://repo1.maven.org")) 123 | } 124 | assertThat(mavenWorkspace.artifacts).hasSize(3) 125 | with(mavenWorkspace.artifacts["com.google.truth:truth:1.0"]) { 126 | requireNotNull(this) 127 | // Note: validation should prevent these three being all non-empty/null but happens elsewhere. 128 | assertThat(exclude).isEqualTo(listOf("foo:bar")) 129 | assertThat(include).isEqualTo(listOf("bar:foo")) 130 | assertThat(deps).isEqualTo(listOf("blah:foo")) 131 | } 132 | } 133 | 134 | @Test fun parseModuleYaml() { 135 | // Yes this is brittle, yes it's testing parsing. But it's making sure the data objects are what 136 | // we want 137 | val serializer = Serializer() 138 | val initial = """ 139 | --- # Project Module 140 | name: "foo-bar" 141 | namespace: "com.squareup.spice" 142 | tools: ["kotlin"] 143 | variants: 144 | debug: 145 | srcs: 146 | - "src/main" 147 | - "src/debug" 148 | tests: 149 | unit: 150 | srcs: ["src/test"] 151 | deps: 152 | - /foo/blah: [foo] 153 | - maven://com.google.truth 154 | - maven://junit 155 | release: 156 | srcs: ["src/main", "src/release"] 157 | tests: 158 | ui: { srcs: ["src/androidTest"] } 159 | """.trimIndent() 160 | val actual = serializer.unmarshall(initial, ModuleDocument::class.java) 161 | with(actual) { 162 | assertThat(name).isEqualTo("foo-bar") 163 | assertThat(namespace).isEqualTo("com.squareup.spice") 164 | assertThat(tools).isEqualTo(setOf("kotlin")) 165 | assertThat(variants).hasSize(2) 166 | assertThat(variants.keys).isEqualTo(setOf("debug", "release")) 167 | with(variants["debug"]) { 168 | requireNotNull(this) 169 | assertThat(this.srcs).isEqualTo(listOf("src/main", "src/debug")) 170 | assertThat(this.tests).hasSize(1) 171 | with(tests["unit"]) { 172 | requireNotNull(this) 173 | assertThat(this.srcs).isEqualTo(listOf("src/test")) 174 | assertThat(this.deps).hasSize(3) 175 | assertThat(this.deps[0].tags).isEqualTo(listOf("foo")) 176 | } 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /core/src/test/resources/BUILD.bazel: -------------------------------------------------------------------------------- 1 | filegroup( 2 | name = "file_workspaces", 3 | srcs = glob(["file_workspaces/**"]), 4 | visibility = [ 5 | "//visibility:public", 6 | ], 7 | ) 8 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/basic/a/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: a 3 | variants: 4 | debug: 5 | release: 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/basic/b/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: b 3 | variants: 4 | debug: 5 | deps: 6 | - /a 7 | release: 8 | deps: 9 | - /a 10 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/basic/c/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: c 3 | variants: 4 | debug: 5 | deps: 6 | - /b 7 | release: 8 | deps: 9 | - /a 10 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/basic/group1/e/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: e 3 | variants: 4 | debug: 5 | deps: 6 | - /c 7 | release: 8 | deps: 9 | - /b 10 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/basic/group1/f/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: b 3 | variants: 4 | debug: 5 | deps: 6 | - /a 7 | - /b 8 | release: 9 | deps: 10 | - /group1/e 11 | - /c 12 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/basic/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "basic" 3 | 4 | definitions: 5 | namespace: "spice.test.example.basic" 6 | tools: ["kotlin"] 7 | variants: 8 | debug: 9 | srcs: ["src/main", "src/debug"] 10 | tests: 11 | unit: 12 | srcs: ["src/test"] 13 | release: 14 | srcs: ["src/main", "src/release"] 15 | 16 | 17 | declared_tools: # Tools declared and available to projects in the workspace 18 | - name: kotlin 19 | 20 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/child_modules/a/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: a 3 | variants: 4 | main: 5 | deps: 6 | - /q 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/child_modules/b/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: b 3 | variants: 4 | main: 5 | deps: 6 | - /a 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/child_modules/c/d/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: d 3 | variants: 4 | main: 5 | deps: 6 | - /b 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/child_modules/c/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: c 3 | variants: 4 | main: 5 | deps: 6 | - /b 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/child_modules/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "child_modules" 3 | 4 | definitions: 5 | namespace: "spice.test.example.basic" 6 | tools: ["kotlin"] 7 | variants: 8 | main: 9 | srcs: ["src/main", "src/debug"] 10 | 11 | declared_tools: # Tools declared and available to projects in the workspace 12 | - name: kotlin 13 | 14 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/a/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /b 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/b/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /a 6 | 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/c/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /c 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/d/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /g 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/e/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /d 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/f/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /e 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/g/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /f 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/h/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /g 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/i/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | variants: 3 | main: 4 | deps: 5 | - /h 6 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/cyclic/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "cyclic" 3 | 4 | definitions: 5 | namespace: "spice.test.example.cyclic" 6 | tools: ["kotlin"] 7 | variants: 8 | main: 9 | srcs: ["src/main"] 10 | 11 | declared_tools: # Tools declared and available to projects in the workspace 12 | - name: kotlin 13 | 14 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/dangling/a/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: a 3 | variants: 4 | main: 5 | deps: 6 | - /q 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/dangling/b/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: b 3 | variants: 4 | main: 5 | deps: 6 | - /a 7 | - /q 8 | - /r 9 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/dangling/c/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: c 3 | variants: 4 | main: 5 | deps: 6 | - /b 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/dangling/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "dangling" 3 | 4 | definitions: 5 | namespace: "spice.test.example.basic" 6 | tools: ["kotlin"] 7 | variants: 8 | main: 9 | srcs: ["src/main", "src/debug"] 10 | 11 | declared_tools: # Tools declared and available to projects in the workspace 12 | - name: kotlin 13 | 14 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/nested_workspaces/a/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: a 3 | variants: 4 | main: 5 | deps: 6 | - /q 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/nested_workspaces/b/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: b 3 | variants: 4 | main: 5 | deps: 6 | - /a 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/nested_workspaces/nested/c/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: c 3 | 4 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/nested_workspaces/nested/d/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: d 3 | variants: 4 | foo: 5 | srcs: ["src/main", "src/gen"] 6 | deps: 7 | - /c 8 | tests: 9 | unit: 10 | srcs: ["src/test"] 11 | deps: 12 | - /d 13 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/nested_workspaces/nested/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "nested" 3 | 4 | definitions: 5 | namespace: "spice.test.example.nested_workspaces.nested" 6 | tools: ["kotlin"] 7 | variants: 8 | foo: 9 | srcs: ["src/main", "src/debug"] 10 | 11 | declared_tools: # Tools declared and available to projects in the workspace 12 | - name: kotlin 13 | 14 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/nested_workspaces/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "nested_modules" 3 | 4 | definitions: 5 | namespace: "spice.test.example.nested_workspaces" 6 | tools: ["kotlin"] 7 | variants: 8 | main: 9 | srcs: ["src/main", "src/debug"] 10 | 11 | declared_tools: # Tools declared and available to projects in the workspace 12 | - name: kotlin 13 | 14 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/no_workspace_file/a/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: a 3 | variants: 4 | main: 5 | deps: 6 | - /q 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/no_workspace_file/b/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: b 3 | variants: 4 | main: 5 | deps: 6 | - /a 7 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/with_symlinks/with_symlinks.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/core/src/test/resources/file_workspaces/with_symlinks/with_symlinks.tgz -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/with_tests/a/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: a 3 | variants: 4 | debug: 5 | tests: 6 | unit: 7 | deps: 8 | - /a 9 | - /b 10 | release: 11 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/with_tests/b/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: b 3 | variants: 4 | debug: 5 | deps: 6 | - /a 7 | tests: 8 | unit: 9 | deps: 10 | - /a 11 | - /b 12 | release: 13 | deps: 14 | - /a 15 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/with_tests/c/module.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: c 3 | variants: 4 | debug: 5 | deps: 6 | - /b 7 | release: 8 | deps: 9 | - /a 10 | -------------------------------------------------------------------------------- /core/src/test/resources/file_workspaces/with_tests/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "basic" 3 | 4 | definitions: 5 | namespace: "spice.test.example.basic" 6 | tools: ["kotlin"] 7 | variants: 8 | debug: 9 | srcs: ["src/main", "src/debug"] 10 | tests: 11 | unit: 12 | srcs: ["src/test"] 13 | release: 14 | srcs: ["src/main", "src/release"] 15 | 16 | declared_tools: # Tools declared and available to projects in the workspace 17 | - name: kotlin 18 | 19 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Spice Apps 2 | 3 | Applications of Spice - cli or other applications of the spice APIs and model. 4 | 5 | -------------------------------------------------------------------------------- /examples/complete/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- # Root workspace of the overall Spice project. 2 | name: "spice" 3 | 4 | # project attributes shared by all projects in this group 5 | definitions: 6 | # A logical group, typically a maven-style groupId providing a namespace context within which 7 | # all of the projects live. 8 | namespace: "com.squareup.spice" 9 | 10 | tools: ["kotlin"] 11 | variants: 12 | - debug: 13 | srcs: ["src/main", "src/debug"] 14 | tests: 15 | - unit: 16 | srcs: ["src/test"] 17 | deps: 18 | - "maven://com.google.truth" 19 | - "maven://junit" 20 | - release: 21 | srcs: ["src/main", "src/release"] 22 | tests: 23 | - ui: 24 | srcs: ["src/androidTest"] 25 | - beta: 26 | srcs: ["src/main", "src/release", "src/beta"] # uses release sources AND beta sources 27 | tests: 28 | - ui: 29 | srcs: ["src/androidTest"] 30 | 31 | 32 | # A list of known tags that may be set in any "tags" section. Used for validation/typo-avoidance. 33 | # These tags are consumed by metadata-aware tools or templates, but may be ignored if irrelevant. 34 | # Tags will be 35 | tags: ["exported"] 36 | 37 | tools: # Tools declared and available to projects in the workspace 38 | - name: kotlin 39 | 40 | # TODO: Work out how to have different flavors of artifacts that maybe have different deps. 41 | # Might be needed, could be a different external workspace? Aliases/Redirects? 42 | 43 | external: 44 | - name: maven 45 | type: maven 46 | repositories: 47 | - id: central 48 | url: http://repo1.maven.org 49 | # The artifacts: list can get really long, so allow for loading it separately 50 | # TODO: Alternatively, consider using the << referencing system, if that can be made multi-file. 51 | # artifact-file: "path/relative/to/workspace/artifacts.spice.yml" 52 | artifacts: 53 | com.google.truth:truth:1.0: 54 | exclude: "foo:bar" 55 | include: "bar:foo" 56 | deps: "blah:foo" # mutually exclusive to exclude/include 57 | junit:junit:4.13: 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/minimal/workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "minimal" 3 | 4 | definitions: 5 | tools: ["java"] 6 | variants: 7 | - main: 8 | srcs: ["src/main"] 9 | tests: 10 | - unit: 11 | srcs: ["src/test"] 12 | deps: 13 | - "maven://junit" 14 | 15 | tools: 16 | - name: java 17 | 18 | external: 19 | - name: maven 20 | type: maven 21 | repositories: 22 | - id: central 23 | url: http://repo1.maven.org 24 | artifacts: 25 | junit:junit:4.13: 26 | # ... add more here. 27 | 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle settings for local development. CI uses the other gradle.properties.* files in this dir. 2 | # If you add or modify something here, consider whether a related change should be made on CI. 3 | 4 | # IDE (e.g. Android Studio) users: 5 | # Gradle settings configured through the IDE *will override* 6 | # any settings specified in this file. 7 | 8 | # For more details on how to configure your build environment visit 9 | # http://www.gradle.org/docs/current/userguide/build_environment.html 10 | 11 | # Specifies the JVM arguments used for the daemon process. Note that when running with 12 | # org.gradle.daemon=false, for instance on Kochiku, the GradleDaemon process is still 13 | # started with these arguments. It just doesn't live on after the build completes. 14 | # 15 | # We use the standard G1GC from JDK 11. Here are the various options used for the JVM: 16 | # (source : https://docs.oracle.com/en/java/javase/11/gctuning/index.html) 17 | # -XX:G1HeapRegionSize: the size of the G1 Regions. It must be a power of 2 between 1M and 32M. 18 | # At most there should be 2048 regions. Setting this value sets an upperbound to Xmx and helps 19 | # the JVM to estimate the peak size of the heap. 20 | # 1M --> Xmx = 2G 2M --> Xmx = 4G 21 | # 4M --> Xmx = 8G 8M --> Xmx = 16G 22 | # 16M --> Xmx = 32G 32M --> Xmx = 64G 23 | # -XX:MinHeapFreeRatio and -XX:MaxHeapFreeRatio: min and max ratio of tolerated free / unused heap. 24 | # If the amount of unused heap is not in this ratio, a GC is triggered. Keeping the range low will 25 | # expand and shrink the heap on demand. 26 | # -XX:GCTimeLimit=20: The upper limit on the amount of time spent in garbage collection in percent of 27 | # total time (default is 98). 28 | # -XX:+UseCompressedOops: Use 32 bit Object Pointers on 64 bit machines. 29 | # The option provides better performance of the JVM. Use only if xmx < 32GB. 30 | 31 | org.gradle.jvmargs=-XX:+UseCompressedOops -XX:G1HeapRegionSize=16M -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeLimit=20 -Xmx17g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Djava.awt.headless=true 32 | 33 | kotlin.daemon.jvmargs=-Xmx5g 34 | 35 | # When configured, Gradle will run in incubating parallel mode. 36 | # This option should only be used with decoupled projects. More details, visit 37 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 38 | org.gradle.parallel=true 39 | 40 | # Use the gradle daemon 41 | org.gradle.daemon=true 42 | 43 | # Enable the Gradle Build Cache 44 | org.gradle.caching=true 45 | 46 | # Only configure modules required for the current build 47 | org.gradle.configureondemand=true 48 | 49 | # Enable File System Watching 50 | org.gradle.vfs.watch=true 51 | 52 | # Prevent uploads of .512 check sum files to artifactory (This is for :in-app-payments) 53 | systemProp.org.gradle.internal.publish.checksums.insecure=true 54 | 55 | # Prevents lint from growing a head. 56 | systemProp.java.awt.headless=true 57 | 58 | # Informs Gradle scripts that this is a local build. 59 | systemProp.ci=false 60 | 61 | # Use Android X instead of the support library. 62 | android.useAndroidX=true 63 | 64 | # Recompile dependencies that use the support library to use Android X instead. 65 | # Needed for external libraries that still use the support library (e.g. sticky headers) 66 | android.enableJetifier=true 67 | 68 | # Tell Jetifier to ignore certain jars, by regex 69 | # bcprov: Needed for the latest Robolectric version, otherwise Jetifier will be mad https://github.com/robolectric/robolectric/issues/6521 70 | # common: needed for Paparazzi 71 | # moshi: moshi 1.13.0+ is a multi-jar, which confuses jetifier https://github.com/square/moshi/issues/1454#issuecomment-989981705 72 | android.jetifier.ignorelist=bcprov,common-\\d{2}\\.\\d+\\.\\d+\\.jar,moshi 73 | 74 | # Suppress experimental option warnings during configuration (including this one!) 75 | android.suppressUnsupportedOptionWarnings=android.dexingNumberOfBuckets,android.suppressUnsupportedOptionWarnings,android.nonTransitiveRClass,android.enableAppCompileTimeRClass 76 | 77 | # Enable rudimentary R class namespacing where each module 78 | # only contains references to the resources it declares instead 79 | # of declarations plus all transitive dependency references. 80 | android.nonTransitiveRClass=true 81 | 82 | # https://issuetracker.google.com/issues/110374966 83 | android.dexingNumberOfBuckets=1 84 | 85 | # Package resources in application modules just as library modules do. 86 | android.enableAppCompileTimeRClass=true 87 | 88 | # Turn on incremental annotation processing with KAPT 89 | # https://blog.jetbrains.com/kotlin/2019/04/kotlin-1-3-30-released/ 90 | kapt.incremental.apt=true 91 | 92 | # Use Gradle's worker API for KAPT. 93 | # https://blog.jetbrains.com/kotlin/2019/04/kotlin-1-3-30-released/ 94 | kapt.use.worker.api=true 95 | 96 | # Turn on compile avoidance for annotation processors 97 | # https://blog.jetbrains.com/kotlin/2019/04/kotlin-1-3-30-released/ 98 | kapt.include.compile.classpath=false 99 | 100 | # Don't recompile Kotlin code if changes in Java don't have an effect. 101 | # https://blog.jetbrains.com/kotlin/2018/01/kotlin-1-2-20-is-out/ 102 | kotlin.incremental.usePreciseJavaTracking=true 103 | 104 | # Do not turn on. By default we use the Kotlin daemon and it performs better than running Kotlin 105 | # compilations in-process. 106 | #kotlin.compiler.execution.strategy="in-process" 107 | 108 | # Enable logging from the kotlin daemon. 109 | # https://jira.sqprod.co/browse/MDX-2626 110 | # Some builds randomly report issues of connections between the gradle daemon and the kotlin 111 | # compiler daemon. 112 | kotlin.daemon.debug.log=true 113 | 114 | # Disable a few buildFeatures by default in AGP 4+ 115 | # Individual modules can enable features as needed individually: 116 | # e.g.: android.buildFeatures.buildconfig true 117 | # Names of default options for both libs, apps and common build features 118 | # are available in https://android.googlesource.com/platform/tools/base/+/mirror-goog-studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/options/BooleanOption.kt 119 | # Listed in https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-master-dev/build-system/gradle-api/src/main/java/com/android/build/api/dsl/BuildFeatures.kt 120 | android.defaults.buildfeatures.buildconfig=false 121 | android.defaults.buildfeatures.aidl=false 122 | android.defaults.buildfeatures.renderScript=false 123 | android.defaults.buildfeatures.compose=false 124 | android.defaults.buildfeatures.resValues=false 125 | android.defaults.buildfeatures.viewBinding=false 126 | android.defaults.buildfeatures.shaders=false 127 | android.defaults.buildfeatures.prefab=false 128 | 129 | # Options for libraries modules 130 | # Listed in https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-master-dev/build-system/gradle-api/src/main/java/com/android/build/api/dsl/LibraryBuildFeatures.kt 131 | android.library.defaults.buildfeatures.androidresources=false 132 | android.defaults.buildfeatures.dataBinding=false 133 | android.defaults.buildfeatures.mlModelBinding=false 134 | android.defaults.buildfeatures.prefabPublishing=false 135 | 136 | mavenRepoUrl=https://maven.global.square 137 | 138 | # For faster builds locally we only bundle x86 (for emulators) and ARM 64 Bit (most physical devices these days). 139 | square.debugAbiFilter=x86,arm64-v8a 140 | 141 | # Dependency Analysis Gradle Plugin 142 | dependency.analysis.autoapply=false 143 | dependency.analysis.test.analysis=false 144 | 145 | # This is required temporarily as the default convention plugin behavior when not set is to 146 | # add the common:profileable:public dependency which adds disabled components. Instead we just don't 147 | # want that dependency at all. Once the convention plugin is released again we can undo this change. 148 | square.enableProfiling=false 149 | 150 | # Plugin versions 151 | square.shadowVersion=7.1.2 152 | square.paparazziVersion=1.0.0-internal03 153 | 154 | square.registerPluginsVersion.consumer=1.41.1 155 | square.registerPluginsVersion.producer=1.41.1 156 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | jackson = "2.12.4" 3 | okhttp = "4.5.0" 4 | okio = "2.4.1" 5 | retrofit = "2.7.2" 6 | wire = "3.0.0" 7 | xenomachina = "2.0.7" 8 | kotlinx = "1.5.1" 9 | snakeyaml = "1.26" 10 | junit = "5.8.2" 11 | junit-gradle = "1.2.0" 12 | truth = "1.0" 13 | 14 | 15 | [libraries] 16 | jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } 17 | jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } 18 | jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } 19 | jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } 20 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" } 21 | okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } 22 | okio = { module = "com.squareup.okio:okio", version.ref = "okio" } 23 | retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } 24 | retrofit-wire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" } 25 | snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } 26 | wire-runtime = { module = "com.squareup.wire:wire-runtime", version.ref = "wire" } 27 | xenomachina-argparser = { module = "com.xenomachina:kotlin-argparser", version.ref = "xenomachina" } 28 | junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } 29 | junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } 30 | junit-gradle = { module = "org.junit.platform:junit-platform-gradle-plugin", version.ref = "junit-gradle" } 31 | google-truth = { module = "com.google.truth:truth", version.ref = "truth" } 32 | -------------------------------------------------------------------------------- /model/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 2 | 3 | kt_jvm_library( 4 | name = "model", 5 | srcs = glob( 6 | ["src/main/**/*.kt"], 7 | exclude = ["src/main/**/test/*.kt"], 8 | ), 9 | visibility = ["//:__subpackages__"], 10 | deps = [ 11 | "@maven//com/fasterxml/jackson/core:jackson-annotations", 12 | ], 13 | ) 14 | 15 | kt_jvm_library( 16 | name = "test", 17 | testonly = True, 18 | srcs = glob(["src/main/**/test/*.kt"]), 19 | visibility = ["//:__subpackages__"], 20 | deps = [ 21 | "//model", 22 | "@maven//com/fasterxml/jackson/core:jackson-annotations", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /model/README.md: -------------------------------------------------------------------------------- 1 | # Spice Model 2 | 3 | The raw data model used by spice. This consists of data classes and very thin conveniences used 4 | for manipulating those, or encoding/validating behavioral semantics of the spice model. 5 | 6 | This should be free of any dependencies other than optional annotations, which are only needed 7 | in apps/libraries which will process the annotations themselves (e.g. serialization formatting) 8 | -------------------------------------------------------------------------------- /model/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | //id 'build-tool-convention' 3 | } 4 | apply { plugin "kotlin" } 5 | 6 | dependencies { 7 | implementation(libs.jackson.annotations) 8 | 9 | testImplementation(libs.junit.api) 10 | testImplementation(libs.google.truth) 11 | testRuntimeOnly(libs.junit.engine) 12 | } 13 | -------------------------------------------------------------------------------- /model/module.spice.yml: -------------------------------------------------------------------------------- 1 | # For illustration - may not be correct (yet) until we dogfood 2 | name: model 3 | tools: [ kotlin ] 4 | variants: 5 | main: 6 | deps: 7 | - maven://com.fasterxml.jackson.core:jackson-annotations 8 | 9 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/builders/DeclarationScope.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.builders 2 | 3 | /** DeclarationScope for setting and properties in fluent builders. */ 4 | class DeclarationScope(private val setProperty: (Pair) -> Unit) { 5 | fun set(vararg kv: Pair) { 6 | kv.forEach(setProperty) 7 | } 8 | 9 | fun define(k: K) { 10 | set(k to null) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/builders/ExternalWorkspaceDocumentBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.builders 2 | 3 | import com.squareup.spice.collections.DeterministicMap 4 | import com.squareup.spice.collections.emptyDeterministicMap 5 | import com.squareup.spice.collections.toDeterministicMap 6 | import com.squareup.spice.model.ExternalNodeConfiguration 7 | import com.squareup.spice.model.ExternalWorkspaceDocument 8 | import java.util.SortedMap 9 | import java.util.TreeMap 10 | 11 | /** 12 | * ExternalWorkspaceDocumentBuilder provides a fluent typesafe builder for external workspaces. 13 | * 14 | * Usage: 15 | * 16 | * val documents = mutableListOf() 17 | * ExternalWorkspaceDocumentBuilder.Scope{ documents.add(it)}.apply { 18 | * workspace("bobMerill").ofType("songwriter") { 19 | * artifacts { 20 | * "hey.mambo" { 21 | * include = listOf("carla.boni") 22 | * exclude = listOf("dean.martin") 23 | * } 24 | * } 25 | * } 26 | * } 27 | * 28 | * assertThat(documents).containsExactly( 29 | * ExternalWorkspaceDocument( 30 | * name = "bobMerill", 31 | * type = "songwriter", 32 | * artifacts = mapOf( 33 | * "hey.mambo" to ExternalNodeConfiguration( 34 | * include = listOf("carla.boni"), 35 | * exclude = listOf("dean.martin") 36 | * ) 37 | * ) 38 | * ) 39 | */ 40 | class ExternalWorkspaceDocumentBuilder(val name: String, private val type: String) { 41 | class Scope(val accept: (ExternalWorkspaceDocument) -> Unit) { 42 | 43 | fun workspace(name: String) = Named(name) 44 | 45 | inner class Named(val name: String) { 46 | fun ofType(type: String, config: ExternalWorkspaceDocumentBuilder.() -> Unit) { 47 | accept(ExternalWorkspaceDocumentBuilder(name, type).apply(config).build()) 48 | } 49 | } 50 | } 51 | 52 | private val properties: SortedMap> = TreeMap() 53 | 54 | private val artifacts: SortedMap = TreeMap() 55 | 56 | fun artifacts(config: ExternalNodeConfigurationBuilder.Scope.() -> Unit) { 57 | ExternalNodeConfigurationBuilder.Scope { n, c -> 58 | artifacts[n] = c 59 | }.apply(config) 60 | } 61 | 62 | fun build() = ExternalWorkspaceDocument( 63 | name, 64 | type, 65 | properties.toDeterministicMap(), 66 | artifacts.toDeterministicMap() 67 | ) 68 | 69 | class ExternalNodeConfigurationBuilder { 70 | class Scope(val accept: (String, ExternalNodeConfiguration?) -> Unit) { 71 | operator fun String.invoke(config: ExternalNodeConfigurationBuilder.() -> Unit) { 72 | accept(this, ExternalNodeConfigurationBuilder().apply(config).build()) 73 | } 74 | } 75 | 76 | var exclude: List = listOf() 77 | var include: List = listOf() 78 | var deps: List = listOf() 79 | private var hashes: DeterministicMap? = emptyDeterministicMap() 80 | 81 | fun build() = when { 82 | exclude.isNotEmpty() || 83 | include.isNotEmpty() || 84 | deps.isNotEmpty() || 85 | (hashes?.isNotEmpty() ?: false) 86 | -> ExternalNodeConfiguration(exclude, include, deps, hashes) 87 | else -> null 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/builders/ModuleDocumentBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.builders 2 | 3 | import com.squareup.spice.collections.DeterministicMap 4 | import com.squareup.spice.collections.MutableDeterministicMap 5 | import com.squareup.spice.collections.MutableDeterministicSet 6 | import com.squareup.spice.collections.mutableDeterministicMapOf 7 | import com.squareup.spice.collections.mutableDeterministicSetOf 8 | import com.squareup.spice.collections.toDeterministicSet 9 | import com.squareup.spice.model.Dependency 10 | import com.squareup.spice.model.ModuleDocument 11 | import com.squareup.spice.model.TestConfiguration 12 | import com.squareup.spice.model.ToolDefinition 13 | import com.squareup.spice.model.VariantConfiguration 14 | 15 | /** ModuleDocumentBuilder defines a fluent interface for creating ModuleDocument instances. 16 | * 17 | * Example: 18 | * 19 | * val bobMerrill = ModuleDocumentBuilder.build("songs") { 20 | * namespace = "music" 21 | * 22 | * tools { 23 | * uses("heyMambo") 24 | * } 25 | * variants { 26 | * "deanMartin" { 27 | * srcs = listOf("src/dean") 28 | * tools { 29 | * uses("jazz") 30 | * } 31 | * tests { 32 | * "apricooSoul" { 33 | * srcs = listOf("src/testSoul") 34 | * } 35 | * } 36 | * deps { 37 | * add("@danzon://a.charanga") 38 | * } 39 | * } 40 | * "carlaBoni" { 41 | * srcs = listOf("src/carla") 42 | * tools { 43 | * uses("recordedHit") 44 | * } 45 | * deps { 46 | * add("latin.Symbolics".tag { 47 | * +"transitive" 48 | * }) 49 | * } 50 | * } 51 | * } 52 | * }.build() 53 | * 54 | * assertThat(bobMerill).isEqualTo( 55 | * ModuleDocument( 56 | * name = "songs", 57 | * namespace = "music", 58 | * tools = listOf("heyMambo"), 59 | * variants = mapOf( 60 | * "deanMartin" to VariantConfiguration( 61 | * srcs = listOf("src/dean"), 62 | * tools = listOf("jazz"), 63 | * deps = listOf(Dependency("@danzon://a.charanga")), 64 | * tests = mapOf( 65 | * "apricooSoul" to TestConfiguration( 66 | * srcs = listOf("src/testSoul") 67 | * ) 68 | * ), 69 | * "carlaBoni" to VariantConfiguration( 70 | * srcs = listOf("src/carla"), 71 | * tools = listOf("recordedHit"). 72 | * deps = listOf(Dependency("latin.Symbolics", listOf("transitive")) 73 | * ) 74 | * ) 75 | * ) 76 | * ) 77 | * 78 | * 79 | */ 80 | @SpiceBuilderScope 81 | class ModuleDocumentBuilder(var name: String? = null) { 82 | 83 | companion object { 84 | fun build(name: String?, config: ModuleDocumentBuilder.() -> Unit) = ModuleDocumentBuilder( 85 | name 86 | ).apply(config).build() 87 | } 88 | 89 | var namespace: String? = null 90 | val tools = ToolSet() 91 | var variants = VariantConfigurationMap() 92 | 93 | fun build() = ModuleDocument( 94 | name, 95 | namespace, 96 | tools.toDeterministicSet(), 97 | variants 98 | ) 99 | 100 | @SpiceBuilderScope 101 | class ToolSet(private val tools: MutableDeterministicSet = mutableDeterministicSetOf()) : 102 | Set by tools { 103 | class Scope(val accept: (String) -> Unit) { 104 | fun uses(name: String) { 105 | accept(name) 106 | } 107 | 108 | fun uses(tools: Iterable) { 109 | tools.forEach(accept) 110 | } 111 | 112 | fun uses(tool: ToolDefinition) { 113 | accept(tool.name) 114 | } 115 | } 116 | 117 | operator fun contains(o: ToolDefinition) = contains(o.name) 118 | 119 | operator fun invoke(config: Scope.() -> Unit): ToolSet { 120 | Scope { tools.add(it) }.apply(config) 121 | return this 122 | } 123 | 124 | operator fun invoke(vararg tools: String) { 125 | this.tools.addAll(tools) 126 | } 127 | 128 | override fun toString(): String { 129 | return tools.toString() 130 | } 131 | 132 | fun toDeterministicSet() = tools.asSequence().sorted().toDeterministicSet() 133 | } 134 | 135 | @SpiceBuilderScope 136 | class VariantConfigurationMap( 137 | private val variants: MutableDeterministicMap = 138 | mutableDeterministicMapOf() 139 | ) : 140 | MutableDeterministicMap by variants { 141 | 142 | @SpiceBuilderScope 143 | class Scope(private val accept: (String, VariantConfiguration) -> Unit) { 144 | operator fun String.invoke(config: Builder.() -> Unit) { 145 | accept(this, Builder().apply(config).build()) 146 | } 147 | 148 | fun set(v: Pair) { 149 | accept(v.first, v.second) 150 | } 151 | } 152 | 153 | @SpiceBuilderScope 154 | class Builder { 155 | var srcs: List = listOf() 156 | val tools: ToolSet = ToolSet() 157 | 158 | val deps = DependencyList() 159 | 160 | val tests = TestMap() 161 | 162 | fun build() = 163 | VariantConfiguration(srcs.sorted(), deps.sortedBy { it.target }, tools.sorted(), tests) 164 | } 165 | 166 | operator fun invoke(config: Scope.() -> Unit) { 167 | Scope { n, v -> variants[n] = v }.apply(config) 168 | } 169 | } 170 | 171 | @SpiceBuilderScope 172 | class TestMap( 173 | private val tests: MutableDeterministicMap = 174 | MutableDeterministicMap.ByHash() 175 | ) : DeterministicMap by tests { 176 | class Scope(val accept: (String, TestConfiguration) -> Unit) { 177 | operator fun String.invoke(config: Builder.() -> Unit) { 178 | accept(this, Builder().apply(config).build()) 179 | } 180 | } 181 | 182 | class Builder { 183 | var srcs: List = emptyList() 184 | var tools: ToolSet = ToolSet() 185 | var deps: DependencyList = DependencyList() 186 | 187 | fun build() = TestConfiguration(srcs.sorted(), deps.sortedBy { it.target }, tools.sorted()) 188 | } 189 | 190 | operator fun invoke(config: Scope.() -> Unit) { 191 | Scope { n, c -> tests[n] = c }.apply(config) 192 | } 193 | } 194 | 195 | @SpiceBuilderScope 196 | class DependencyList( 197 | private val deps: MutableList = mutableListOf() 198 | ) : List by deps { 199 | class Scope(val accept: (Dependency) -> Unit) { 200 | fun String.tag(add: TagSet.() -> Unit) = 201 | Dependency(this, TagSet().apply(add).toList()) 202 | 203 | fun add(vararg ds: Dependency) { 204 | ds.forEach(accept) 205 | } 206 | 207 | fun add(vararg t: String) { 208 | t.map { it.tag {} }.forEach(accept) 209 | } 210 | 211 | inner class TagSet(val tags: MutableSet = mutableSetOf()) : Set by tags { 212 | operator fun String.unaryPlus() { 213 | tags.add(this) 214 | } 215 | 216 | operator fun plus(t: String): TagSet { 217 | tags.add(t) 218 | return this 219 | } 220 | 221 | fun toList() = tags.sorted().toList() 222 | } 223 | } 224 | 225 | operator fun invoke(config: Scope.() -> Unit): DependencyList = apply { 226 | Scope { deps.add(it) }.apply(config) 227 | } 228 | } 229 | } 230 | 231 | fun module(config: ModuleDocumentBuilder.() -> Unit): ModuleDocument { 232 | return ModuleDocumentBuilder().apply(config).build() 233 | } 234 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/builders/SpiceBuilderScope.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.builders 2 | 3 | @DslMarker 4 | annotation class SpiceBuilderScope() 5 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/builders/ToolDefinitionSet.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.builders 2 | 3 | import com.squareup.spice.collections.MutableDeterministicMap 4 | import com.squareup.spice.collections.MutableDeterministicSet 5 | import com.squareup.spice.collections.mutableDeterministicMapOf 6 | import com.squareup.spice.collections.mutableDeterministicSetOf 7 | import com.squareup.spice.collections.toDeterministicSet 8 | import com.squareup.spice.model.ToolDefinition 9 | 10 | /** ToolDefinitionList encapsulates building ToolDefinition instances. 11 | * 12 | * Example: 13 | * 14 | * val merrilBob = ToolDefinitionSet() { 15 | * "hey.mambo" { 16 | * not = listOf("tarantella") 17 | * any = listOf("enchilada", "baccala") 18 | * all = listOf("Siciliano") 19 | * properties { set("mambo" to "italiano") } 20 | * } 21 | * } 22 | * 23 | * assertThat(merrilBob).containsExactly( 24 | * ToolDefinition( 25 | * name = "hey.mambo", 26 | * not = listOf("tarantella"), 27 | * any = listOf("enchilada", "baccala"), 28 | * all = listOf("Siciliano"), 29 | * properties = mapOf("mambo" to "italiano") 30 | * ) 31 | * ) 32 | * 33 | * 34 | */ 35 | class ToolDefinitionSet( 36 | private val tools: MutableDeterministicSet = mutableDeterministicSetOf() 37 | ) : 38 | Set by tools { 39 | 40 | companion object { 41 | fun defineTool( 42 | name: String, 43 | definition: Builder.() -> Unit 44 | ) = Builder(name).apply(definition) 45 | .build() 46 | } 47 | 48 | class Scope(val accept: (ToolDefinition) -> Unit) { 49 | operator fun String.invoke(config: Builder.() -> Unit) { 50 | accept(Builder(this).apply(config).build()) 51 | } 52 | } 53 | 54 | class Builder(var name: String) { 55 | var any: List = listOf() 56 | var all: List = listOf() 57 | var not: List = listOf() 58 | val properties: MutableDeterministicMap = mutableDeterministicMapOf() 59 | fun properties(vararg properties: Pair) { 60 | this.properties.putAll(properties) 61 | } 62 | 63 | fun properties(properties: Map) { 64 | this.properties.putAll(properties) 65 | } 66 | 67 | fun properties(declaration: DeclarationScope.() -> Unit) { 68 | DeclarationScope { (k, v) -> properties[k] = v ?: "" }.apply(declaration) 69 | } 70 | 71 | fun build() = ToolDefinition(name, any.sorted(), all.sorted(), not.sorted(), properties) 72 | } 73 | 74 | operator fun invoke(definedTools: Iterable) { 75 | tools.addAll(definedTools) 76 | } 77 | 78 | operator fun invoke(config: Scope.() -> Unit) { 79 | Scope { tools.add(it) }.apply(config) 80 | } 81 | 82 | fun toDeterministicSet() = sortedBy { it.name }.toDeterministicSet() 83 | } 84 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/builders/WorkspaceDocumentBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.builders 2 | 3 | import com.squareup.spice.collections.MutableDeterministicSet 4 | import com.squareup.spice.collections.mutableDeterministicSetOf 5 | import com.squareup.spice.model.ExternalWorkspaceDocument 6 | import com.squareup.spice.model.ModuleDocument 7 | import com.squareup.spice.model.WorkspaceDocument 8 | 9 | /** WorkspaceDocumentBuilder defines a simplified api for assembling WorkspaceDocument instances. 10 | * 11 | * Usage: see [ToolDefinitionSet], [ExternalWorkspaceDocumentBuilder], [ModuleDocumentBuilder]. 12 | */ 13 | @SpiceBuilderScope 14 | class WorkspaceDocumentBuilder(val name: String) { 15 | val tools = ToolDefinitionSet() 16 | 17 | val external: MutableDeterministicSet = mutableDeterministicSetOf() 18 | 19 | fun external(config: ExternalWorkspaceDocumentBuilder.Scope.() -> Unit) { 20 | ExternalWorkspaceDocumentBuilder.Scope { external.add(it) }.apply(config) 21 | } 22 | 23 | var definitions: ModuleDocument = ModuleDocument() 24 | fun definitions(config: ModuleDocumentBuilder.() -> Unit) { 25 | definitions = ModuleDocumentBuilder().apply(config).build() 26 | } 27 | 28 | fun build(config: WorkspaceDocumentBuilder.() -> Unit) = apply(config).build() 29 | 30 | fun build() = WorkspaceDocument( 31 | name, tools.sortedBy { it.name }, 32 | external.sortedBy { it.name }.toList(), 33 | definitions 34 | ) 35 | } 36 | 37 | fun workspaceDocument( 38 | name: String, 39 | invokable: WorkspaceDocumentBuilder.() -> Unit 40 | ): WorkspaceDocument { 41 | val builder = WorkspaceDocumentBuilder(name) 42 | invokable.invoke(builder) 43 | return builder.build() 44 | } 45 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/collections/DeterministicMap.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.collections 2 | 3 | import java.io.Serializable 4 | 5 | /** MutableDeterministicMap defines a mutable map that retains iteration order. */ 6 | interface MutableDeterministicMap : MutableMap, DeterministicMap { 7 | class ByHash(private val backing: LinkedHashMap = LinkedHashMap()) : 8 | Serializable, 9 | MutableDeterministicMap, 10 | MutableMap by backing { 11 | override fun toString(): String { 12 | return backing.toString() 13 | } 14 | 15 | override fun hashCode(): Int { 16 | return backing.hashCode() 17 | } 18 | 19 | override fun equals(other: Any?): Boolean = when (other) { 20 | is ByHash<*, *> -> backing == other.backing 21 | else -> backing == other 22 | } 23 | } 24 | } 25 | 26 | /** DeterministicMap defines a map that retains iteration order. */ 27 | interface DeterministicMap : Map { 28 | override fun equals(other: Any?): Boolean 29 | override fun toString(): String 30 | override fun hashCode(): Int 31 | } 32 | 33 | inline fun emptyDeterministicMap(): DeterministicMap { 34 | val instance by lazy> { deterministicMapOf() } 35 | return instance 36 | } 37 | 38 | inline fun mutableDeterministicMapOf(vararg e: Pair) = 39 | sequenceOf(*e).toMap(MutableDeterministicMap.ByHash()) 40 | 41 | inline fun deterministicMapOf(vararg e: Pair) = 42 | sequenceOf(*e).toDeterministicMap() 43 | 44 | inline fun Map.toDeterministicMap(): DeterministicMap = 45 | when (this) { 46 | is DeterministicMap -> this 47 | is LinkedHashMap -> MutableDeterministicMap.ByHash(this) 48 | else -> MutableDeterministicMap.ByHash().also { it.putAll(this) } 49 | } 50 | 51 | inline fun Iterable>.toDeterministicMap(): DeterministicMap = 52 | asSequence().toDeterministicMap() 53 | 54 | inline fun Sequence>.toDeterministicMap(): DeterministicMap = 55 | toMap(MutableDeterministicMap.ByHash()) 56 | 57 | inline fun Iterable.associateInOrder( 58 | transform: (T) -> Pair 59 | ): DeterministicMap = 60 | map(transform).toDeterministicMap() 61 | 62 | fun Sequence.foldToDeterministicMap( 63 | fold: MutableMap.(TYPE) -> Unit 64 | ): MutableDeterministicMap { 65 | return fold(MutableDeterministicMap.ByHash()) { acc, t -> 66 | acc.apply { fold(t) } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/collections/DeterministicSet.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.collections 2 | 3 | import java.io.Serializable 4 | 5 | /** MutableDeterministicSet is a MutableSet that retains an consistent iteration order. */ 6 | interface MutableDeterministicSet : MutableSet, DeterministicSet { 7 | class ByHash(private val backing: LinkedHashSet = linkedSetOf()) : 8 | MutableDeterministicSet, 9 | Serializable, 10 | MutableSet by backing { 11 | override fun toString(): String = backing.toString() 12 | override fun hashCode(): Int = backing.hashCode() 13 | override fun equals(other: Any?): Boolean = when (other) { 14 | is ByHash<*> -> backing == other.backing 15 | else -> backing == other 16 | } 17 | } 18 | } 19 | 20 | /** DeterministicSet is a Set that retains an consistent iteration order. */ 21 | interface DeterministicSet : Set 22 | 23 | inline fun emptyDeterministicSet(): DeterministicSet { 24 | val instance by lazy { 25 | deterministicSetOf() 26 | } 27 | return instance 28 | } 29 | 30 | inline fun deterministicSetOf(vararg v: T): DeterministicSet = 31 | mutableDeterministicSetOf(*v) 32 | 33 | inline fun mutableDeterministicSetOf(vararg v: T): MutableDeterministicSet = 34 | MutableDeterministicSet.ByHash(linkedSetOf(*v)) 35 | 36 | inline fun Iterable?.toDeterministicSet(): DeterministicSet = when (this) { 37 | null -> MutableDeterministicSet.ByHash() 38 | is DeterministicSet -> this 39 | is LinkedHashSet -> MutableDeterministicSet.ByHash(this) 40 | else -> MutableDeterministicSet.ByHash().also { it.addAll(this) } 41 | } 42 | 43 | inline fun Sequence?.toDeterministicSet(): DeterministicSet = when (this) { 44 | null -> MutableDeterministicSet.ByHash() 45 | else -> asIterable().toDeterministicSet() 46 | } 47 | 48 | inline operator fun DeterministicSet.plus( 49 | other: DeterministicSet 50 | ): DeterministicSet = (this as Set + other).toDeterministicSet() 51 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/Edge.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | /** 4 | * A relationship (with metadata) to a target [GraphNode]. These are returned by [Slice.dependenciesOf] 5 | * and represent a dependency relationship between two nodes. 6 | * 7 | * The [tags] are stored metadata which don't affect Spice's understanding of the graph, but may 8 | * be understood by tools to imply something about the semantics of these relationships. [tags] are 9 | * not part of the key - that is, there aren't two different a->b edges, with different tag-sets. 10 | * Edges do not hold their source, and are temporary objects in the context of a particular request. 11 | * 12 | * > TODO: Decide if graph edges should be source--{variant}-->target, and cacheable. 13 | */ 14 | interface Edge { 15 | /** The node to which this graph edge points */ 16 | val target: String 17 | val tags: List 18 | } 19 | 20 | /** 21 | * A simple [Edge] implementation. 22 | */ 23 | data class SimpleEdge( 24 | override val target: String, 25 | override val tags: List 26 | ) : Edge 27 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/ExternalWorkspaceDocument.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.squareup.spice.collections.DeterministicMap 4 | import com.squareup.spice.collections.emptyDeterministicMap 5 | 6 | /** 7 | * Defines the core contract of an external workspace. External workspaces provide non-local 8 | * nodes in the graph using a URI addressing scheme ://
. The specific
9 | * is implementation dependant. Calling code that obtains external workspaces should cast to the 10 | * appropriate type if it needs to do something fancy. 11 | */ 12 | data class ExternalWorkspaceDocument( 13 | /** 14 | * The name by which this workspace will be referenced in the global address space. 15 | * addresses of the form ://: will reference artifacts specified 16 | * in this workspace. 17 | */ 18 | val name: String, 19 | 20 | /** 21 | * A string identifier to help consuming software understand how to interpret this external 22 | * workspace. (e.g. "maven", would signal the software to interpret this workspace as a maven 23 | * artifact "universe" and handle any resolution that might be necessary to provide a fully 24 | * articulated graph. 25 | */ 26 | val type: String, 27 | 28 | /** 29 | * Metadata about this workspace that can be consumed by tools that understand it's [type]. 30 | * 31 | * For instance, maven external workspaces may interpret a "repositories" property which contains 32 | * the information needed to set up resolution from maven repos. 33 | */ 34 | // TODO: See if we need a more complex Property object that can contain scalar/array/map data. 35 | val properties: DeterministicMap> = emptyDeterministicMap(), 36 | 37 | /** 38 | * A map of artifacts to configuration. These artifact labels are interpreted by software that 39 | * knows about the external workspace [type]. For instance, a maven based external workspace would 40 | * interpret these artifacts as group_id:artifact_id:version tuples. 41 | */ 42 | val artifacts: DeterministicMap = emptyDeterministicMap() 43 | ) 44 | 45 | /** 46 | * Configuration elements for artifacts in external workspaces. These are limited to deps-surgery 47 | * primitives and artifact hashes (if used). 48 | * 49 | * [include] and [exclude] can be used together, but are incompatible with [deps]. 50 | */ 51 | data class ExternalNodeConfiguration( 52 | val exclude: List = listOf(), 53 | val include: List = listOf(), 54 | val deps: List = listOf(), 55 | val hashes: DeterministicMap? = emptyDeterministicMap() 56 | ) 57 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/GradleSerializable.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import java.io.IOException 4 | import java.io.ObjectOutputStream 5 | import java.io.Serializable 6 | 7 | /** 8 | * GradleSerializable is a marker interface to satisfy the requirements of @Input annotations. 9 | * 10 | * While Spice does not have any planned uses of serialization, Gradle appears to. Implementors of 11 | * this interface should follow the guidance in the Serializable and implement a 12 | * @JvmStatic serialVersionUID long. 13 | */ 14 | interface GradleSerializable : Serializable { 15 | @Throws(IOException::class) 16 | fun writeObject(stream: ObjectOutputStream) { 17 | stream.defaultWriteObject() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/Model.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package c.s.spice.model { 4 | 5 | interface Slice { 6 | + variant: String 7 | } 8 | 9 | interface Workspace 10 | 11 | together { 12 | interface Node { 13 | + address: String 14 | } 15 | 16 | interface InternalNode 17 | 18 | class ExternalNode { 19 | } 20 | } 21 | 22 | class ModuleNode { 23 | + val module: ModuleDocument 24 | + val path: List 25 | } 26 | 27 | class TestNode { 28 | + val config: TestConfiguration 29 | + val variant: String 30 | } 31 | 32 | class VariantConfiguration { 33 | + val srcs: List 34 | + val tools: List 35 | + val deps: List 36 | } 37 | 38 | class TestConfiguration { 39 | + val srcs: List 40 | + val tools: List 41 | + val deps: List 42 | } 43 | 44 | class Dependency { 45 | + val target: String 46 | + val tags: List 47 | } 48 | 49 | Node <|-right- ExternalNode 50 | Node <|-- InternalNode 51 | InternalNode <|-- ModuleNode 52 | InternalNode <|-- TestNode 53 | 54 | ModuleNode "1" *-- "*" VariantConfiguration 55 | VariantConfiguration "1" *-right- "*" TestNode 56 | TestNode "1" *-right- "1" TestConfiguration 57 | VariantConfiguration "1" o-- "*" Dependency 58 | TestConfiguration "1" o-- "*" Dependency 59 | 60 | Workspace "1" --* Slice 61 | Slice "1" *-- InternalNode 62 | Workspace "1" --* ExternalNode 63 | 64 | } 65 | 66 | @enduml 67 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/ModuleDocument.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator 4 | import com.squareup.spice.collections.DeterministicMap 5 | import com.squareup.spice.collections.DeterministicSet 6 | import com.squareup.spice.collections.emptyDeterministicMap 7 | import com.squareup.spice.collections.emptyDeterministicSet 8 | import com.squareup.spice.collections.mutableDeterministicMapOf 9 | import com.squareup.spice.collections.plus 10 | import com.squareup.spice.collections.toDeterministicMap 11 | 12 | /** A simple interface for merging. */ 13 | interface Mergeable> { 14 | /** 15 | * Merges another object into a copy of this one, with implementers choosing the strategy 16 | * for merging. The object returned may be one of the two objects merged, if the merge would leave 17 | * it unchanged and it is immutable. 18 | */ 19 | fun merge(other: T): T 20 | } 21 | 22 | /** 23 | * Represents a module in the workspace, generally some definitional metadata, as well as a set 24 | * of tools which are applied to this module (by templates/client-apps) and variants. Every module 25 | * must have at least one variant, though it can have many, as defined in the workspace. 26 | * 27 | * A module may have a name and namespace if tools which care about those will be used (say, maven 28 | * artifact publisher, documentation generator, etc.) 29 | * 30 | * Modules parsed from `module.spice.yml` files will be merged with the module defaults specified in 31 | * the [WorkspaceDocument.definitions]. Merge will start with the definitions as the base, and the loaded 32 | * module overriding those defaults with the following heuristic.: 33 | * ``` 34 | * name: # replace 35 | * namespace: # replace 36 | * tools: [ a ] # append 37 | * variants: # merge section 38 | * main: # merge if exists, new if not 39 | * srcs: [ a/path ] # replace (within "main") 40 | * tests: # merge 41 | * unit: # merge if exists, new if not 42 | * srcs: [ b/path ] # replace 43 | * deps: [ a, b ] # append 44 | * ``` 45 | */ 46 | data class ModuleDocument( 47 | /** 48 | * The name of this module. If null, it will be inferred by most tools from the workspace name 49 | * and the local directory name (e.g. "foo-mymodule") 50 | */ 51 | val name: String? = null, 52 | /** 53 | * A namespace, analogous to a gradle group or maven group_id. 54 | */ 55 | val namespace: String? = null, 56 | 57 | /** 58 | * An ordered set of tools to be applied to this module. 59 | */ 60 | val tools: DeterministicSet = emptyDeterministicSet(), 61 | 62 | /** 63 | * An ordered map of variant configurations, keyed by name. 64 | * 65 | * Each module must have at least one variant. 66 | */ 67 | val variants: DeterministicMap = emptyDeterministicMap(), 68 | 69 | /** 70 | * Describes if this is expected to be a complete module or if it requires merging with defaults. 71 | */ 72 | val complete: Boolean = false 73 | ) : Mergeable { 74 | /** 75 | * Merges the current configuration with the given defaults module 76 | * 77 | * TBD - fully document the semantics here, referencing examples in the tests themselves. 78 | */ 79 | fun mergeDefaults(definitions: ModuleDocument): ModuleDocument { 80 | if (complete) throw IllegalStateException("Tried to merge definitions into a complete module.") 81 | return definitions.merge(this) 82 | } 83 | 84 | /** 85 | * Merges a [ModuleDocument] into the receiver, using the receiver as a base (default) and 86 | * merging in the more specific [ModuleDocument] information. 87 | * 88 | * Importantly, [name] and [namespace] are replaced if specified in the incoming object, 89 | * while [tools] are unioned, and [variants] are merged 90 | */ 91 | override fun merge(other: ModuleDocument): ModuleDocument { 92 | return ModuleDocument( 93 | name = other.name ?: name, 94 | namespace = other.namespace ?: namespace, 95 | tools = tools + other.tools, 96 | variants = variants.merge(other.variants), 97 | complete = true 98 | ) 99 | } 100 | } 101 | 102 | /** 103 | * Merges a map of named [Mergeable] types into the receiver, where the receiver contains the 104 | * defaults, and the supplied mergeables override. If the mergeable exists in the defaults, they 105 | * are merged, based on the logic in [Mergeable.merge], otherwise the config supplied is 106 | * used as-is, and added to the map. The result in a union of the two. 107 | */ 108 | internal inline fun > Map.merge( 109 | configs: Map 110 | ): DeterministicMap = 111 | mutableDeterministicMapOf().also { union -> 112 | union.putAll(this) 113 | configs.forEach { (name, mergeable) -> 114 | val default = union[name] 115 | when { 116 | default == null && mergeable == null -> { /* nothing to do */ } 117 | default == null -> union[name] = mergeable!! 118 | mergeable == null -> union[name] = default // redundant, but for completeness 119 | else -> union[name] = default.merge(mergeable) 120 | } 121 | } 122 | }.toDeterministicMap() 123 | 124 | /** 125 | * Configures a variant of a given node in the graph. 126 | * This configuration includes srcs and deps and any tests. 127 | */ 128 | data class VariantConfiguration constructor( 129 | val srcs: List = listOf(), 130 | val deps: List = listOf(), 131 | val tools: List = listOf(), 132 | val tests: DeterministicMap = emptyDeterministicMap() 133 | ) : Mergeable { 134 | 135 | /** 136 | * This is a completely ridiculous construct used to work around Jackson's really weird failures 137 | * in the cases of null handling of lists. 138 | */ 139 | @JsonCreator 140 | constructor( 141 | srcs: List?, 142 | deps: List?, 143 | tools: List?, 144 | tests: DeterministicMap?, 145 | @Suppress("UNUSED_PARAMETER") ignore_this_fake_value: Boolean? 146 | ) : this( 147 | srcs ?: emptyList(), 148 | deps ?: emptyList(), 149 | tools ?: emptyList(), 150 | tests ?: emptyDeterministicMap() 151 | ) 152 | 153 | override fun merge(other: VariantConfiguration): VariantConfiguration { 154 | return VariantConfiguration( 155 | srcs = if (other.srcs.isNullOrEmpty()) this.srcs else other.srcs, 156 | deps = deps + other.deps, 157 | tests = tests.merge(other.tests) 158 | ) 159 | } 160 | } 161 | 162 | /** Holds the configuration information for a test node */ 163 | data class TestConfiguration( 164 | val srcs: List = listOf(), 165 | val deps: List = listOf(), 166 | val tools: List = listOf() 167 | ) : Mergeable { 168 | /** 169 | * Merges a [TestConfiguration] into the receiver, using the receiver as a base (default) and 170 | * merging in the more specific [TestConfiguration]. 171 | * 172 | * Importantly, [srcs] are replaced if specified in the incoming object, while [deps] and [tools] are 173 | * concatenated. 174 | */ 175 | override fun merge(other: TestConfiguration): TestConfiguration { 176 | return TestConfiguration( 177 | srcs = if (other.srcs.isNullOrEmpty()) this.srcs else other.srcs, 178 | deps = this.deps + other.deps, 179 | tools = this.tools + other.tools 180 | ) 181 | } 182 | } 183 | 184 | /** 185 | * Holds a dependency target address and its metadata 186 | */ 187 | data class Dependency( 188 | 189 | /** The target of a dependency, as a string address */ 190 | val target: String, 191 | /** 192 | * String tags which are interpreted by tooling. 193 | * 194 | * Such tags don't have inherent semantic meaning to Spice, but may have meaning to specific 195 | * tools or templates. An example would be using a tag (say, "transitive" or "export_all") to 196 | * singal to a gradle template that this dependency should carry with it all of its transitive 197 | * closure - that is, be an "api dependency" (in AGP terms). General practice is to have a semantic 198 | * default for un-tagged deps interpreted by a tool/template, and then configure where there's a 199 | * semantic different (to reduce noise in the human readable output. 200 | */ 201 | val tags: List = listOf() 202 | ) 203 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/Node.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | /** 4 | * A node in the graph representing a unit of action, which may have dependencies on other nodes. 5 | * It could be a local module, or an external workspace address, or a test-set. It can in some ways 6 | * be looked at as a source for dependencies, though different kinds of nodes may have their own 7 | * metadata. 8 | * 9 | * All nodes are addressable, and are the chief handle the calling code has on a given part of the 10 | * graph. 11 | * 12 | * Node's dependencies will always be in the context of a particular variant slice of the graph. 13 | */ 14 | interface Node { 15 | /** 16 | * The address of this node in the workspace's address-space (e.g. /foo or maven://foo.bar:blah) 17 | * 18 | * Different types of nodes will have different addressing schemes. Notably, external nodes will 19 | * have a uri protocol (e.g. `maven://foo.bar:blah`) which will be a signal to any tooling on 20 | * how the address should be interpreted. 21 | * 22 | * Internal nodes will start with `/` and have the form `/path/to/node:variant:test_target`. 23 | * e.g. `/foo/bar/baz:debug:unit-tests`. A module will have a simple ath such as `/foo/bar`. 24 | * It is generally not referenced with a variant unless in a context where variant-filtering 25 | * is needed, such as a query. 26 | */ 27 | val address: String 28 | 29 | /** 30 | * Represents a local module, addressed from the workspace root. Local modules "contain" 31 | * variants and tests, though the [ModuleNode] does not maintain direct references to its 32 | * variants or tests, these being 33 | */ 34 | // TODO: Express this more as a front-end API for a ModelDocument, such that the ModuleDocument 35 | // can be an implementation detail of ModuleNode. 36 | data class ModuleNode( 37 | override val address: String, 38 | val module: ModuleDocument 39 | ) : Node { 40 | val path: List by lazy { address.trim('/').split("/") } 41 | } 42 | 43 | /** 44 | * Represents a test, which is associated with a module (in the context of a variant). 45 | * 46 | * TestNode addresses must be of the form `/path/to/module:variant:test-set-name`. If 47 | * a test node exists within a workspace, then its "parent" module must also exist. i.e. 48 | * `/path/to/module:main:unit` implies the existence of an obtainable `/path/to/module` 49 | * node. 50 | * 51 | * Tests may (and will) be the target of dependency edges. While tests are typically 52 | * dependent on their module code, that is not assumed, but should be represented in 53 | * dependency edges. 54 | * 55 | * Test nodes are also special in that they may only exist within the context of a 56 | * variant, and it is an error to attempt to resolve a test node against a variant 57 | * in which it does not sit. That is to say, a [Slice] may not resolve a test node 58 | * which is not within its own variant. 59 | */ 60 | data class TestNode( 61 | override val address: String, 62 | val config: TestConfiguration 63 | ) : Node { 64 | /** 65 | * Preferred way to manually construct, to ensure that tests derived from module document 66 | * information are properly addressed. 67 | */ 68 | constructor( 69 | moduleAddress: String, 70 | variantName: String, 71 | testName: String, 72 | config: TestConfiguration 73 | ) : this("$moduleAddress:$variantName:$testName", config) 74 | 75 | val module: String = address.split(":")[0] 76 | val variant: String = address.split(":")[1] 77 | val name: String = address.split(":")[2] 78 | 79 | companion object { 80 | val VALID_ADDRESS_PATTERN = "/[a-zA-Z1-9_/-]*:[a-zA-Z1-9_-]*:[a-zA-Z1-9_-]*".toRegex() 81 | } 82 | } 83 | 84 | /** 85 | * Represents an external node (e.g. a maven artifact). The address should be reified against an 86 | * external workspace, and any metadata for it in that context should be obtained from that 87 | * workspace. 88 | */ 89 | data class ExternalNode( 90 | override val address: String 91 | ) : Node 92 | } 93 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/ToolDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.squareup.spice.collections.DeterministicMap 4 | import com.squareup.spice.collections.emptyDeterministicMap 5 | 6 | /** 7 | * Defines a tool, properties, and its relationship to other tools - namely, which tools it requires (if it does) 8 | * and which tools it is incompatible with. These are used to validate tool application in modules. 9 | * 10 | * Tools themselves have no further definitions, but the semantics of how to apply the tools are 11 | * delegated to templates or client apps. 12 | */ 13 | data class ToolDefinition( 14 | val name: String, 15 | val any: List = listOf(), 16 | val all: List = listOf(), 17 | val not: List = listOf(), 18 | val properties: DeterministicMap = emptyDeterministicMap() 19 | ) 20 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/WholeGraph.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import kotlin.annotation.AnnotationRetention.BINARY 4 | import kotlin.annotation.AnnotationTarget.FUNCTION 5 | import kotlin.annotation.AnnotationTarget.PROPERTY 6 | 7 | /** 8 | * Tags an API as a whole-graph operation (or containing a whole-graph operation implicitly). This 9 | * means that implementations of that API may have to perform expensive preparation in order to be 10 | * ready to return their results, and so be used advisedly in situations of performance bottlenecks. 11 | * 12 | * There is no guarantee that the operation MUST be expensive, merely that implementations may likely 13 | * need to perform expensive operations. The lack of this annotation also does not provide contractual 14 | * guarantees of cheapness. However operations without this in the spice model should be able to 15 | * be implemented to only do incrementally required preparatory work. 16 | * 17 | * > Note: This is a purely documentary annotation, though it could be used for IDE signalling, etc. 18 | */ 19 | @Target(FUNCTION, PROPERTY) 20 | @Retention(BINARY) 21 | annotation class WholeGraph 22 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/Workspace.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.squareup.spice.model.Node.ModuleNode 4 | import com.squareup.spice.model.Node.TestNode 5 | import com.squareup.spice.model.validation.Validator 6 | 7 | /** 8 | * The supergraph of all possible graphs defined in a spice system, this is a key entrypoint, from 9 | * which address lookup can be done, nodes can be fetched, variant [slices][Slice] can be obtained, 10 | * as well as the list of available variants. 11 | * 12 | * Generally graph operations will be done on [Slice] instances, but the whole system, and some 13 | * holistic operations are located on the Workspace. 14 | */ 15 | interface Workspace { 16 | 17 | /** The list of variants registered in this workspace */ 18 | val variants: List 19 | 20 | /** The underlying configuration data structure for this workspace */ 21 | val document: WorkspaceDocument 22 | 23 | /** 24 | * A sequence of all nodes present in this workspace. This can be expensive if the underlying 25 | * implementation needs to load data in order ot fulfill this. 26 | */ 27 | @WholeGraph 28 | val nodes: Sequence 29 | 30 | /** 31 | * For all slices, applies the given validators, in the order given. 32 | * 33 | * Completeness simply means that there are no dangling references (no node could be found for 34 | * the target of an edge). Validations of subgraphs should be done via [Slice.validate] methods. 35 | * 36 | * This can be expensive, if the implementation needs to load a lot of information to have the 37 | * full graph available. 38 | */ 39 | @WholeGraph 40 | fun validate(validators: List) { 41 | variants.forEach { slice(it).validate(validators, *nodes.toList().toTypedArray()) } 42 | } 43 | 44 | /** 45 | * Obtain the node for a given address (load semantics are an implementation responsibility) 46 | * 47 | * This node is not constrained by any variant slice, but can freely be given to [Slice] instances 48 | * from this workspace for graph operations. 49 | * 50 | * Any supported node can be queried, with a few constraints - if a node nested with in a Module 51 | * is queried, the appropriate node may be returned - that node must have a "parent" node in the 52 | * system, such that it could be queried. e.g., if "/foo/bar:main:unit" exists, "/foo/bar" must 53 | * exist as a [ModuleNode]. 54 | */ 55 | fun nodeAt(address: String): Node 56 | 57 | /** 58 | * Returns one or more nodes (if any is loaded) in which the supplied path may be found. Those nodes 59 | * should be contained within one module, and a [FindResult.GeneralFindResult] should only be 60 | * returned if the path is found within a given module, but not within any source sets that would 61 | * attribute it more narrowly. 62 | */ 63 | fun findNode(path: String): Sequence> 64 | 65 | /** 66 | * Returns a [ModuleNode] in which the supplied path may be found (that is, it's a member of that 67 | * module). 68 | */ 69 | fun findModule(path: String): ModuleNode? 70 | 71 | /** 72 | * Obtains a slice of the graph, which is a variant-filtered view. Other than basic module loading 73 | * and managing external workspace interactions, all graph operations occur on Slice instances. 74 | */ 75 | fun slice(variant: String): Slice 76 | } 77 | 78 | /** 79 | * A result for the [Workspace.findModule] function, returning one of several known types of responses 80 | * depending on type. 81 | * 82 | * It is not strictly impossible for a file to exist in a variant node and a test node, or more than 83 | * one variant node or test node, if the source sets are so-specified. It should never be the case 84 | * that a file is returned in a [GeneralFindResult] if it is found in the more narrow 85 | * [VariantFindResult] or [TestFindResult]. 86 | * 87 | * A file should never be found in more than one module. 88 | */ 89 | sealed class FindResult { 90 | abstract val node: T 91 | /** 92 | * A module node result that is constrained to a variant. Files will be in this result if they 93 | * are found within the variant's source-set. A file may be found in multiple []VariantFindResult] 94 | * objects if the same source-set is listed in multiple variants. 95 | */ 96 | data class VariantFindResult( 97 | override val node: ModuleNode, 98 | val variant: String 99 | ) : FindResult() 100 | 101 | /** 102 | * References a [ModuleNode] where a file is not a part of any declared source-set, but exists within the 103 | * overall file structure of the module. Useful for finding common files, build files, and 104 | * identifying the node, where there isn't a variant the file is associated with. 105 | */ 106 | data class GeneralFindResult(override val node: ModuleNode) : FindResult() 107 | 108 | /** 109 | * References a [TestNode] where the file was found in the source set of a test node. 110 | */ 111 | data class TestFindResult(override val node: TestNode) : FindResult() 112 | } 113 | 114 | // TODO: supply an ExternalWorkspace API similar to Slice, for graph operations. 115 | // This can encapsulate any resolution logic needed to let external workspaces participate 116 | // in normal graph operations, either in a shallow (no deps) or deep (resolved deps) mode. 117 | // For now this doc just provides the raw metadata. 118 | 119 | /** 120 | * A variant-constrained view of the workspace, and the locus of all graph operations. 121 | * 122 | * Spice workspaces contain many subgraphs, both local variants and external, graph operations must 123 | * happen in the context of a specific slice. 124 | * 125 | * > Note: this contract assumes enough graph state is loaded by any implementation such that its 126 | * > graph walk operations (dependenciesOf/dependenciesOn) will function. Implementations may 127 | * > choose to load up-front, load dynamically, or fail with an exception if the graph is not 128 | * > sufficiently complete. No slice should ever return a partial or incomplete answer. 129 | */ 130 | interface Slice { 131 | /** The named variant of the whole graph this slice represents */ 132 | val variant: String 133 | 134 | /** The [Workspace] of which this [Slice] is a member. */ 135 | val workspace: Workspace 136 | 137 | /** 138 | * Applies the supplied validators to the supplied nodes, in the order given. 139 | * 140 | * Implementations may choose to load graph state incrementally, or throw an exception on a 141 | * partially loaded workspace 142 | */ 143 | fun validate(validators: List, vararg roots: String): Slice { 144 | return validate(validators, *roots.map { nodeAt(it) }.toTypedArray()) 145 | } 146 | 147 | /** 148 | * Applies the supplied validators to the supplied nodes, in the order given. 149 | * 150 | * Implementations may choose to load graph state incrementally, or throw an exception on a 151 | * partially loaded workspace 152 | */ 153 | fun validate(validators: List, vararg roots: Node): Slice { 154 | validators.forEach { it.validate(workspace, this, *roots) } 155 | return this 156 | } 157 | 158 | /** The same operation as [Workspace.nodeAt], but located on [Slice] for convenience. */ 159 | fun nodeAt(address: String): Node 160 | 161 | /** 162 | * Returns the "edges" of dependency relationships (and their configuration [tags]) having the 163 | * node at the given address as the source. 164 | * 165 | * > Note: Assumes sufficient workspace state is loaded to provide a correct answer. 166 | */ 167 | fun dependenciesOf(address: String): List 168 | 169 | /** 170 | * Returns the "edges" of dependency relationships (and their configuration [tags]) having the 171 | * given node as the source. 172 | * 173 | * > Note: Assumes sufficient workspace state is loaded to provide a correct answer. 174 | */ 175 | fun dependenciesOf(node: Node): List 176 | 177 | /** 178 | * Returns the "edges" of dependency relationships (and their configuration [tags]) having the 179 | * node at the given address as their destination. This is the inverse of dependenciesOf, sometimes 180 | * called reverse-dependencies. 181 | * 182 | * > Note: Assumes sufficient workspace state is loaded to provide a correct answer. 183 | */ 184 | fun dependenciesOn(address: String): List 185 | 186 | /** 187 | * Returns the "edges" of dependency relationships (and their configuration [tags]) having the 188 | * given node as their destination. This is the inverse of dependenciesOf, sometimes called 189 | * reverse-dependencies. 190 | * 191 | * > Note: Assumes sufficient workspace state is loaded to provide a correct answer. 192 | */ 193 | fun dependenciesOn(node: Node): List 194 | } 195 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/WorkspaceDocument.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | 5 | /** 6 | * Represents a spice workspace, the root of a set of modules, and the context in which all such 7 | * modules exist. It can define defaults for modules, as well as define tools applied to modules, 8 | * as well as external workspaces (non-local, imported parts of the graph, such as from maven 9 | * dependencies) 10 | */ 11 | // TODO: Possibly separate this with an interface. 12 | data class WorkspaceDocument( 13 | /** A name for the overall workspace, largely for documentary and tool-reporting concerns. */ 14 | val name: String, 15 | @JsonProperty("declared_tools") 16 | val tools: List = listOf(), 17 | val external: List = listOf(), 18 | val definitions: ModuleDocument 19 | ) 20 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/errors.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | /** An error thrown if an graph fails a validator */ 4 | open class InvalidGraph @JvmOverloads constructor( 5 | reason: String, 6 | cause: Throwable? = null 7 | ) : RuntimeException(reason, cause) 8 | 9 | /** 10 | * An error thrown if a graph contains a dependency cycle (that is, is not a directed-acyclic-graph). 11 | */ 12 | class CyclicReferenceError( 13 | val variant: String, 14 | val root: String, 15 | val path: List 16 | ) : InvalidGraph(message(variant, root, path)) { 17 | companion object { 18 | private fun message(variant: String, root: String, path: List): String { 19 | val fromText = if (root == path.first()) ":" else " from ${path.first()}:" 20 | val pathText = path.joinToString("") { " - $it\n" } 21 | return "Cycle detected in '$variant' at \"$root\"$fromText\n$pathText" 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * An error thrown if graph validation finds dangling references. 28 | */ 29 | class IncompleteGraph(missingRefs: Map>) : InvalidGraph( 30 | "Incomplete graph had dependency targets which could not be found in the graph:\n" + 31 | missingRefs.entries.joinToString("") { (target, rdeps) -> 32 | " - $target (referenced by ${rdeps.joinToString()})\n" 33 | } 34 | ) 35 | 36 | /** 37 | * An error thrown if an address could not be materialized. If there is an underlying cause (say a 38 | * file not found error) it will be carried as the [cause] property. 39 | */ 40 | class NoSuchAddress @JvmOverloads constructor( 41 | val address: String, 42 | cause: Throwable? = null 43 | ) : InvalidGraph("No such address found in graph: $address", cause) 44 | 45 | /** 46 | * An error thrown if an address is unsupported, or invalid in some other way. This can be because 47 | * it is incoherently structured, or if it represents an external workspace node that is not supported 48 | * by this graph implementation. 49 | */ 50 | class InvalidAddress @JvmOverloads constructor( 51 | val address: String, 52 | reason: String? = null 53 | ) : InvalidGraph("Unsupported address: $address ${reason?.let{"($reason)"} ?: ""}") 54 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/traversal/BreadthFirstDependencyVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.traversal 2 | 3 | import com.squareup.spice.model.Node 4 | import com.squareup.spice.model.Slice 5 | 6 | /** 7 | * A visitor which visits each node from the root, according to its dependency relationships. 8 | * That is to say, it follows the [Slice.dependenciesOf] (deps) relationship when selecting new nodes, 9 | * queued up breadth-first. 10 | */ 11 | class BreadthFirstDependencyVisitor( 12 | moreError: (slice: Slice, address: String, error: Throwable) -> Sequence = 13 | { _, _, t -> throw t }, 14 | nodeError: (slice: Slice, address: String, error: Throwable) -> Node? = 15 | { _, _, t -> throw t }, 16 | on: (Node) -> Unit 17 | ) : BreadthFirstNodeVisitor( 18 | more = { slice: Slice, node: Node -> slice.dependenciesOf(node).asSequence() }, 19 | moreError = moreError, 20 | nodeError = nodeError, 21 | on = on 22 | ) 23 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/traversal/BreadthFirstNodeVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.traversal 2 | 3 | import com.squareup.spice.model.Edge 4 | import com.squareup.spice.model.Node 5 | import com.squareup.spice.model.Slice 6 | import java.util.Collections 7 | import java.util.LinkedList 8 | import java.util.concurrent.ConcurrentHashMap 9 | 10 | /** 11 | * A visiter helper supertype which implements a breadth-first traversal of whatever nodes are 12 | * returned from the [more] lambda. This is not a parallel or recursive traversal, instead using a 13 | * simple to-process-queue/seen-set to loop through the nodes. 14 | */ 15 | open class BreadthFirstNodeVisitor( 16 | protected val more: (Slice, Node) -> Sequence, 17 | protected val moreError: (slice: Slice, address: String, error: Throwable) -> Sequence = 18 | { _, _, t -> throw t }, 19 | protected val nodeError: (slice: Slice, address: String, error: Throwable) -> Node? = 20 | { _, _, t -> throw t }, 21 | protected val on: (Node) -> Unit 22 | ) : NodeVisitor { 23 | override fun visit(slice: Slice, vararg roots: Node) { 24 | val rootSet = roots.map { it.address }.toSet() 25 | val seen = Collections.newSetFromMap(ConcurrentHashMap()) 26 | val queue = LinkedList().apply { addAll(rootSet) } 27 | while (queue.isNotEmpty()) { 28 | val current = queue.pollFirst() 29 | if (current in seen) continue 30 | else seen.add(current) 31 | val currentNode = try { 32 | slice.nodeAt(current) // may perform a load 33 | } catch (t: Throwable) { 34 | nodeError(slice, current, t) 35 | } ?: continue 36 | on.invoke(currentNode) 37 | val deps = try { 38 | more.invoke(slice, currentNode).map { it.target } 39 | } catch (t: Throwable) { 40 | moreError.invoke(slice, currentNode.address, t) 41 | } 42 | deps.forEach { queue.addLast(it) } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/traversal/BreadthFirstReverseDependencyVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.traversal 2 | 3 | import com.squareup.spice.model.Node 4 | import com.squareup.spice.model.Slice 5 | 6 | /** 7 | * A visitor which visits each node from the root, according to its dependency relationships. 8 | * That is to say, it follows the [Slice.dependenciesOn] (rdeps) relationship when selecting new nodes, 9 | * queued up breadth-first. 10 | */ 11 | class BreadthFirstReverseDependencyVisitor( 12 | moreError: (slice: Slice, address: String, error: Throwable) -> Sequence = 13 | { _, _, t -> throw t }, 14 | nodeError: (slice: Slice, address: String, error: Throwable) -> Node? = 15 | { _, _, t -> throw t }, 16 | on: (Node) -> Unit 17 | ) : BreadthFirstNodeVisitor( 18 | more = { slice: Slice, node: Node -> slice.dependenciesOn(node).asSequence() }, 19 | moreError = moreError, 20 | nodeError = nodeError, 21 | on = on 22 | ) 23 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/traversal/DepsCollector.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.traversal 2 | 3 | /** 4 | * A [NodeVisitor] which does a breadth-first traversal, gathering the list of dependency 5 | * addresses from the given starting nodes. 6 | * 7 | * This is a stateful object, and so repeated calls to visitor() will result in more addresses 8 | * being added ot the tail of the list - if that is undesirable, create a new one. 9 | */ 10 | class DepsCollector( 11 | private val mutableDeps: MutableList = mutableListOf(), 12 | private val visitor: BreadthFirstDependencyVisitor = 13 | BreadthFirstDependencyVisitor { mutableDeps.add(it.address) } 14 | ) : NodeVisitor by visitor { 15 | val deps: List get() = mutableDeps.toList() 16 | } 17 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/traversal/NodeVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.traversal 2 | 3 | import com.squareup.spice.model.Node 4 | import com.squareup.spice.model.Slice 5 | 6 | /** 7 | * A visitor pattern, to help structure orderly traversals of the graph. Takes a lambda to offer 8 | * more values (e.g. from a dependency relationship), and a lambda to act on the visited node. 9 | * 10 | * The visit method takes a slice of the graph and one or more root nodes. Implementations should 11 | * only rely on the public contract of Slice. 12 | */ 13 | interface NodeVisitor { 14 | fun visit(slice: Slice, vararg roots: Node) 15 | } 16 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/util/collections.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.util 2 | 3 | // TODO: Move to a new util project for common code. 4 | 5 | /** 6 | * Not a true multimap structure, but the typical use-case of a SetMultimap, usable where 7 | * [toMap] might be used, but where duplicate keys result in a set of values. 8 | */ 9 | fun List>.toSetMultimap(): Map> = 10 | linkedMapOf>().also { map -> 11 | this.forEach { (k, v) -> map.getOrPut(k) { linkedSetOf() }.add(v) } 12 | } 13 | 14 | /** 15 | * Inverts a set-multimap so that the values are the keys and the keys are the values 16 | */ 17 | fun Map>.invert(): Map> = 18 | entries.flatMap { (k, v) -> v.map { value -> value to k } }.toSetMultimap() 19 | 20 | /** 21 | * Not a true multimap structure, but the typical use-case of a ListMultimap, usable where 22 | * [toMap] might be used, but where duplicate keys result in a list of values. 23 | */ 24 | fun List>.toListMultimap(): Map> = 25 | linkedMapOf>().also { map -> 26 | this.forEach { (k, v) -> map.getOrPut(k) { mutableListOf() }.add(v) } 27 | } 28 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/validation/DependencyCycleValidator.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.validation 2 | 3 | import com.squareup.spice.collections.MutableDeterministicSet 4 | import com.squareup.spice.collections.mutableDeterministicSetOf 5 | import com.squareup.spice.model.CyclicReferenceError 6 | import com.squareup.spice.model.Node 7 | import com.squareup.spice.model.Slice 8 | import com.squareup.spice.model.Workspace 9 | 10 | /** 11 | * Scans the graph from given roots, determining if a node has been seen on that path of 12 | * the traversal. 13 | */ 14 | object DependencyCycleValidator : Validator { 15 | override fun validate(workspace: Workspace, slice: Slice, vararg roots: Node) { 16 | cycleCheck(slice, *roots) 17 | } 18 | 19 | private fun cycleCheck( 20 | slice: Slice, 21 | vararg nodes: Node, 22 | validated: MutableDeterministicSet = mutableDeterministicSetOf(), 23 | path: MutableDeterministicSet = mutableDeterministicSetOf() 24 | ) { 25 | // This is single-threaded, so a mutable set is used. Switch to copy-on-write if made concurrent 26 | nodes.forEach { node -> 27 | if (node.address in validated) return 28 | if (node.address in path) 29 | throw CyclicReferenceError(slice.variant, node.address, path.toList() + node.address) 30 | path.add(node.address) 31 | slice.dependenciesOf(node).forEach { 32 | cycleCheck(slice, slice.nodeAt(it.target), validated = validated, path = path) 33 | } 34 | path.remove(node.address) 35 | validated.add(node.address) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/validation/GraphCompletenessValidator.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.validation 2 | 3 | import com.squareup.spice.model.IncompleteGraph 4 | import com.squareup.spice.model.NoSuchAddress 5 | import com.squareup.spice.model.Node 6 | import com.squareup.spice.model.Slice 7 | import com.squareup.spice.model.Workspace 8 | import com.squareup.spice.model.traversal.BreadthFirstDependencyVisitor 9 | 10 | /** 11 | * Scans the dependency graph from the supplied root nodes, ensuring every node and it's 12 | * transitive closure can be loaded. 13 | */ 14 | object GraphCompletenessValidator : Validator { 15 | override fun validate(workspace: Workspace, slice: Slice, vararg roots: Node) { 16 | val references: MutableMap> = linkedMapOf() 17 | BreadthFirstDependencyVisitor(nodeError = { errorSlice, address, throwable -> 18 | when (throwable) { 19 | is NoSuchAddress -> { 20 | references[address] = errorSlice.dependenciesOn(address).map { it.target }.toSet() 21 | } 22 | else -> throw throwable 23 | } 24 | null 25 | }) {}.visit(slice, *roots) 26 | if (references.isNotEmpty()) { 27 | throw IncompleteGraph(references) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/validation/TestLeafValidator.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.validation 2 | 3 | import com.squareup.spice.model.InvalidGraph 4 | import com.squareup.spice.model.Node 5 | import com.squareup.spice.model.Node.TestNode 6 | import com.squareup.spice.model.Slice 7 | import com.squareup.spice.model.Workspace 8 | 9 | object TestLeafValidator : Validator { 10 | 11 | override fun validate(workspace: Workspace, slice: Slice, vararg roots: Node) { 12 | val testNodes = workspace.nodes.filterIsInstance() 13 | with(testNodes.filterNot { it.address.matches(TestNode.VALID_ADDRESS_PATTERN) }.toList()) { 14 | if (isNotEmpty()) { 15 | throw InvalidGraph( 16 | "InvalidGraph: Test nodes have invalid addresses:\n${ 17 | joinToString("") { " - ${it.address}\n" } 18 | }" 19 | ) 20 | } 21 | } 22 | 23 | val testDependants = workspace.nodes.filterIsInstance().flatMap { node -> 24 | slice.dependenciesOn(node).asSequence().map { it.target to node.address } 25 | }.toSet() 26 | if (testDependants.isNotEmpty()) { 27 | throw InvalidGraph( 28 | "InvalidGraph: Tests may not be the target of a dependency:\n${ 29 | testDependants.joinToString("") { (src, dest) -> " $src -> $dest" } 30 | }" 31 | ) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/model/validation/Validator.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.validation 2 | 3 | import com.squareup.spice.model.Node 4 | import com.squareup.spice.model.Slice 5 | import com.squareup.spice.model.Workspace 6 | 7 | val STANDARD_VALIDATORS: List = listOf( 8 | DependencyCycleValidator, 9 | GraphCompletenessValidator, 10 | TestLeafValidator 11 | ) 12 | 13 | /** 14 | * A validator which can be applied to slices, validating nodes or graphs of nodes. 15 | */ 16 | interface Validator { 17 | fun validate(workspace: Workspace, slice: Slice, vararg roots: Node) 18 | } 19 | -------------------------------------------------------------------------------- /model/src/main/kotlin/com/squareup/spice/test/FakeWorkspace.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.test 2 | 3 | import com.squareup.spice.model.Edge 4 | import com.squareup.spice.model.FindResult 5 | import com.squareup.spice.model.FindResult.GeneralFindResult 6 | import com.squareup.spice.model.FindResult.TestFindResult 7 | import com.squareup.spice.model.FindResult.VariantFindResult 8 | import com.squareup.spice.model.NoSuchAddress 9 | import com.squareup.spice.model.Node 10 | import com.squareup.spice.model.Node.ExternalNode 11 | import com.squareup.spice.model.Node.ModuleNode 12 | import com.squareup.spice.model.Node.TestNode 13 | import com.squareup.spice.model.SimpleEdge 14 | import com.squareup.spice.model.Slice 15 | import com.squareup.spice.model.Workspace 16 | import com.squareup.spice.model.WorkspaceDocument 17 | import com.squareup.spice.model.util.toSetMultimap 18 | 19 | /** 20 | * A small, in-memory Workspace designed only for testing, with canned data. 21 | * 22 | * While not as feature-full as [com.squareup.spice.serialization.FileWorkspace] it does parse out 23 | * test nodes from module documents in ModuleNodes, etc. 24 | * 25 | * It is not optimized for large scale, nor for multithreaded operation. 26 | */ 27 | class FakeWorkspace private constructor( 28 | override val document: WorkspaceDocument, 29 | private val nodeIndex: Map 30 | ) : Workspace { 31 | constructor( 32 | document: WorkspaceDocument, 33 | vararg nodes: Node 34 | ) : this( 35 | document, 36 | nodes.flatMap { 37 | val nodes = mutableListOf>() 38 | when (it) { 39 | is ModuleNode -> { 40 | val mergedModuleDocument = it.module.mergeDefaults(document.definitions) 41 | nodes.add(it.address to it.copy(it.address, mergedModuleDocument)) 42 | mergedModuleDocument.variants.forEach { (variantName, variantConfig) -> 43 | variantConfig.tests.forEach { (testName, testConfig) -> 44 | with(TestNode(it.address, variantName, testName, testConfig)) { 45 | nodes.add(address to this) 46 | } 47 | } 48 | } 49 | } 50 | else -> nodes.add(it.address to it) 51 | } 52 | nodes 53 | } 54 | .also { 55 | val nodeMultiset = it.toSetMultimap().filterValues { v -> v.size > 1 } 56 | if (nodeMultiset.isNotEmpty()) throw IllegalArgumentException( 57 | "Duplicate addresses added to FakeWorkspace: ${nodeMultiset.keys}" 58 | ) 59 | } 60 | .toMap() 61 | ) 62 | 63 | override val variants: List = document.definitions.variants.keys.toList() 64 | override val nodes get() = nodeIndex.values.asSequence() 65 | private val slices = variants.map { it to FakeSlice(it, this) }.toMap() 66 | private val testIndex: Map> by lazy { 67 | nodes.filterIsInstance().map { it.address.split(":")[0] to it } 68 | .toList() 69 | .toSetMultimap() 70 | } 71 | 72 | override fun nodeAt(address: String) = nodeIndex[address] ?: throw NoSuchAddress(address) 73 | 74 | override fun findModule(path: String) = nodeIndex.values 75 | .filterIsInstance() 76 | .firstOrNull { node -> path.startsWith(node.address) } 77 | 78 | override fun findNode(path: String): Sequence> = nodeIndex.asSequence() 79 | .flatMap { (_, node: Node) -> 80 | when (node) { 81 | is ModuleNode -> { 82 | val nodeRoot = node.address 83 | node.module.variants.flatMap { (name, config) -> 84 | val srcs = if (config.srcs.any { path.startsWith("$nodeRoot/$it") }) { 85 | listOf(VariantFindResult(node, variant = name)) 86 | } else listOf() 87 | val tests = testIndex[node.address]?.flatMap { testNode -> 88 | if (testNode.config.srcs.any { path.startsWith("$nodeRoot/$it") }) { 89 | listOf(TestFindResult(testNode)) 90 | } else listOf() 91 | } ?: listOf() 92 | (srcs + tests).ifEmpty { 93 | if (path.startsWith(nodeRoot)) listOf(GeneralFindResult(node)) 94 | else listOf() 95 | } 96 | }.asSequence>() 97 | } 98 | is TestNode -> sequenceOf() // skip because we're handling test nodes from a different index. 99 | else -> sequenceOf() 100 | } 101 | } 102 | 103 | override fun slice(variant: String) = 104 | slices[variant] ?: throw IllegalArgumentException("No such variant: $variant") 105 | 106 | class FakeSlice( 107 | override val variant: String, 108 | override val workspace: FakeWorkspace 109 | ) : Slice { 110 | val deps: Map> by lazy { 111 | workspace.nodeIndex.map { (addr, node) -> 112 | addr to when (node) { 113 | is ModuleNode -> node.module.variants[variant]!!.deps.map { SimpleEdge(it.target, it.tags) } 114 | is TestNode -> node.config.deps.map { SimpleEdge(it.target, it.tags) } 115 | is ExternalNode -> TODO("External nodes not yet validated") 116 | else -> throw IllegalStateException("Unsupported node type ${node::class.qualifiedName}") 117 | } 118 | }.toMap() 119 | } 120 | 121 | val rdeps: Map> by lazy { 122 | val outer = linkedMapOf>() 123 | workspace.nodeIndex.forEach { (addr, node) -> 124 | when (node) { 125 | is ModuleNode -> node.module.variants[variant]!!.deps.forEach { 126 | outer.getOrPut(it.target) { linkedSetOf() }.add(SimpleEdge(addr, it.tags)) 127 | } 128 | is TestNode -> node.config.deps.forEach { 129 | outer.getOrPut(it.target) { linkedSetOf() }.add(SimpleEdge(addr, it.tags)) 130 | } 131 | is ExternalNode -> TODO("External nodes not yet validated") 132 | else -> throw IllegalStateException("Unsupported node type ${node::class.qualifiedName}") 133 | } 134 | } 135 | outer.entries.map { (k, v) -> k to v.toSet() }.toMap() 136 | } 137 | 138 | override fun nodeAt(address: String) = workspace.nodeAt(address) 139 | 140 | override fun dependenciesOf(address: String) = deps[address] ?: listOf() 141 | 142 | override fun dependenciesOf(node: Node): List = dependenciesOf(node.address) 143 | 144 | override fun dependenciesOn(address: String) = rdeps[address]?.toList() ?: listOf() 145 | 146 | override fun dependenciesOn(node: Node) = dependenciesOn(node.address) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/collections/DeterministicTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.collections 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.jupiter.api.Test 5 | 6 | class DeterministicTest { 7 | @Test 8 | fun equals() { 9 | assertThat( 10 | deterministicMapOf( 11 | "a" to "b", 12 | "c" to "d" 13 | ) 14 | ).isEqualTo(deterministicMapOf("c" to "d", "a" to "b")) 15 | 16 | assertThat(deterministicSetOf("b", "a")).isEqualTo(deterministicSetOf("b", "a")) 17 | } 18 | 19 | @Test fun hash() { 20 | assertThat( 21 | deterministicMapOf( 22 | "a" to "b", 23 | "c" to "d" 24 | ).hashCode() 25 | ).isEqualTo(deterministicMapOf("a" to "b", "c" to "d").hashCode()) 26 | 27 | assertThat(deterministicSetOf("b", "a").hashCode()) 28 | .isEqualTo(deterministicSetOf("b", "a").hashCode()) 29 | } 30 | 31 | @Test fun emptyEquals() { 32 | assertThat(emptyDeterministicMap()).isEqualTo(deterministicMapOf()) 33 | assertThat(emptyDeterministicSet()).isEqualTo(deterministicSetOf()) 34 | } 35 | 36 | @Test fun deterministic() { 37 | assertThat(hashMapOf("z" to "y", "a" to "b", "m" to "n").toList()) 38 | .isNotEqualTo(listOf("z" to "y", "a" to "b", "m" to "n")) 39 | 40 | assertThat(deterministicMapOf("z" to "y", "a" to "b", "m" to "n").toList()) 41 | .isEqualTo(listOf("z" to "y", "a" to "b", "m" to "n")) 42 | 43 | assertThat(deterministicSetOf("z" to "y", "a" to "b", "m" to "n").toList()) 44 | .isEqualTo(listOf("z" to "y", "a" to "b", "m" to "n")) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 2 | load("//tools:junit5.bzl", "kt_jvm_test") 3 | 4 | kt_jvm_test( 5 | name = "FakeWorkspaceTest", 6 | srcs = ["FakeWorkspaceTest.kt"], 7 | friends = ["//model"], 8 | test_class = "com.squareup.spice.model.FakeWorkspaceTest", 9 | deps = [ 10 | "//model:test", 11 | "@maven//com/google/truth", 12 | "@maven//org/junit/jupiter:junit-jupiter-api", 13 | ], 14 | ) 15 | 16 | kt_jvm_test( 17 | name = "MergeTest", 18 | srcs = ["MergeTest.kt"], 19 | friends = ["//model"], 20 | test_class = "com.squareup.spice.model.MergeTest", 21 | deps = [ 22 | "@maven//com/google/truth", 23 | "@maven//org/junit/jupiter:junit-jupiter-api", 24 | ], 25 | ) 26 | 27 | kt_jvm_test( 28 | name = "TestNodeUnitTest", 29 | srcs = ["TestNodeUnitTest.kt"], 30 | friends = ["//model"], 31 | test_class = "com.squareup.spice.model.TestNodeUnitTest", 32 | deps = [ 33 | "//model:test", 34 | "@maven//com/google/truth", 35 | "@maven//org/junit/jupiter:junit-jupiter-api", 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/FakeWorkspaceTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.builders.module 5 | import com.squareup.spice.builders.workspaceDocument 6 | import com.squareup.spice.model.FindResult.GeneralFindResult 7 | import com.squareup.spice.model.FindResult.TestFindResult 8 | import com.squareup.spice.model.FindResult.VariantFindResult 9 | import com.squareup.spice.model.Node.ModuleNode 10 | import com.squareup.spice.model.validation.DependencyCycleValidator 11 | import com.squareup.spice.model.validation.GraphCompletenessValidator 12 | import com.squareup.spice.test.FakeWorkspace 13 | import org.junit.jupiter.api.Assertions.assertThrows 14 | import org.junit.jupiter.api.Test 15 | 16 | class FakeWorkspaceTest { 17 | val wsDoc = workspaceDocument("fake") { 18 | definitions { 19 | namespace = "" 20 | tools { ToolDefinition("kotlin") } 21 | variants { 22 | "main" { 23 | srcs = listOf("src/main/java") 24 | tools { uses("java") } 25 | tests { "unit" { srcs = listOf("src/test/java") } } 26 | } 27 | } 28 | } 29 | } 30 | 31 | @Test fun simple() { 32 | val ws: Workspace = FakeWorkspace( 33 | wsDoc, 34 | ModuleNode("/a", module {}), 35 | ModuleNode("/b", module { variants { "main" { deps { add("/a") } } } }), 36 | ModuleNode("/c", module { variants { "main" { deps { add("/b") } } } }), 37 | ModuleNode("/d", module { variants { "main" { deps { add("/a") } } } }), 38 | ModuleNode("/e", module { variants { "main" { deps { add("/c", "/b") } } } }) 39 | ) 40 | 41 | val slice = ws.slice("main") 42 | assertThat(ws.nodeAt("/b").address).isEqualTo("/b") 43 | assertThat(slice.dependenciesOf("/c").map { it.target }).isEqualTo(listOf("/b")) 44 | assertThat(slice.dependenciesOn("/c").map { it.target }).isEqualTo(listOf("/e")) 45 | ws.validate(listOf(GraphCompletenessValidator, DependencyCycleValidator)) 46 | } 47 | 48 | @Test fun duplicateAddresses() { 49 | val t = assertThrows(IllegalArgumentException::class.java) { 50 | FakeWorkspace( 51 | wsDoc, 52 | ModuleNode("/a", module {}), 53 | ModuleNode("/a", module { variants { "main" { deps { add("/a") } } } }) 54 | ) 55 | } 56 | assertThat(t).hasMessageThat().isEqualTo("Duplicate addresses added to FakeWorkspace: [/a]") 57 | } 58 | 59 | @Test fun lookupModules() { 60 | val ws: Workspace = FakeWorkspace( 61 | wsDoc, 62 | ModuleNode("/a", module {}), 63 | ModuleNode( 64 | "/d", 65 | module { 66 | variants { 67 | "main" { 68 | deps { add("/a") } 69 | tests { "unit" { deps { add("/d") } } } 70 | } 71 | } 72 | } 73 | ) 74 | ) 75 | 76 | ws.validate(listOf(GraphCompletenessValidator, DependencyCycleValidator)) 77 | assertThat(ws.findModule("/q/foo/bar")).isNull() 78 | assertThat(ws.findModule("/a/foo/bar")!!.address).isEqualTo("/a") 79 | assertThat(ws.findModule("/d/foo/bar")!!.address).isEqualTo("/d") 80 | assertThat(ws.findModule("/d/src/main/java/foo/bar")!!.address).isEqualTo("/d") 81 | assertThat(ws.findModule("/d/src/test/java/foo/bar")!!.address).isEqualTo("/d") 82 | } 83 | 84 | @Test fun lookupNodes() { 85 | val ws: Workspace = FakeWorkspace( 86 | wsDoc, 87 | ModuleNode("/a", module {}), 88 | ModuleNode( 89 | "/d", 90 | module { 91 | variants { 92 | "main" { 93 | deps { add("/a") } 94 | tests { "unit" { deps { add("/d") } } } 95 | } 96 | } 97 | } 98 | ) 99 | ) 100 | 101 | ws.validate(listOf(GraphCompletenessValidator, DependencyCycleValidator)) 102 | assertThat(ws.findNode("/q/blah.txt").toList()).isEmpty() 103 | ws.findNode("/d/blah.txt").toList().only().let { actual -> 104 | assertThat(actual).isInstanceOf(GeneralFindResult::class.java) 105 | assertThat(actual.node.address).isEqualTo("/d") 106 | } 107 | ws.findNode("/d/src/main/java/some/file.txt").toList().only().let { actual -> 108 | assertThat(actual).isInstanceOf(VariantFindResult::class.java) 109 | actual as VariantFindResult 110 | assertThat(actual.variant) 111 | assertThat(actual.node.address).isEqualTo("/d") 112 | } 113 | ws.findNode("/d/src/test/java/some/file.txt").toList().only().let { actual -> 114 | assertThat(actual).isInstanceOf(TestFindResult::class.java) 115 | assertThat(actual.node.address).isEqualTo("/d:main:unit") 116 | } 117 | } 118 | 119 | private fun List.only(): T { 120 | if (size != 1) throw AssertionError("List should have had one element: $this") 121 | return first() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/MergeTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.collections.deterministicMapOf 5 | import com.squareup.spice.collections.deterministicSetOf 6 | import org.junit.jupiter.api.Test 7 | 8 | class MergeTest { 9 | 10 | @Test fun merge() { 11 | val definitions = ModuleDocument( 12 | namespace = "some.namespace", 13 | tools = deterministicSetOf("tool1"), 14 | variants = deterministicMapOf( 15 | "debug" to VariantConfiguration( 16 | srcs = listOf("src/main", "src/debug"), 17 | deps = listOf(Dependency("/bar")), // Don't do this for real. Default deps are weird. 18 | tests = deterministicMapOf( 19 | "unit" to TestConfiguration( 20 | listOf("src/test"), 21 | deps = listOf( 22 | Dependency("maven://junit"), 23 | Dependency("maven://com.google.truth") 24 | ) 25 | ) 26 | ) 27 | ) 28 | ) 29 | ) 30 | val module = ModuleDocument( 31 | name = "foo-module", 32 | tools = deterministicSetOf("tool2"), 33 | variants = deterministicMapOf( 34 | "debug" to VariantConfiguration( 35 | deps = listOf(Dependency("/baz")), 36 | tests = deterministicMapOf( 37 | "unit" to TestConfiguration( 38 | listOf("src/test", "src/test3"), 39 | deps = listOf( 40 | Dependency("/baz"), 41 | Dependency("/foo-module") 42 | ) 43 | ), 44 | "unit2" to TestConfiguration( 45 | listOf("src/test2"), 46 | deps = listOf( 47 | Dependency("/baz"), 48 | Dependency("/foo-module") 49 | ) 50 | ) 51 | ) 52 | ) 53 | ) 54 | ) 55 | val merged = module.mergeDefaults(definitions) 56 | with(merged) { 57 | assertThat(name).isEqualTo("foo-module") 58 | assertThat(namespace).isEqualTo("some.namespace") 59 | assertThat(tools).isEqualTo(deterministicSetOf("tool1", "tool2")) 60 | assertThat(variants.keys).isEqualTo(setOf("debug")) 61 | val debug = requireNotNull(variants["debug"]) 62 | assertThat(debug.deps.map { it.target }).containsExactly("/bar", "/baz") 63 | assertThat(debug.tests).hasSize(2) 64 | val unit = requireNotNull(debug.tests["unit"]) 65 | assertThat(unit.srcs).containsExactly("src/test", "src/test3") 66 | assertThat(unit.deps.map { it.target }).containsExactly( 67 | "maven://junit", 68 | "maven://com.google.truth", 69 | "/baz", 70 | "/foo-module" 71 | ) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/TestNodeUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.builders.module 5 | import com.squareup.spice.builders.workspaceDocument 6 | import com.squareup.spice.model.Node.ModuleNode 7 | import com.squareup.spice.model.validation.STANDARD_VALIDATORS 8 | import com.squareup.spice.test.FakeWorkspace 9 | import org.junit.jupiter.api.Assertions.assertThrows 10 | import org.junit.jupiter.api.Test 11 | 12 | class TestNodeUnitTest { 13 | 14 | @Test fun simple() { 15 | val ws: Workspace = FakeWorkspace( 16 | workspaceDocument("fake") { 17 | definitions { 18 | namespace = "" 19 | tools { ToolDefinition("kotlin") } 20 | variants { 21 | "main" { 22 | srcs = listOf("src/main/java") 23 | tools { uses("java") } 24 | tests { "unit" { srcs = listOf("src/test/java") } } 25 | } 26 | } 27 | } 28 | }, 29 | ModuleNode("/a", module {}), 30 | ModuleNode("/b", module { variants { "main" { deps { add("/a") } } } }), 31 | ModuleNode("/c", module { variants { "main" { deps { add("/b") } } } }), 32 | ModuleNode( 33 | "/d", 34 | module { 35 | variants { 36 | "main" { 37 | deps { add("/a") } 38 | tests { "unit" { deps { add("/c") } } } 39 | } 40 | } 41 | } 42 | ), 43 | ModuleNode("/e", module { variants { "main" { deps { add("/c", "/b") } } } }) 44 | ) 45 | val slice = ws.slice("main") 46 | assertThat(ws.nodeAt("/b").address).isEqualTo("/b") 47 | assertThat(slice.dependenciesOf("/c").map { it.target }).containsExactly("/b") 48 | assertThat(slice.dependenciesOn("/c").map { it.target }).containsExactly("/e", "/d:main:unit") 49 | ws.validate(STANDARD_VALIDATORS) 50 | } 51 | 52 | @Test fun dependantsOfTests() { 53 | val ws: Workspace = FakeWorkspace( 54 | workspaceDocument("fake") { 55 | definitions { 56 | namespace = "" 57 | tools { ToolDefinition("kotlin") } 58 | variants { 59 | "main" { 60 | srcs = listOf("src/main/java") 61 | tools { uses("java") } 62 | tests { "unit" { srcs = listOf("src/test/java") } } 63 | } 64 | } 65 | } 66 | }, 67 | ModuleNode( 68 | "/a", 69 | module { 70 | variants { 71 | "main" { 72 | tests { "unit" { deps { add("/a") } } } 73 | } 74 | } 75 | } 76 | ), 77 | ModuleNode( 78 | "/b", 79 | module { 80 | variants { 81 | "main" { 82 | deps { add("/a") } 83 | tests { "unit" { deps { add("/a:main:unit") } } } 84 | } 85 | } 86 | } 87 | ) 88 | ) 89 | val e: InvalidGraph = assertThrows(InvalidGraph::class.java) { ws.validate(STANDARD_VALIDATORS) } 90 | assertThat(e).hasMessageThat().contains("/b:main:unit -> /a:main:unit") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/traversal/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 2 | load("//tools:junit5.bzl", "kt_jvm_test") 3 | 4 | kt_jvm_test( 5 | name = "BreadthFirstDependencyVisitorTest", 6 | srcs = ["BreadthFirstDependencyVisitorTest.kt"], 7 | friends = ["//model"], 8 | test_class = "com.squareup.spice.model.traversal.BreadthFirstDependencyVisitorTest", 9 | deps = [ 10 | "//model:test", 11 | "@maven//com/google/truth", 12 | "@maven//org/junit/jupiter:junit-jupiter-api", 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/traversal/BreadthFirstDependencyVisitorTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.traversal 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.builders.module 5 | import com.squareup.spice.builders.workspaceDocument 6 | import com.squareup.spice.model.Node.ModuleNode 7 | import com.squareup.spice.model.ToolDefinition 8 | import com.squareup.spice.model.Workspace 9 | import com.squareup.spice.test.FakeWorkspace 10 | import org.junit.jupiter.api.Test 11 | 12 | class BreadthFirstDependencyVisitorTest { 13 | 14 | val ws: Workspace = FakeWorkspace( 15 | workspaceDocument("fake") { 16 | definitions { 17 | namespace = "" 18 | tools { ToolDefinition("kotlin") } 19 | variants { 20 | "main" { 21 | srcs = listOf("src/test/java") 22 | tools { uses("java") } 23 | tests { "unit" { srcs = listOf("src/test/java") } } 24 | } 25 | } 26 | } 27 | }, 28 | ModuleNode("/a", module {}), 29 | ModuleNode("/b", module { variants { "main" { deps { add("/a") } } } }), 30 | ModuleNode("/c", module { variants { "main" { deps { add("/a") } } } }), 31 | ModuleNode("/d", module { variants { "main" { deps { add("/b") } } } }), 32 | ModuleNode("/e", module { variants { "main" { deps { add("/c") } } } }), 33 | ModuleNode("/f", module { variants { "main" { deps { add("/e", "/b") } } } }), 34 | ModuleNode("/g", module { variants { "main" { deps { add("/d") } } } }), 35 | ModuleNode("/h", module { variants { "main" { deps { add("/g", "/f") } } } }) 36 | ) 37 | val slice = ws.slice("main") 38 | 39 | @Test fun foo() { 40 | // Force lazy loading for debug purposes. 41 | slice.dependenciesOn(slice.dependenciesOf(ws.nodeAt("/h")).first().target) 42 | 43 | val queue = arrayListOf() 44 | BreadthFirstDependencyVisitor { module -> queue.add(module.address) }.visit(slice, ws.nodeAt("/h")) 45 | // Breadth-first, sorted at each target->set. 46 | // (h) -> (h: f, g) -> (f: b, e) -> (g: d) -> (b: a) -> (e: c) (duplicate occurrences ignored) 47 | // h, f, g, b, e, d, a, c 48 | assertThat(queue.toList()).isEqualTo(listOf("/h", "/f", "/g", "/b", "/e", "/d", "/a", "/c")) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/validation/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 2 | load("//tools:junit5.bzl", "kt_jvm_test") 3 | 4 | kt_jvm_test( 5 | name = "DependencyCycleValidatorTest", 6 | srcs = ["DependencyCycleValidatorTest.kt"], 7 | friends = ["//model"], 8 | test_class = "com.squareup.spice.model.validation.DependencyCycleValidatorTest", 9 | deps = [ 10 | "//model:test", 11 | "@maven//com/google/truth", 12 | "@maven//org/junit/jupiter:junit-jupiter-api", 13 | ], 14 | ) 15 | 16 | kt_jvm_test( 17 | name = "GraphCompletenessValidatorTest", 18 | srcs = ["GraphCompletenessValidatorTest.kt"], 19 | friends = ["//model"], 20 | test_class = "com.squareup.spice.model.validation.GraphCompletenessValidatorTest", 21 | deps = [ 22 | "//model:test", 23 | "@maven//com/google/truth", 24 | "@maven//org/junit/jupiter:junit-jupiter-api", 25 | ], 26 | ) 27 | 28 | kt_jvm_test( 29 | name = "TestLeafValidatorTest", 30 | srcs = ["TestLeafValidatorTest.kt"], 31 | friends = ["//model"], 32 | test_class = "com.squareup.spice.model.validation.TestLeafValidatorTest", 33 | deps = [ 34 | "//model:test", 35 | "@maven//com/google/truth", 36 | "@maven//org/junit/jupiter:junit-jupiter-api", 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/validation/DependencyCycleValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.validation 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.builders.module 5 | import com.squareup.spice.builders.workspaceDocument 6 | import com.squareup.spice.model.CyclicReferenceError 7 | import com.squareup.spice.model.Dependency 8 | import com.squareup.spice.model.Node.ModuleNode 9 | import com.squareup.spice.model.Workspace 10 | import com.squareup.spice.test.FakeWorkspace 11 | import org.junit.jupiter.api.Assertions.assertThrows 12 | import org.junit.jupiter.api.Test 13 | 14 | class DependencyCycleValidatorTest { 15 | @Test fun cycleSelf() { 16 | val e = assertThrows(CyclicReferenceError::class.java) { 17 | workspace().slice("main").validate(listOf(DependencyCycleValidator), "/c") 18 | } 19 | assertThat(e) 20 | .hasMessageThat().contains("Cycle detected in 'main' at \"/c\":") 21 | } 22 | 23 | @Test fun cyclePair() { 24 | val e = assertThrows(CyclicReferenceError::class.java) { 25 | workspace().slice("main").validate(listOf(DependencyCycleValidator), "/a") 26 | } 27 | assertThat(e) 28 | .hasMessageThat().contains("Cycle detected in 'main' at \"/a\":") 29 | } 30 | 31 | @Test fun cycleChain() { 32 | val e = assertThrows(CyclicReferenceError::class.java) { 33 | workspace().slice("main").validate(listOf(DependencyCycleValidator), "/i") 34 | } 35 | assertThat(e) 36 | .hasMessageThat().contains("Cycle detected in 'main' at \"/g\" from /i:") 37 | } 38 | 39 | fun workspace(): Workspace { 40 | return FakeWorkspace( 41 | workspaceDocument("fake") { 42 | definitions { 43 | namespace = "" 44 | variants { "main" {} } 45 | } 46 | }, 47 | // Small cycle 48 | ModuleNode("/a", module { variants { "main" { deps { add(Dependency("/b")) } } } }), 49 | ModuleNode("/b", module { variants { "main" { deps { add(Dependency("/a")) } } } }), 50 | // Self cycle 51 | ModuleNode("/c", module { variants { "main" { deps { add(Dependency("/c")) } } } }), 52 | // Long cycle 53 | ModuleNode("/d", module { variants { "main" { deps { add(Dependency("/g")) } } } }), 54 | ModuleNode("/e", module { variants { "main" { deps { add(Dependency("/d")) } } } }), 55 | ModuleNode("/f", module { variants { "main" { deps { add(Dependency("/e")) } } } }), 56 | ModuleNode("/g", module { variants { "main" { deps { add(Dependency("/f")) } } } }), 57 | ModuleNode("/h", module { variants { "main" { deps { add(Dependency("/g")) } } } }), 58 | ModuleNode("/i", module { variants { "main" { deps { add(Dependency("/h")) } } } }) 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/validation/GraphCompletenessValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.validation 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.builders.module 5 | import com.squareup.spice.builders.workspaceDocument 6 | import com.squareup.spice.model.IncompleteGraph 7 | import com.squareup.spice.model.Node.ModuleNode 8 | import com.squareup.spice.test.FakeWorkspace 9 | import org.junit.jupiter.api.Assertions.assertThrows 10 | import org.junit.jupiter.api.Test 11 | 12 | class GraphCompletenessValidatorTest { 13 | 14 | @Test fun loadAllWithDanglingReferences() { 15 | val ws = workspace() 16 | assertThat(ws.nodes.count()).isEqualTo(3) 17 | val e = assertThrows(IncompleteGraph::class.java) { 18 | ws.validate(listOf(GraphCompletenessValidator)) 19 | } 20 | assertThat(e).hasMessageThat().contains("Incomplete graph") 21 | assertThat(e).hasMessageThat().contains("- /r (referenced by /b)") 22 | assertThat(e).hasMessageThat().contains("- /q (referenced by /a, /b)") 23 | } 24 | 25 | fun workspace(): FakeWorkspace { 26 | return FakeWorkspace( 27 | workspaceDocument("fake") { 28 | definitions { 29 | namespace = "" 30 | variants { "main" {} } 31 | } 32 | }, 33 | ModuleNode("/a", module { variants { "main" { deps { add("/q") } } } }), 34 | ModuleNode("/b", module { variants { "main" { deps { add("/a", "/q", "/r") } } } }), 35 | ModuleNode("/c", module { variants { "main" { deps { add("/c") } } } }) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /model/src/test/kotlin/com/squareup/spice/model/validation/TestLeafValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.squareup.spice.model.validation 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import com.squareup.spice.builders.module 5 | import com.squareup.spice.builders.workspaceDocument 6 | import com.squareup.spice.model.InvalidGraph 7 | import com.squareup.spice.model.Node.ModuleNode 8 | import com.squareup.spice.model.Node.TestNode 9 | import com.squareup.spice.model.TestConfiguration 10 | import com.squareup.spice.test.FakeWorkspace 11 | import org.junit.jupiter.api.Assertions.assertThrows 12 | import org.junit.jupiter.api.Test 13 | 14 | class TestLeafValidatorTest { 15 | val workspaceDocument = workspaceDocument("fake") { 16 | definitions { 17 | namespace = "" 18 | variants { "main" {} } 19 | } 20 | } 21 | 22 | @Test fun invalidAddress() { 23 | val workspace = FakeWorkspace( 24 | workspaceDocument, 25 | ModuleNode("/a", module { variants { "main" { } } }), 26 | ModuleNode( 27 | "/b", 28 | module { 29 | variants { 30 | "main" { 31 | deps { add("/a") } 32 | tests { "foo/bar" { } } 33 | } 34 | } 35 | } 36 | ), 37 | ModuleNode( 38 | "/group1/c", 39 | module { 40 | variants { 41 | "main" { 42 | deps { add("/b") } 43 | tests { "unit" { } } 44 | } 45 | } 46 | } 47 | ), 48 | TestNode("/a:foo/bar:blah", TestConfiguration()) 49 | ) 50 | val e = assertThrows(InvalidGraph::class.java) { workspace.validate(STANDARD_VALIDATORS) } 51 | assertThat(e).hasMessageThat().contains(" - /a:foo/bar:blah") 52 | assertThat(e).hasMessageThat().doesNotContain(" - /a\n") 53 | assertThat(e).hasMessageThat().doesNotContain(" - /group1/c:main:unit") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /query/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/query/BUILD.bazel -------------------------------------------------------------------------------- /query/README.md: -------------------------------------------------------------------------------- 1 | # Spice Query 2 | 3 | A query command-line tool which performs graph search operations of various kinds, including: 4 | 5 | - shortest path 6 | - all paths 7 | - all downstream dependants of this node 8 | - all upstream dependencies of this node -------------------------------------------------------------------------------- /query/build.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/query/build.gradle -------------------------------------------------------------------------------- /query/module.spice.yml: -------------------------------------------------------------------------------- 1 | name: query 2 | tools: [ kotlin ] 3 | variants: 4 | main: 5 | deps: 6 | - /model 7 | - /core 8 | tests: 9 | unit: 10 | srcs: ["src/test"] 11 | deps: 12 | - /query 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | // includeBuild '../../build-logic/build-logic-plugin' 3 | def mavenRepoUrl = hasProperty('mavenRepoUrl') 4 | ? getProperty('mavenRepoUrl') : "https://maven.global.square" 5 | repositories { 6 | maven { 7 | url mavenRepoUrl + '/artifactory/square-public' 8 | } 9 | mavenLocal() 10 | } 11 | } 12 | 13 | enableFeaturePreview('VERSION_CATALOGS') 14 | 15 | def allowMavenLocal = providers.systemProperty('allow-maven-local').orElse('false') 16 | .forUseAtConfigurationTime() 17 | def allowSnapshots = providers.systemProperty('allow-snapshots').orElse('false') 18 | .forUseAtConfigurationTime() 19 | 20 | dependencyResolutionManagement { 21 | versionCatalogs { 22 | libs { 23 | from(files("libs.versions.toml")) 24 | } 25 | } 26 | repositories { 27 | if (allowMavenLocal.get()) { 28 | mavenLocal() 29 | } 30 | maven { 31 | url "$mavenRepoUrl/artifactory/square-public" 32 | } 33 | if (allowSnapshots.get()) { 34 | maven { 35 | url "https://oss.sonatype.org/content/repositories/snapshots" 36 | } 37 | maven { 38 | url "https://s01.oss.sonatype.org/content/repositories/snapshots" 39 | } 40 | maven { 41 | url "$mavenRepoUrl/artifactory/snapshots" 42 | } 43 | } 44 | } 45 | } 46 | 47 | //includeBuild('../../build-logic/conventions') { build -> 48 | // println(build.name) 49 | // build.dependencySubstitution { 50 | // substitute(module('com.squareup.register.conventions:rustler')).using(project(':rustler')) 51 | // substitute(module('com.squareup.register.conventions:id')).using(project(':id')) 52 | // substitute(module('com.squareup.register.conventions:utilities-testing')).using(project(':utilities-testing')) 53 | // } 54 | //} 55 | 56 | rootProject.name = 'spice' 57 | include ':model' 58 | include ':core' 59 | 60 | 61 | -------------------------------------------------------------------------------- /templating/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/templating/BUILD.bazel -------------------------------------------------------------------------------- /templating/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/templating/README.md -------------------------------------------------------------------------------- /templating/module.spice.yml: -------------------------------------------------------------------------------- 1 | name: "spice-templating" 2 | tools: ["kotlin"] 3 | variants: 4 | main: 5 | deps: 6 | - "/model" 7 | - "/core" 8 | 9 | -------------------------------------------------------------------------------- /tools/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/spice/f1b77cdb5578d279e1b942bfe873e5b0105fe477/tools/BUILD.bazel -------------------------------------------------------------------------------- /tools/junit5.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | Provides JUnit5 support for running kotlin tests. 3 | 4 | This is a shim until rules_kotlin supports JUnit 5. 5 | """ 6 | 7 | load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 8 | 9 | def kt_jvm_test(name, test_class, data = [], main_class = "", friends = [], size = "medium", **kwargs): 10 | kt_jvm_library( 11 | name = name + "_kt_lib", 12 | testonly = True, 13 | associates = friends, 14 | **kwargs 15 | ) 16 | 17 | pkg, _, _ = test_class.rpartition(".") 18 | 19 | native.java_test( 20 | name = name, 21 | use_testrunner = False, 22 | main_class = "org.junit.platform.console.ConsoleLauncher", 23 | data = data, 24 | size = size, 25 | args = ["--select-package", pkg, "--disable-banner", "--fail-if-no-tests", "--disable-ansi-colors", "--details", "summary"], 26 | runtime_deps = [ 27 | "@maven//org/junit/jupiter:junit-jupiter-api", 28 | "@maven//org/junit/jupiter:junit-jupiter-engine", 29 | "@maven//org/junit/platform:junit-platform-commons", 30 | "@maven//org/junit/platform:junit-platform-console", 31 | "@maven//org/junit/platform:junit-platform-engine", 32 | "@maven//org/junit/platform:junit-platform-launcher", 33 | ":" + name + "_kt_lib", 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /workspace.spice.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Spice - a metadata system for source code 3 | # For illustration - may not be correct (yet) until we dogfood 4 | name: "spice" 5 | 6 | definitions: 7 | namespace: "com.squareup.spice" 8 | 9 | tools: ["kotlin"] 10 | variants: 11 | main: 12 | srcs: ["src/main"] 13 | tests: 14 | unit: 15 | srcs: ["src/test"] 16 | deps: 17 | - "maven://com.google.truth" 18 | - "maven://junit" 19 | 20 | declared_tools: # Tools declared and available to projects in the workspace 21 | - name: kotlin 22 | 23 | external: 24 | - name: maven 25 | type: maven 26 | properties: 27 | repositories: 28 | - central:http://repo1.maven.org 29 | artifacts: 30 | com.google.truth:truth:1.0: 31 | junit:junit:4.13: 32 | 33 | 34 | --------------------------------------------------------------------------------