├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── NOTICE ├── README.md ├── build.sbt ├── laws ├── README.md └── src │ └── main │ └── scala │ ├── Explore.scala │ ├── Flag.scala │ ├── Generator.scala │ ├── Instance.scala │ ├── Instantiator.scala │ ├── Laws.scala │ ├── Numbers.scala │ ├── Ops.scala │ ├── Outcome.scala │ ├── Report.scala │ ├── Runner.scala │ ├── Tags.scala │ ├── Test.scala │ ├── TestInfo.scala │ └── Util.scala ├── project └── build.properties ├── run.sh └── tests └── build.sbt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | java: [8, 11, 17, 21] 13 | scala: [2.13.x] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: coursier/cache-action@v6 20 | - uses: actions/setup-java@v4 21 | with: 22 | distribution: temurin 23 | java-version: ${{matrix.java}} 24 | - uses: sbt/setup-sbt@v1 25 | - name: Test 26 | run: sbt ++${{matrix.scala}} laws/run test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tests/src/ 2 | /tests/*.scala 3 | **/target/ 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | all repositories in these organizations: 2 | 3 | * [scala](https://github.com/scala) 4 | * [scalacenter](https://github.com/scalacenter) 5 | * [lampepfl](https://github.com/lampepfl) 6 | 7 | are covered by the Scala Code of Conduct: https://scala-lang.org/conduct/ 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2014-2025 Lightbend, Inc. dba Akka 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala-collection-laws 2 | ====================== 3 | 4 | Partially automatic generation of tests for the Scala collections library. The 5 | goal is to have a quasi-comprehensive set of collections tests that will catch 6 | regressions in basic functionality in any collection. 7 | 8 | These tests are expressed as a series of "laws": one-liners (mostly) that express 9 | relationships that should be true of some or all collections. 10 | 11 | These laws are written into a series of collection-specific tests by a code generator 12 | (so that implicit resolution and the like is also tested) which can be run with 13 | a method call so the results can be inspected, or can be run from the command-line 14 | with results printed out, or can be invoked by JUnit as a standard test. 15 | 16 | ## Warning--this is a complete rewrite from earlier versions! 17 | 18 | Earlier versions of scala-collection-laws had several weird text-based DSLs. This 19 | has been abandoned in favor of plain Scala code with the `sourcecode` plugin helping 20 | to produce meaningful automatic reports. 21 | 22 | ## Quick start 23 | 24 | Clone the repository, update `build.sbt` with the `scalaVersion` you want to test and 25 | a compatible `sourcecode`, and run 26 | 27 | ```bash 28 | bash run.sh 29 | ``` 30 | 31 | on a Unix-based machine. It should produce output that includes stuff like 32 | 33 | ``` 34 | [info] Running laws.GenerateAll 35 | Generated code for 88 collection/element combinations 36 | 88 updated since last run 37 | ``` 38 | 39 | This is the code generator and law framework in the `laws` folder generating 40 | source files in `tests`. The tests are then compiled, and the output 41 | continues with 42 | 43 | ``` 44 | [info] Compiling 87 Scala sources to /.../laws/tests/target/scala-2.12/classes... 45 | [info] Compiling 1 Scala source to /.../laws/tests/target/scala-2.12/test-classes... 46 | Untested methods in ImmInt_BitSet: 47 | ^ andThen apply compare 48 | compose empty firstKey from 49 | ... 50 | ``` 51 | 52 | and finally ends with 53 | 54 | ``` 55 | Untested methods in Root_Iterator: 56 | hasDefiniteSize isTraversableAgain sliding$default$2 57 | 3 laws never used 58 | #980 val x0 = x; x0.`reduceToSize`(n); x0.`size` == n 59 | #1018 { val x0 = x; x0.`trimEnd`(n); x0 sameAs x.`dropRight`(n) } 60 | #1020 { val x0 = x; x0.`trimStart`(n); x0 sameAs x.`drop`(n) } 61 | [info] Passed: Total 86, Failed 0, Errors 0, Passed 86 62 | ``` 63 | 64 | Congratuations! Your collections were quasi-comprehensively tested. 65 | 66 | ### Local testing quick start 67 | 68 | If you want to test changes to the compiler or standard library, you will presumably want to do something like the following. 69 | 70 | 1. Fork the Scala compiler somewhere on your machine. (You've probably done that already.) 71 | 2. Publish the build locally by running `sbt publishLocal` and note what it's called (e.g. `2.13.0-pre-SNAPSHOT`) 72 | 3. Fork [sourcecode](https://github.com/lihaoyi/sourcecode.git) 73 | 4. Alter `sourcecode`'s `build.sbt` in the following ways: 74 | a. In `baseSettings`, change `scalaVersion` to the locally built compiler 75 | b. In `baseSettings`, you may also wish to change `version` to a custom name for your local build (e.g. I would change `"0.1.5-SNAPSHOT"` to `"0.1.5-local-SNAPSHOT"`) 76 | c. Remove `NativePlatform` and maybe `JSPlatform` from `lazy val sourcecode = crossProject(...)` 77 | d. Remove the `.nativeSettings` and maybe `.jsSettings` from the end of the definition of `lazy val sourcecode` 78 | e. Remove `lazy val native` and maybe `lazy val js` from the end of the file 79 | f. Run `sbt`, enter `project sourcecodeJVM` and then `publishLocal`, noting what it's called 80 | 5. Alter `scala-collection-laws`'s `build.sbt` to request the local versions of the compiler and sourcecode 81 | 82 | Now you can run `bash run.sh` to commence testing. (Note--this only tests the JVM build.) 83 | 84 | Each time you change the library or compiler, you'll need to publish both the compiler and `sourcecode` locally before running collections-laws again. 85 | 86 | ## Common tasks 87 | 88 | ### Catching a regression 89 | 90 | Catching a regression typically does not require deep knowledge of the workings 91 | of scala-collection-laws. Simply change the target version of Scala in the 92 | `build.sbt` file and use `run.sh` again. 93 | 94 | #### Regression class 1: expected relationship fails. 95 | 96 | The test files contain methods that try different combinations of inputs to laws 97 | that require the same inputs. If a single assertion fails in one of these 98 | methods, the testing in that method terminates. 99 | 100 | For example, suppose we expected `Iterator` to obey 101 | 102 | ```scala 103 | x.`hasNext` == x.`drop`(1).hasNext 104 | ``` 105 | 106 | (the backquotes indicate that the collection must have that method for the test to run). 107 | 108 | In addition to the normal report, at the end we would see something like 109 | 110 | ```scala 111 | 1 laws never succeeded on line 112 | #1054 failed 2 times x.`hasNext` == x.`drop`(1).hasNext 113 | ***************************************** 114 | ********** Failures in line 1054 115 | ***************************************** 116 | ****** Test_Root_Iterator_Int ****** 117 | Fail(x.`hasNext` == x.`drop`(1).hasNext 118 | // @ Laws.scala, line 1054 119 | ,Failed(Test_Root_Iterator_Int @ Test_Root_Iterator_Int.scala, line 6 120 | Numbers: 0, 0, -1, 0, 0 121 | Provider: singleton 0 with 122 | Iterator(0) ; len 1 123 | Iterator() ; len 0 124 | Ops 125 | plusOne @ Ops.scala, line 136 126 | bit33 @ Ops.scala, line 146 127 | summation @ Ops.scala, line 156 128 | mod3 @ Ops.scala, line 166 129 | halfEven @ Ops.scala, line 178 130 | ),Some(...),None) 131 | 132 | ****** Test_Root_Iterator_Str ****** 133 | ... 134 | (very similar information repeats regarding `String` element type) 135 | ... 136 | ************************************ 137 | ************************************ 138 | ************** 2 errors 139 | ************************************ 140 | ************************************ 141 | [error] Test laws.Test_Everything_With_JUnit failed: assertion failed 142 | [error] Failed: Total 87, Failed 1, Errors 0, Passed 86 143 | [error] Failed tests: 144 | [error] laws.Test_Everything_With_JUnit 145 | [error] (test:test) sbt.TestsFailedException: Tests unsuccessful 146 | ``` 147 | 148 | Every time the law fails--for every collection that runs it, for every element 149 | type that is used for it--it will appear in the output list. Consequently, 150 | the list can be very long if something major has gone wrong. 151 | 152 | In this case, there is only a single failure and the information provided allows 153 | us to replicate it in the REPL easily. In this case, we can see by inspection 154 | that the law is silly and go to `Laws.scala` line 1054 to fix it. 155 | 156 | In the case of errors that are more mysterious, the test prints out enough information 157 | to manually reconstruct the test case, albeit with a little effort. In this case, one 158 | can manually create the `Numbers`, `Ops`, and `Instance` values like so: 159 | 160 | ``` 161 | tests$ sbt -J-Xmx6G -J-XX:MaxMetaspaceSize=4G 162 | [info] Loading global plugins from /.../.sbt/0.13/plugins 163 | [info] Set current project to collections-laws-tests (in build file:/.../laws/tests/) 164 | > console 165 | [info] Starting scala interpreter... 166 | [info] 167 | Welcome to Scala 2.12.4 (OpenJDK 64-Bit Server VM, Java 1.8.0_151). 168 | Type in expressions for evaluation. Or try :help. 169 | 170 | scala> import laws._ 171 | import laws._ 172 | 173 | scala> val num = Numbers(0, 0, -1, 0, 0) 174 | num: laws.Numbers = Numbers: 0, 0, -1, 0, 0 175 | 176 | scala> val ops = Int 177 | Int IntTest Integer2int InternalError 178 | IntGenerator Integer Integral InterruptedException 179 | 180 | scala> val ops = Str 181 | StrGenerator StrictMath StringContext 182 | StrLongGenerator String StringFormat 183 | StrLongTest StringBuffer StringIndexOutOfBoundsException 184 | StrTest StringBuilder 185 | Stream StringCanBuildFrom 186 | 187 | scala> val ops = Ops(Ops.IntFns.plusOne, Ops.IntToLongs.bit33, Ops.IntOpFns.summation, Ops.IntPreds.mod3, Ops.IntParts.halfEven) 188 | ops: laws.Ops[Int,Long] = 189 | Ops 190 | plusOne @ Ops.scala, line 136 191 | bit33 @ Ops.scala, line 146 192 | summation @ Ops.scala, line 156 193 | mod3 @ Ops.scala, line 166 194 | halfEven @ Ops.scala, line 178 195 | 196 | scala> val inst = InstantiatorsOfInt.Root.iterator().apply(0, Array(0), Array.empty[Int]) 197 | inst: laws.Instance[Int,Iterator[Int]] = 198 | Provider: singleton 0 with 199 | Iterator(0) ; len 1 200 | Iterator() ; len 0 201 | ``` 202 | 203 | and used in a custom source file in the `tests` directory; or the variables used 204 | can be manually filled in and the test pasted in inline (not very interesting in 205 | this case; it's just `def x = Iterator(0)`--make sure to use `def` for collections 206 | with state!). 207 | 208 | In principle one ought to be able to create a new test instance with e.g. 209 | `val test = new Test_Root_Iterator_Int(num, ops, inst, 1054)` inside SBT, but 210 | I've hit classloader issues before, so don't count on this working. 211 | 212 | #### Regression class 2: runtime exception 213 | 214 | The test framework tries to catch runtime exceptions. Generally, as long as the 215 | collection can be built without error, the information will be similar to the 216 | relationship failure class. For instance, if we decide arrays should obey 217 | 218 | ```scala 219 | "xsize > 0 implies x(xsize-1) == x(-1)".law(ARR) 220 | ``` 221 | 222 | (which it would if we had negative indices running from the end of the array), we 223 | get the following: 224 | 225 | ``` 226 | ********** Failures in line 1054 227 | ***************************************** 228 | ****** Test_Mut_Array_Str ****** 229 | Fail(xsize > 0 implies x(xsize-1) == x(-1) 230 | // # ARRAY 231 | // @ Laws.scala, line 1054 232 | , Threw(Test_Mut_Array_Str @ Test_Mut_Array_Str.scala, line 6 233 | Numbers: 0, 0, -1, 0, 0 234 | Provider: singleton with 235 | Array(0) ; len 1 236 | Array() ; len 0 237 | Ops 238 | upper @ Ops.scala, line 141 239 | natural @ Ops.scala, line 151 240 | concat @ Ops.scala, line 161 241 | increasing @ Ops.scala, line 172 242 | oddMirror @ Ops.scala, line 184 243 | , java.lang.ArrayIndexOutOfBoundsException: -1 244 | laws.Test_Mut_Array_Str.$anonfun$runLaw1054$1(Test_Mut_Array_Str.scala:934) 245 | laws.Test$Logic.implies(Test.scala:239) 246 | laws.Test_Mut_Array_Str.runLaw1054(Test_Mut_Array_Str.scala:934) 247 | laws.Test_Mut_Array_Str.$anonfun$lawTable$196(Test_Mut_Array_Str.scala:1135) 248 | laws.Test_Mut_Array_Str.$anonfun$runLaw$1(Test_Mut_Array_Str.scala:1138) 249 | laws.Test_Mut_Array_Str.$anonfun$runLaw$1$adapted(Test_Mut_Array_Str.scala:1138) 250 | scala.Option.map(Option.scala:146) 251 | laws.Test_Mut_Array_Str.runLaw(Test_Mut_Array_Str.scala:1138) 252 | laws.Test.run(Test.scala:123) 253 | laws.Runner.runOne(Runner.scala:44) 254 | laws.Runner.runNums(Runner.scala:62) 255 | laws.Runner.runOps(Runner.scala:94) 256 | laws.Runner.run(Runner.scala:126) 257 | laws.Test_Mut_Array_Str$.run(Test_Mut_Array_Str.scala:1161) 258 | ... 259 | ), (), Some(...), Some(...)} 260 | 261 | ``` 262 | 263 | The test, array and element type, and exception thrown are all visible from inspection, 264 | and replicating the error can be done as in the previous case. 265 | 266 | If, however, there is an exception during creation of the collection, the error 267 | reporting is less complete. For instance, if we change iterator creation from 268 | 269 | ```scala 270 | val iterator = C(a => (new IteratorKnowsSize[A](a)): Iterator[A]) 271 | ``` 272 | 273 | to 274 | 275 | ```scala 276 | val iterator = C(a => { 277 | if (a.length < 10) new IteratorKnowsSize[A](a) 278 | else (for (i <- 0 until a.length*2) yield a(i)).iterator 279 | }) 280 | ``` 281 | 282 | we get the following error: 283 | 284 | ``` 285 | ********** Failures in line 184 286 | ***************************************** 287 | ****** Test_Root_Iterator_Int ****** 288 | Fail(x sameType x.`map`(f) 289 | // # !BITSET_MAP_BREAKS_BOUNDS !SUPER_ITREES !SUPER_MOPENHM 290 | // @ Laws.scala, line 184 291 | , Error(java.lang.ArrayIndexOutOfBoundsException: 12 292 | scala.runtime.ScalaRunTime$.array_apply(ScalaRunTime.scala:55) 293 | laws.InstantiatorsOf$Root$.$anonfun$iterator$2(Instantiator.scala:166) 294 | laws.InstantiatorsOf$Root$.$anonfun$iterator$2$adapted(Instantiator.scala:166) 295 | scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:234) 296 | scala.collection.immutable.Range.foreach(Range.scala:156) 297 | scala.collection.TraversableLike.map(TraversableLike.scala:234) 298 | scala.collection.TraversableLike.map$(TraversableLike.scala:227) 299 | scala.collection.AbstractTraversable.map(Traversable.scala:104) 300 | laws.InstantiatorsOf$Root$.$anonfun$iterator$1(Instantiator.scala:166) 301 | laws.Instance$.from(Instance.scala:95) 302 | laws.Instance$$anon$2.apply(Instance.scala:121) 303 | laws.Instance$$anon$2.apply(Instance.scala:120) 304 | scala.Function3.$anonfun$tupled$1(Function3.scala:35) 305 | scala.Option.map(Option.scala:146) 306 | laws.Exploratory$$anon$2.lookup(Explore.scala:124) 307 | laws.Exploratory.$anonfun$lookup$1(Explore.scala:120) 308 | scala.Option.flatMap(Option.scala:171) 309 | laws.Exploratory.lookup(Explore.scala:120) 310 | laws.Exploratory.lookup$(Explore.scala:120) 311 | laws.Exploratory$$anon$2.lookup(Explore.scala:122) 312 | laws.Runner.run(Runner.scala:123) 313 | laws.Test_Root_Iterator_Int$.run(Test_Root_Iterator_Int.scala:649) 314 | ... 315 | ), (), None, Some(...)} 316 | ``` 317 | 318 | plus similar errors for every other test that `Iterator` has. This situation 319 | could be improved (by better capturing context), but presently, this is all you 320 | have to go on. 321 | 322 | #### Regression class 3: compilation error 323 | 324 | The presumption is that compilation errors will be rare. If a change has caused 325 | a compilation error, you will have to work through SBT's error reporting facilities 326 | and look at the generated code to figure out what went wrong. Since each method 327 | is named after the line that the law came from, you can generally quickly get back 328 | to the offending law. 329 | 330 | In some cases, the code generation itself may be inappropriate. In this case, 331 | the code generation routines in `Generator#code` and/or `GenerateAll` (both in 332 | Generator.scala) should be examined. 333 | 334 | 335 | ### Dealing with collections bugs 336 | 337 | The collections tests will not pass successfully if even a single error is found. 338 | This requires the entire test-suite to avoid any existing bugs in the source code. 339 | 340 | The convention for bugs is to create a new flag (in Flag.scala) with the issue 341 | number, e.g. 342 | 343 | ```scala 344 | val ISSUE_1234 = T 345 | ``` 346 | 347 | Then, you decorate each affected collection with the bug, e.g. by changing 348 | the collection instantiator in `InstantiatorsOf[A]#Imm` from 349 | 350 | ```scala 351 | val seq = C(_.to[collection.immutable.Seq], SEQ) 352 | ``` 353 | 354 | to 355 | 356 | ```scala 357 | val seq = C(_.to[collection.immutable.Seq], SEQ, ISSUE_1234) 358 | ``` 359 | 360 | Finally, you can create positive and negative versions of each test, as necessary. 361 | 362 | For instance, if the functionality is simply broken, you would write a law like 363 | 364 | ```scala 365 | "x.`foo` sameAs x".law(ISSUE_1234.!) 366 | ``` 367 | 368 | On the other hand, if you want to verify the undesired behavior, you can write an 369 | alternate test for the buggy case: 370 | 371 | ```scala 372 | "x.`foo` sameAs x".law(ISSUE_1234.!) 373 | 374 | "x.`foo` sameAs x.`reverse`".law(ISSUE_1234) 375 | ``` 376 | 377 | When the bug is fixed, it should be removed from the test sources. Presently 378 | there is no ability to have a single set of laws that handles multiple versions 379 | of Scala with different sets of bugs, but git branches can be used to maintain 380 | slightly different versions of the source for each Scala version. 381 | 382 | ### Adding or altering laws 383 | 384 | Laws are all specified in `Laws.scala` inside strings which are marked for 385 | code generation by appending `.law`, possibly with arguments. 386 | 387 | Laws should only be run on collections that actually have the appropriate methods. 388 | In order to mark which methods in the code need to be tested, write them inside 389 | backticks, i.e. 390 | 391 | ``` 392 | "x.`tail` == x.`drop`(1)".law 393 | ``` 394 | 395 | (Note that the above is not a valid law, as `tail` and `drop(1)` have different 396 | behavior on empty collections.) 397 | 398 | #### Available variables and types 399 | 400 | Within the code of the law you have access to sixteen variables whose values will 401 | be varied if use is detected, and three or five types: 402 | 403 | | Type Name | Meaning | 404 | |-----------|---------| 405 | | `A` | The type of element stored in the collection under test | 406 | | `B` | The type that `A` is mapped to via the function `g` | 407 | | `CC` | The type of the collection (not parametric!) | 408 | | `K` | Maps only: the type of the keys | 409 | | `V` | Maps only: the type of the values | 410 | 411 | _Note that `CC` is the fully applied type, e.g. `Iterator[Int]`; this is 412 | necessary in case `CC` has no type parameters or has multiple parameters, 413 | e.g. `BitSet` or `Map[String, Long]`._ 414 | 415 | _Note that `A` is identically `(K, V)` for maps._ 416 | 417 | | Variable Name | Expected Values | Meaning | 418 | |---------------|-------------------|---------| 419 | | `a` | an element | Some single instance of the collection's element type | 420 | | `b` | an element | Some single instance of the type that `A` is mapped to via `g` 421 | | `x` | a collection | May be empty or have one or more elements | 422 | | `xsize` | `x.size` | Contains the pre-computed size of `x` | 423 | | `y` | another collection| In general is not the same as `x` (but can be) | 424 | | `ysize` | `y.size` | Precomputed size of `y` | 425 | | `n` | 0 until `x.length`| An index into the `x` collection | 426 | | `f` | `A => A` | Transformation that does not alter element type | 427 | | `g` | `A => B` | Transformation that does alter element type | 428 | | `op` | `(A, A) => A` | An operator that collapses elements | 429 | | `p` | `A => Boolean` | A predicate that tests the elements | 430 | | `pf` | partial function | A transformation defined on only some elements; does not alter element type | 431 | | `n` | in `0 until xsize`| An integer value that could be used as a valid index into `x` | 432 | | `nn` | non-negative | An integer value that is a valid index, but maybe not for `x` | 433 | | `m` | in `0 until ysize`| An integer value that could be used as a valid index into `y` | 434 | | `mm` | non-negative | An integer value that is a valid index, but maybe not for `y`; in general is different than `nn` | 435 | | `r` | integer | An integer value that could be anything | 436 | 437 | You can find the range of variation for these variables in `Numbers.scala` for 438 | `n`, `nn`, `m`, `mm`, and `r`; in `Ops.scala` for `f`, `g`, `op`, `p`, and `pf`; 439 | and in `Instantiator.scala` for `a`, `x`, and `y` (`xsize` and `ysize` are determined 440 | by `x` and `y`). In the last case, look for `possible_a`, `possible_x`, and `possible_y`. 441 | 442 | #### Compilation errors 443 | 444 | If you write a law that doesn't compile, e.g. 445 | 446 | ``` 447 | "x.`tail` = x.`head`".law 448 | ``` 449 | 450 | You will get compile errors after the code generation phase: 451 | 452 | ``` 453 | [info] Compiling 87 Scala sources to /.../laws/tests/target/scala-2.12/classes... 454 | [error] /.../laws/tests/Test_ImmInt_BitSet_Int.scala:619: value tail_= is not a member of scala.collection.immutable.BitSet 455 | [error] x.tail = x.head 456 | [error] ^ 457 | [error] /.../laws/tests/Test_ImmKV_HashMap_Long_String.scala:571: value tail_= is not a member of scala.collection.immutable.HashMap[Long,String] 458 | [error] x.tail = x.head 459 | [error] ^ 460 | ... 461 | ``` 462 | 463 | Usually this is enough to see the problem, but as the source line is also given, 464 | one can inspect the full generated code if the error message is inadequate on its 465 | own. 466 | 467 | #### Restricting the collections to which the law applies 468 | 469 | The primary way to restrict the applicability of laws is to use the flags 470 | in `Flag.scala`. `SEQ`, `SET`, and `MAP` are particularly useful flags, as 471 | these collections have rather different behavior from each other. 472 | 473 | Collections are marked with a subset of flags; in order to run only collections 474 | that are marked, name the flag in the parameters of the `law` method: 475 | 476 | ``` 477 | "x.`reverseIterator` sameAs x.`reverse`".law(SEQ) 478 | ``` 479 | 480 | In contrast, if you want to only consider collections that do *not* have the flag, 481 | append `.!` to the flag name: 482 | 483 | ``` 484 | "x.`+`(a).`contains`(a)".law(MAP.!) 485 | ``` 486 | 487 | Only those collections that have all the positive flags and are missing all the 488 | negative flags will have a test generated for that law. Note that if the conditions 489 | are so restrictive that no collections are tested, the law will be listed as 490 | untested in the output. 491 | 492 | Additional testing is available at runtime. The best way to achieve this is to 493 | use the `Filt` helper object which allows you to query the values of the parameters 494 | before the test is actually run. For instance, 495 | 496 | ``` 497 | "x.`permutations`.size == x.`permutations`.toSet.size".law(Filt.xsize(_ <= 8)) 498 | ``` 499 | 500 | demands that all permutations are distinct, but would take impractically long 501 | for large collections, so it only runs when `xsize` is no more than 8. 502 | 503 | ### Adding or altering collections 504 | 505 | #### Step one: specify how to build the collection 506 | 507 | Collections are specified in `Instantiators.scala`. There are objects for 508 | each concrete element type, and within that objects for each namespace that 509 | contain builders for the collections. These specify how to build the relevant 510 | collection from an array of the appropriate elements. 511 | 512 | There is a fair bit of code duplication, as the type signatures get very hairy 513 | if generalized; this is not clearly the right strategy, however. 514 | 515 | In any case, if you're using an existing namespace, you can simply add the 516 | collection to the appropriate place. For instance, if there were a new `Rope` 517 | collection in `collection.immutable`, one would add to `InstantiatorsOf[A]`, inside 518 | the `Imm` object, the following line: 519 | 520 | ```scala 521 | val rope = C(_.to[collection.immutable.Rope], SEQ) 522 | ``` 523 | 524 | If it is a map, add to `InstantiatorsOfKV[K, V]` instead. If the element type 525 | must be restricted, add to the element-specific objects, e.g. `InstantiatorsOfInt`. 526 | 527 | If the namespace is different, e.g. `collection.concurrent`, a new inner object 528 | should be created much like `Imm`. Cutting and pasting should be sufficient; use 529 | `Mut` as a template if the collection has state that can alter (in which case, code 530 | that references `x` and `y` will get freshly generated copies each time they are 531 | named), or `Imm` if not (in which case the collection is created once and cached). 532 | 533 | For instance, for `collection.concurrent` we would have 534 | 535 | ```scala 536 | object Conc extends Instance.PackagePath { 537 | def nickname = Conc" 538 | def fullyQualified = "scala.collection.concurrent" 539 | def C[CC: TypeTag: Sizable](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[A, CC] = { 540 | val gen = inst.makeWith(ccf, flags: _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 541 | val ans = new Deployed[A, CC]{ 542 | val secretly = gen 543 | var accesses: Int = 0 544 | val name = nm.value.toString 545 | def group = typeTagA.tpe.toString + " in " + nickname 546 | def apply(): Instance.FromArray[A, CC] = { accesses += 1; secretly } 547 | } 548 | registry += ans 549 | ans 550 | } 551 | 552 | val imaginarySkipList = C(_.to[collection.concurrent.ImaginarySkipList], SEQ) 553 | } 554 | ``` 555 | 556 | Note that the `val` name _must_ be the collection name with the first letter lower-cased. 557 | The generator uses this val name to generate the type signature of the class. 558 | 559 | Finally, you must find the `val force` lines for each fully specified instantiator object 560 | and add `:: Conc` to them (to actually add `imaginarySkipList` to the list of collections 561 | for which to generate tests). 562 | 563 | #### Step two: create the test code generator for the collection 564 | 565 | Once the collection has been created, a parallel structure needs to be created in 566 | `Generators.scala` to actually generate code for the class. In the future, perhaps it would 567 | be better to combine these two so one cannot specify an instantiator without a 568 | corresponding generator. 569 | 570 | In any case, simply add the collection to the element-type-specific generator objects, 571 | in the appropriate place that mirrors the path to the instantiator (this is mostly just 572 | convention, but it makes it easier to avoid mistakes). For instance, ropes would be 573 | added to both `AllIntGenerators` and `AllStrGenerators` inside `Imm` as 574 | 575 | ``` 576 | val rope = register(io.Imm)(_.rope()) 577 | ``` 578 | 579 | while for `Conc` one would create a new `object Conc` inside `AllIntGenerators` 580 | and `AllStrGenerators` that looked like 581 | 582 | ``` 583 | object Conc { 584 | val imagnarySkipList = register(io.Conc)(_.imaginarySkipList()) 585 | } 586 | ``` 587 | 588 | and then `:: Conc` would be added to `val force` in both `All___Generators` objects. 589 | 590 | #### Creating a new element type 591 | 592 | Follow the examples of `BitSet` and/or `LongMap`; between them they illustrate 593 | most or all of the issues one must address to get a specific collection type working. 594 | 595 | You will also need to add the new element-type-generator to the `write` method 596 | in `GenerateAll`. 597 | 598 | ## Conclusion 599 | 600 | Thanks for reading! Good luck! 601 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / scalaVersion := "2.13.16" 2 | ThisBuild / scalacOptions ++= Seq("-unchecked", "-feature", "-deprecation") 3 | ThisBuild / libraryDependencies ++= Seq( 4 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 5 | "com.lihaoyi" %% "sourcecode" % "0.4.2" 6 | ) 7 | 8 | val laws = project.settings( 9 | scalacOptions += "-Werror", 10 | scalacOptions += "-Xsource:3-cross", 11 | ) 12 | val tests = project.dependsOn(laws) 13 | -------------------------------------------------------------------------------- /laws/README.md: -------------------------------------------------------------------------------- 1 | # Laws for Collections-Laws 2 | 3 | The code in this project defines the laws that collections must pass. This file describes the logic behind how these laws are organized and generated. 4 | 5 | ## What is a `Law`? 6 | 7 | A `Law` contains a test that should be true for many collections (see `Laws.scala`). At its core it is a piece of code that returns a `Boolean`--typically an equality--that should hold for any valid collection, operation, and/or numeric value passed in to it. 8 | 9 | The code is simply text that will form the body of a `run` method of a generated test (see below for information on what a `Test` is). 10 | 11 | A `Law` is intended to apply broadly but perhaps not universally to all collections. The running of a `Law` is restricted in two ways. 12 | 13 | 1. By availability of method. Within a test, any method that is quoted in back-ticks is checked before generation to ensure that it is actually available on the collection in question. If one or more methods are unavailable, the test is not run. If the test is designed to ensure that a method is available on a collection, then the method should be used unquoted! Note also that extension methods cannot be detected by the run-time reflection used to validate method availability. 14 | 15 | 2. By flags and/or more intricate testing. Each collection type can define a number labels that are instances of the `Tag` class. Each law can specify a subset of tags that must be present, and/or a subset that must not be present, in order for the test to be valid. Additionally, a law can query additional features of the particular test and operation selected, such as whether a binary operation is expected to be associative and symmetric, or not. 16 | 17 | Refinement of laws is therefore a two-step process. When a method is not available, code generation for the combination of that collection and that law will not proceed. Furthermore, tags are a property only of collection type and will prevent code generation. If methods are present and text flags pass, then a test class will be generated. 18 | 19 | In the second set of the process, tests will be run only for those parameters for which all additional tests pass. 20 | 21 | Thus, availability of back-quoted methods and fulfillment of text tags should be used to ensure that a test can compile. (Negative tests are not supported--the point of this is to test laws that specify run-time behavior.) Additional exclusion of inappropriate combinations of particular operations and collections can be achieved by filtering on the `TestInfo` trait before running. 22 | 23 | The individual laws are specified inside the `Laws` object. In addition to extension methods on strings that assist in constructing a `Law` object, `Law` itself allows tags to be added using the `and`, `not`, and `filter` methods (the last of which takes a predicate that checks the `TestInfo` for that test at runtime). 24 | 25 | The `sourcecode` package is used to enable informative error reporting by capturing line numbers and file names of various components of laws, most notably the line where the text is declared in the `Laws` object. 26 | 27 | ### Available variables 28 | 29 | The variables available to code in laws are those in the `Test` class (see below for details about this class). While _all_ methods of `Test` are technically available (including those code-generated in the leaf classes that inherit from `Test` and contain the literal code), the following are intended for use: 30 | 31 | #### Elements and collections 32 | 33 | * `x` is the primary collection being tested. Its length is `xsize`. 34 | * `y` is an auxillary collection of the same type (used for operations like `++`). Its length is `ysize`. 35 | * `a` is an element of the same type as stored in `x` 36 | * `b` is an element of a different type (the same type as produced from `a` by the function `g` - see below) 37 | * `zero` is an element of the type stored in `x` which is a zero with respect to the function `op` - see below); call `hasZero` to see if it exists 38 | 39 | #### Functions, predicates, and binary operations 40 | 41 | * `f` is a function that converts elements of `x` into other elements of the same type 42 | * `g` is a function that converts elements of `x` into elements of some different type (the type of `b`). 43 | * `p` is a predicate that returns true or false for elements of `x` 44 | * `pf` is a partial function that will where defined convert elements of `x` into other elements of the same type 45 | * `op` is a binary operation that will take two elements of `x` and produce one of the same type; if the operation is associative and symmetric (like addition), `isSymOp` will return true 46 | 47 | #### Numbers 48 | 49 | * `n` is a number between 0 and the length of `x` 50 | * `nn` is a non-negative number (may be bigger than `x`) 51 | * `m` is a number between 0 and the length of `y` 52 | * `mm` is another non-negative number (may be bigger than `y`) 53 | * `r` is a number that may be positive or negative 54 | 55 | 56 | ### Available methods 57 | 58 | In addition to the normal methods availble in Scala code, the extension methods provided by `Tests.ComparesTo` and `Tests.Logic` are imported into the scope of the test methods. 59 | 60 | #### Comparison methods 61 | 62 | * `theSameAs` checks whether two things that can at least be implicitly converted to a `TraversableOnce` have the same elements in the same order. For example, `Iterator('c', 'o', 'd') theSameAs "cod"` would return true. 63 | 64 | * `correspondsTo` checks whether the two sides have the same elements as each other, the same number of times; order is not important. For example, `"bass" correspondsTo Array('s', 'b', 's', a')` would return true. 65 | 66 | * `isPartOf` checks whether every element in the left-hand side also exists in the right (with the right having at least as many duplicates if there are duplicates). For example, `Vector('n', 'n', 'm') isPartOf "minnow"` would return true. 67 | 68 | #### Logical methods 69 | 70 | * `implies` is a short-circuiting method that returns false if the left-hand side is true but the right is false; it otherwise returns true (as is the case with logical implication). The left-hand side is evaluated first. 71 | 72 | * `impliedBy` is a short-circuiting method that returns false if the right-hand side is true but the left is false; it otherwise returns true (as is the case with the logical reverse implication). The left-hand side is evaluated first, which makes it potentially have different sid effects than `implies` with the order of arguments flipped. 73 | 74 | ## The fundamental unit of work: `Test` 75 | 76 | A `Test` is a class that runs a single test; it is parameterized by collection type and element type, and takes as arguments a particular instance of the collection to be tested, a particular set of operations to apply (if called for by the test), and a particular set of numeric arguments. 77 | 78 | The leaf `Test` classes are produced via code generation and contain code to run each valid law, as well as to select a single test to run. Because the run methods within a generated class, a number of variables are available for use, including those corresponding to a collection of the appropriate type, an element of the same type, and various numbers that one might use in calling methods of that collection (e.g. `take`). 79 | 80 | The actual instances of tests are produced by code generators that assemble the four components of a test: `Instance`, `Ops`, `Numbers`, and `Law`. 81 | 82 | ### Instance 83 | 84 | ### Ops 85 | 86 | ### Numbers 87 | 88 | ## Generating Test Code with Companions 89 | 90 | ## Running tests with Runner 91 | 92 | ### Generating diversity with `Explore` and `Exploratory` 93 | -------------------------------------------------------------------------------- /laws/src/main/scala/Explore.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | class Explore(possibilities: Array[Int]) { 4 | val N = possibilities.foldLeft(1L)(_ * _) match { 5 | case x if x >= 0 && x < 1024*1024*1024 => x.toInt 6 | case x => throw new IllegalArgumentException(f"Too many options: $x") 7 | } 8 | 9 | val visiting = new collection.mutable.BitSet(N) 10 | val itinerary = collection.mutable.Queue(Array.fill(possibilities.length)(0)) 11 | visiting += 0 12 | 13 | def index(ixs: Array[Int]) = 14 | if (ixs.length != possibilities.length) -1 15 | else { 16 | var n = 0L 17 | var m = 1L 18 | var k = 0 19 | while (k < ixs.length) { 20 | n += ixs(k)*m 21 | m *= possibilities(k) 22 | k += 1 23 | } 24 | assert(n >= 0 && n < N) 25 | n.toInt 26 | } 27 | 28 | def advance(used: Array[Boolean]): Boolean = { 29 | if (used.length != possibilities.length) 30 | throw new IllegalArgumentException(f"Expected ${possibilities.length} entries, got ${used.length}") 31 | 32 | if (itinerary.isEmpty) false 33 | else { 34 | val old = itinerary.dequeue() 35 | var i = 0 36 | var n = 0 37 | while (i < used.length) { if (used(i)) n += 1 ; i += 1 } 38 | if (n > 0) { 39 | val explore = new Array[Array[Int]](2*n) 40 | var j = 0 41 | i = 0 42 | while (i < used.length) { 43 | if (used(i)) { 44 | if (old(i) > 0) { 45 | val another = old.clone 46 | another(i) -= 1 47 | val ix = index(another) 48 | if (!visiting(ix)) { 49 | visiting += ix 50 | explore(j) = another 51 | j += 1 52 | } 53 | } 54 | if (old(i)+1 < possibilities(i)) { 55 | val another = old.clone 56 | another(i) += 1 57 | val ix = index(another) 58 | if (!visiting(ix)) { 59 | visiting += ix 60 | explore(j) = another 61 | j += 1 62 | } 63 | } 64 | } 65 | i += 1 66 | } 67 | if (j > 0) { 68 | if (j == explore.length) itinerary.enqueueAll(explore) 69 | else itinerary.enqueueAll(java.util.Arrays.copyOf(explore, j)) 70 | } 71 | } 72 | itinerary.nonEmpty 73 | } 74 | } 75 | 76 | def current: Option[Array[Int]] = if (itinerary.nonEmpty) Some(itinerary.front) else None 77 | 78 | override def toString = current match { 79 | case Some(xs) => f"Visited ${visiting.size} with ${itinerary.size} pending\n ${xs.map(x => "%2d".format(x)).mkString(" ")}" 80 | case None => f"Visited ${visiting.size} with ${itinerary.size} pending" 81 | } 82 | } 83 | 84 | 85 | /** Indicates that there is some set of parameters to be explored */ 86 | trait Exploratory[A] { self => 87 | def sizes: Array[Int] 88 | 89 | final def explore(): Explore = new Explore(sizes) 90 | 91 | final def completeIterator(): Iterator[A] = { 92 | val e = explore() 93 | val b = Array.fill(sizes.length)(true) 94 | new collection.AbstractIterator[A] { 95 | private[this] var maybeA: Option[A] = None 96 | @annotation.tailrec def hasNext = 97 | maybeA.isDefined || (e.itinerary.nonEmpty && e.advance(b) && { maybeA = e.current.flatMap(self.lookup); hasNext }) 98 | def next() = 99 | if (!hasNext) throw new NoSuchElementException("Empty exploratory iterator") 100 | else { 101 | val ans = maybeA.get 102 | maybeA = None 103 | ans 104 | } 105 | } 106 | } 107 | 108 | protected def validate(ixs: Array[Int]): Boolean = 109 | if (ixs.length != sizes.length) false 110 | else { 111 | var i = 0 112 | while (i < ixs.length) { 113 | if (ixs(i) < 0 || ixs(i) >= sizes(i)) return false 114 | i += 1 115 | } 116 | true 117 | } 118 | 119 | def lookup(ixs: Array[Int]): Option[A] 120 | final def lookup(e: Explore): Option[A] = e.current.flatMap{ ixs => lookup(ixs) } 121 | 122 | def map[B](f: A => B): Exploratory[B] = new Exploratory[B] { 123 | def sizes = self.sizes 124 | def lookup(ixs: Array[Int]) = self.lookup(ixs).map(f) 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /laws/src/main/scala/Flag.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /** A quality that can be attached to a collection or law to categorize its properties. 4 | * Flags are used both for general properties of collections (e.g. is it a sequence) 5 | * and to indicate atypical or buggy behavior for which a test should or should not 6 | * be run. 7 | */ 8 | final class Flag(val disabled: Boolean = false)(implicit val nm: sourcecode.Name) 9 | extends Ordered[Flag] { 10 | override val toString = nm.value.toString 11 | override def compare(that: Flag) = toString.compareTo(that.toString) 12 | override def equals(that: Any) = that match { 13 | case f: Flag => this.toString == that.toString 14 | case _ => false 15 | } 16 | override val hashCode = toString.hashCode 17 | } 18 | object Flag { 19 | def F(implicit nm: sourcecode.Name) = new Flag()(nm) // This sets a flag 20 | def X(implicit nm: sourcecode.Name) = new Flag(true)(nm) // Use this to "comment out" a flag 21 | 22 | // Fundamental properties of sequences 23 | val INT = F // Uses integers as the element type 24 | val MAP = F // Is a map 25 | val SEQ = F // Is a sequence 26 | val SET = F // Is a set 27 | val STR = F // Uses strings as the element type 28 | 29 | // Unusual "collections" that are not expected to behave exactly like others 30 | val ARRAY = F // Is an Array 31 | val STRING = F // Is a String 32 | val STRAW = F // strawman collections (when used together with regular collections) 33 | val ORDERLY = F // Collection is sorted, but can't maintain itself with all operations as it might lose its ordering 34 | val ONCE = F // Collection is consumed on traversal 35 | val INDEF = F // Collection's size is not yet fixed (lazy collections) 36 | val SPECTYPE= F // Collection has constraints on element type, which makes some operations not work 37 | val BITSET = F // Collection is specificially a bitset (mutable or immutable) 38 | val INSORD = F // Collection traverses itself in insertion order even though it's not intrinsically ordered 39 | 40 | // Everything down here is _highly_ dubious behavior but is included to get tests to pass 41 | val PRIORITYQUEUE_IS_SPECIAL = F // Inconsistent behavior regarding what is dequeued (ordered) vs. not 42 | 43 | // Workarounds for identified bugs go here. 44 | val BITSET_MAP_AMBIG = F // Bit maps don't know whether to use StrictOptimized or SortedSet ops for map. 45 | val BITSET_ZIP_AMBIG = F // Same problem with zipping 46 | 47 | // Pure bugs that aren't fixed yet 48 | val LISTBUF_PIP_11438 = F // ListBuffer throws an exception on a no-op patchInPlace 49 | val QUEUE_SLIDE_11440 = F // Queue and ArrayStack will not give you an underfull sliding window (everything else does) 50 | val PQ_MIP_NPE_11439 = F // Priority Queue can just give null when empty! 51 | 52 | // Mysterious bugs that can't easily be replciated 53 | val SORTWITH_INT_CCE = F // Array (but nothing else) gives class cast error in `sortWith` on ints! Can't reproduce in REPL. 54 | } 55 | -------------------------------------------------------------------------------- /laws/src/main/scala/Generator.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /** Generates code for a particular element/collection combination. 4 | * 5 | * To understand what the various methods do, inspect where they are used inside the code generation. 6 | */ 7 | trait Generator[A, B, CC] { 8 | def instanceExplorer(): Exploratory[Instance[A, CC]] 9 | def opsExplorer(): Exploratory[Ops[A, B]] 10 | def autoTags: Set[Flag] 11 | def ccType: String 12 | def eltType: String 13 | def eltCC: String = eltType 14 | def safeElt: String = eltType.collect{ 15 | case ',' => '_' 16 | case c if c.isLetterOrDigit => c 17 | }.mkString // TODO--remove `mkString` when String has `collect` again! 18 | def altType: String 19 | def heritage: String 20 | def generatorName: String 21 | def pkgName: String 22 | def pkgFull: String 23 | def colType: String = f"$pkgFull.$ccType[$eltCC]" 24 | def instTypes: String = f"$eltType, $colType" 25 | def opsTypes: String = f"$eltType, $altType" 26 | 27 | /** The name of the generated test class */ 28 | def className: String = f"Test_${pkgName}_${ccType}_${safeElt}" 29 | 30 | /** Generates the code */ 31 | def code: String = { 32 | val instance = instanceExplorer().completeIterator().take(1).toList.head 33 | val quotedMethods = instance.methods.methods. 34 | toList.sorted. 35 | map{ s => "\"" + (if (s contains '\\') s.replace("\\", "\\\\") else s) + "\"" } 36 | val appropriate = Laws.all. 37 | filter(law => law.checker passes instance.methods). 38 | filter(law => law.tags compatible (autoTags ++ instance.flags)). 39 | sortBy(_.lineNumber); 40 | ( 41 | Array( 42 | f"// Autogenerated test for collection $ccType with element type $eltType", 43 | f"", 44 | f"package laws", 45 | f"", 46 | f"class $className(numb: Numbers, oper: Ops[$opsTypes], inst: Instance[$instTypes], lln: Int)", 47 | f"extends $heritage[$colType, $className](numb, oper, inst, lln) {", 48 | f" import Test.Once.Conversions._", 49 | if (instance.flags contains Flag.SEQ) { 50 | " import Test.EqualInOrder" 51 | } 52 | else { 53 | " import Test.EqualInCount" 54 | }, 55 | f" import Test.SubsetInCount", 56 | f" import Test.Logic", 57 | f" import Test.SameCompilerType", 58 | f"", 59 | f"", 60 | f" def renumber(numb: Numbers) = ", 61 | f" new $className(numb, ops, instance, lawLine)", 62 | f"", 63 | f" def reinstance(inst: Instance[$instTypes]) = ", 64 | f" new $className(num, ops, inst, lawLine)", 65 | f"", 66 | f" def reoperate(oper: Ops[$opsTypes]) = ", 67 | f" new $className(num, oper, instance, lawLine)", 68 | f"", 69 | f" def relaw(lln: Int) = new $className(num, ops, instance, lln)", 70 | f"", 71 | f" /***** Individual laws begin here *****/", 72 | f"" 73 | ) ++ 74 | appropriate.map{ law => 75 | f" def runLaw${law.lineNumber}: Boolean = {\n" + 76 | law.cleanCode.split("\n").map(" " + _).mkString("", "\n", "\n") + 77 | f" }\n" 78 | } ++ 79 | Array( 80 | f" /****** Individual laws end here ******/", 81 | f"", 82 | f" val lawTable: Map[Int, () => Boolean] = Map(" 83 | ) ++ 84 | appropriate.map(_.lineNumber).zipWithIndex.map{ case (n, i) => 85 | f" $n -> (() => runLaw$n)${if (i+1 < appropriate.length)"," else ""}" 86 | } ++ 87 | Array( 88 | f" )", 89 | f"", 90 | f" def runLaw(n: Int): Option[Boolean] = lawTable.get(n).map(_())", 91 | f"}", 92 | f"", 93 | f"", 94 | f"object $className extends Test.Companion {", 95 | f""" def name = "$className" """, 96 | f"", 97 | f" val lawNumbers = ${appropriate.map(_.lineNumber).mkString("Set[Int](", ", ", ")")}", 98 | f"", 99 | f" val obeys = lawNumbers.map(n => n -> Laws.byLineNumber(n)).toMap", 100 | f"", 101 | f" val methods = Set(${quotedMethods.mkString(", ")})", 102 | f"", 103 | f" val factory: (Int, Instance[$instTypes], Ops[$opsTypes], Numbers) => $className =", 104 | f" (l, i, o, n) => new $className(n, o, i, l)", 105 | f"", 106 | f" val instanceExpy = () => $generatorName.instanceExplorer", 107 | f"", 108 | f" val opsExpy = () => $generatorName.opsExplorer", 109 | f"", 110 | f" def runnerOf(lln: Int): Runner[$eltType, $altType, $colType, $className] =", 111 | f" new Runner(lln, instanceExpy, opsExpy, factory)", 112 | f"", 113 | f" def run(lln: Int): Either[Outcome, Long] = runnerOf(lln).run", 114 | f"}", 115 | f"" 116 | ) 117 | ).mkString("\n") 118 | } 119 | } 120 | 121 | /** Generates tests for collections using `Int` elements */ 122 | abstract class IntGenerator[CC] extends Generator[Int, Long, CC] { 123 | val opsExplorer = Ops.IntExplorer 124 | val heritage = "IntTest" 125 | val eltType = "Int" 126 | val altType = "Long" 127 | val autoTags = Set(Flag.INT) 128 | } 129 | 130 | /** Generates tests for collections using `String` elements */ 131 | abstract class StrGenerator[CC] extends Generator[String, Option[String], CC] { 132 | val opsExplorer = Ops.StrExplorer 133 | val heritage = "StrTest" 134 | val eltType = "String" 135 | val altType = "Option[String]" 136 | val autoTags = Set(Flag.STR) 137 | override def className: String = f"Test_${pkgName}_${ccType}_Str" 138 | } 139 | 140 | /** Generates tests for collections (maps, specifically) using `(Long, String)` elements */ 141 | abstract class LongStrGenerator[CC] extends Generator[(Long, String), (String, Long), CC] { 142 | val opsExplorer = Ops.LongStrExplorer 143 | val heritage = "LongStrTest" 144 | val eltType = "(Long, String)" 145 | val altType = "(String, Long)" 146 | val autoTags = Set.empty[Flag] 147 | } 148 | 149 | /** Generates tests for collections (maps, specifically) using `(String, Long)` elements */ 150 | abstract class StrLongGenerator[CC] extends Generator[(String, Long), (Long, String), CC] { 151 | val opsExplorer = Ops.StrLongExplorer 152 | val heritage = "StrLongTest" 153 | val eltType = "(String, Long)" 154 | val altType = "(Long, String)" 155 | val autoTags = Set.empty[Flag] 156 | } 157 | 158 | /** Generates all classes testing collections that take `Int` element types. 159 | * 160 | * Many commonalities with `AllStrGenerators`, but it's a such a pain to make everything generic that it's not worth abstracting. 161 | */ 162 | object AllIntGenerators { 163 | val io = InstantiatorsOfInt 164 | 165 | /** An actual generator */ 166 | class Gen[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[Int, CC], ct: String = "")(implicit name: sourcecode.Name) 167 | extends IntGenerator[CC] { 168 | def generatorName = f"AllIntGenerators.${p.nickname}.${name.value}" 169 | def pkgName = p.nickname 170 | def pkgFull = p.fullyQualified 171 | override def colType = if (ct.isEmpty) super.colType else ct 172 | val instanceExplorer = io.map(iexp(p).tupled) 173 | def ccType = name.value.toString.capitalize 174 | } 175 | 176 | private val everyoneBuffer = Array.newBuilder[Gen[_, _]] 177 | 178 | /** Registers a particular generator to write its output to a file */ 179 | def register[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[Int, CC], ct: String = "")(implicit name: sourcecode.Name): Gen[P, CC] = { 180 | val ans = new Gen(p)(iexp, ct)(name) 181 | everyoneBuffer += ans 182 | ans 183 | } 184 | 185 | /** Generators for all immutable collections. 186 | * 187 | * This is a fairly tedious listing of everything in the corresponding instantiator. 188 | * 189 | * For now, this is kept separate to make it easier to temporarily leave out problematic collections. 190 | * (You'll get a warning if you leave one out, though.) 191 | */ 192 | object Imm { 193 | val arraySeq = register(io.Imm)(_.arraySeq()) 194 | val hashSet = register(io.Imm)(_.hashSet()) 195 | val indexedSeq = register(io.Imm)(_.indexedSeq()) 196 | val iterable = register(io.Imm)(_.iterable()) 197 | val lazyList = register(io.Imm)(_.lazyList()) 198 | val linearSeq = register(io.Imm)(_.linearSeq()) 199 | val list = register(io.Imm)(_.list()) 200 | val queue = register(io.Imm)(_.queue()) 201 | val seq = register(io.Imm)(_.seq()) 202 | val set = register(io.Imm)(_.set()) 203 | val sortedSet = register(io.Imm)(_.sortedSet()) 204 | val stream = register(io.Imm)(_.stream()) 205 | val traversable = register(io.Imm)(_.traversable()) 206 | val treeSet = register(io.Imm)(_.treeSet()) 207 | val vector = register(io.Imm)(_.vector()) 208 | } 209 | 210 | /** Generators for all mutable collections. 211 | * 212 | * This is a fairly tedious listing of everything in the corresponding instantiator. 213 | * 214 | * For now, this is kept separate to make it easier to temporarily leave out problematic collections. 215 | * (You'll get a warning if you leave one out, though.) 216 | */ 217 | object Mut { 218 | val array = register(io.Mut)(_.array(), "Array[Int]") 219 | val arrayBuffer = register(io.Mut)(_.arrayBuffer()) 220 | val arrayDeque = register(io.Mut)(_.arrayDeque()) 221 | val arraySeq = register(io.Mut)(_.arraySeq()) 222 | val arrayStack = register(io.Mut)(_.arrayStack()) 223 | val buffer = register(io.Mut)(_.buffer()) 224 | val hashSet = register(io.Mut)(_.hashSet()) 225 | val indexedSeq = register(io.Mut)(_.indexedSeq()) 226 | val iterable = register(io.Mut)(_.iterable()) 227 | val linkedHashSet = register(io.Mut)(_.linkedHashSet()) 228 | val listBuffer = register(io.Mut)(_.listBuffer()) 229 | val priorityQueue = register(io.Mut)(_.priorityQueue()) 230 | val queue = register(io.Mut)(_.queue()) 231 | val seq = register(io.Mut)(_.seq()) 232 | val stack = register(io.Mut)(_.stack()) 233 | val treeSet = register(io.Mut)(_.treeSet()) 234 | val wrappedArray = register(io.Mut)(_.wrappedArray()) 235 | } 236 | 237 | /** Generator for iterators and views. */ 238 | object Root { 239 | val iterator = register(io.Root)(_.iterator()) 240 | val view = register(io.Root)(_.view()) 241 | 242 | // These don't work because they require a different collection type for arguments 243 | // val indexedSeqView = register(io.Root)(_.indexedSeqView()) 244 | // val seqView = register(io.Root)(_.seqView()) 245 | } 246 | 247 | /** Generator for immutable collections that take only ints (which belong here, since we're dealing with ints). */ 248 | object ImmInt { 249 | val bitSet = register(io.ImmInt)(_.bitSet(), "collection.immutable.BitSet") 250 | //val range = register(io.ImmInt)(_.range(), "collection.immutable.Range") 251 | } 252 | 253 | /** Generator for mutable collections that take only ints (which belong here, since we're dealing with ints). */ 254 | object MutInt { 255 | val bitSet = register(io.MutInt)(_.bitSet(), "collection.mutable.BitSet") 256 | } 257 | 258 | /** This line is needed to actually perform the registration of all generators! */ 259 | val force = Imm :: Mut :: Root :: ImmInt :: MutInt :: Nil 260 | 261 | /** All registered generators */ 262 | lazy val all = everyoneBuffer.result() 263 | 264 | /** Writes all the classes to the indicated directory. Returns a map indicating which ones 265 | * actually changed. (If the generated code is identical to the file, the file is not re-written.) 266 | */ 267 | def write(targetDir: java.io.File): Map[String, Boolean] = 268 | all.map(g => g.className -> FileIO(new java.io.File(targetDir, g.className + ".scala"), g.code)).toMap 269 | } 270 | 271 | /** Generates all classes that test collections taking `String` element types. 272 | * 273 | * Many commonalities with `AllStrGenerators`, but it's a such a pain to make everything generic that it's not worth abstracting. 274 | */ 275 | object AllStrGenerators { 276 | val io = InstantiatorsOfStr 277 | 278 | /** An actual generator */ 279 | class Gen[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[String, CC], ct: String = "")(implicit name: sourcecode.Name) 280 | extends StrGenerator[CC] { 281 | def generatorName = f"AllStrGenerators.${p.nickname}.${name.value}" 282 | def pkgName = p.nickname 283 | def pkgFull = p.fullyQualified 284 | override def colType = if (ct.isEmpty) super.colType else ct 285 | val instanceExplorer = io.map(iexp(p).tupled) 286 | def ccType = name.value.toString.capitalize 287 | } 288 | 289 | private val everyoneBuffer = Array.newBuilder[Gen[_, _]] 290 | 291 | /** Registers a particular generator to write its output to a file */ 292 | def register[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[String, CC], ct: String = "")(implicit name: sourcecode.Name): Gen[P, CC] = { 293 | val ans = new Gen(p)(iexp, ct)(name) 294 | everyoneBuffer += ans 295 | ans 296 | } 297 | 298 | /** Generators for all immutable collections. 299 | * 300 | * This is a fairly tedious listing of everything in the corresponding instantiator. 301 | * 302 | * For now, this is kept separate to make it easier to temporarily leave out problematic collections. 303 | * (You'll get a warning if you leave one out, though.) 304 | */ 305 | object Imm { 306 | val arraySeq = register(io.Imm)(_.arraySeq()) 307 | val hashSet = register(io.Imm)(_.hashSet()) 308 | val indexedSeq = register(io.Imm)(_.indexedSeq()) 309 | val iterable = register(io.Imm)(_.iterable()) 310 | val lazyList = register(io.Imm)(_.lazyList()) 311 | val linearSeq = register(io.Imm)(_.linearSeq()) 312 | val list = register(io.Imm)(_.list()) 313 | val queue = register(io.Imm)(_.queue()) 314 | val seq = register(io.Imm)(_.seq()) 315 | val set = register(io.Imm)(_.set()) 316 | val sortedSet = register(io.Imm)(_.sortedSet()) 317 | val stream = register(io.Imm)(_.stream()) 318 | val traversable = register(io.Imm)(_.traversable()) 319 | val treeSet = register(io.Imm)(_.treeSet()) 320 | val vector = register(io.Imm)(_.vector()) 321 | } 322 | 323 | /** Generators for all mutable collections. 324 | * 325 | * This is a fairly tedious listing of everything in the corresponding instantiator. 326 | * 327 | * For now, this is kept separate to make it easier to temporarily leave out problematic collections. 328 | * (You'll get a warning if you leave one out, though.) 329 | */ 330 | object Mut { 331 | val array = register(io.Mut)(_.array(), "Array[String]") 332 | val arrayBuffer = register(io.Mut)(_.arrayBuffer()) 333 | val arrayDeque = register(io.Mut)(_.arrayDeque()) 334 | val arraySeq = register(io.Mut)(_.arraySeq()) 335 | val arrayStack = register(io.Mut)(_.arrayStack()) 336 | val buffer = register(io.Mut)(_.buffer()) 337 | val hashSet = register(io.Mut)(_.hashSet()) 338 | val indexedSeq = register(io.Mut)(_.indexedSeq()) 339 | val iterable = register(io.Mut)(_.iterable()) 340 | val linkedHashSet = register(io.Mut)(_.linkedHashSet()) 341 | val listBuffer = register(io.Mut)(_.listBuffer()) 342 | val priorityQueue = register(io.Mut)(_.priorityQueue()) 343 | val queue = register(io.Mut)(_.queue()) 344 | val seq = register(io.Mut)(_.seq()) 345 | val stack = register(io.Mut)(_.stack()) 346 | val treeSet = register(io.Mut)(_.treeSet()) 347 | val wrappedArray = register(io.Mut)(_.wrappedArray()) 348 | } 349 | 350 | /** Generator for iterators. */ 351 | object Root { 352 | val iterator = register(io.Root)(_.iterator()) 353 | val view = register(io.Root)(_.view()) 354 | 355 | // These don't work because they require a different collection type for arguments 356 | // val indexedSeqView = register(io.Root)(_.indexedSeqView()) 357 | // val seqView = register(io.Root)(_.seqView()) 358 | } 359 | 360 | /** This line is needed to actually perform the registration of all generators! */ 361 | val force = Imm :: Mut :: Root :: Nil 362 | 363 | /** All registered generators */ 364 | lazy val all = everyoneBuffer.result() 365 | 366 | /** Writes all the classes to the indicated directory. Returns a map indicating which ones 367 | * actually changed. (If the generated code is identical to the file, the file is not re-written.) 368 | */ 369 | def write(targetDir: java.io.File): Map[String, Boolean] = 370 | all.map(g => g.className -> FileIO(new java.io.File(targetDir, g.className + ".scala"), g.code)).toMap 371 | } 372 | 373 | /** Generates all classes that test collections (maps) with `(Long, String)` element types. 374 | * 375 | * Many commonalities with other generators, but it's a such a pain to make everything generic that it's not worth abstracting. 376 | */ 377 | object AllLongStrGenerators { 378 | val io = InstantiatorsOfLongStr 379 | 380 | class Gen[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[(Long, String), CC], ct: String = "")(implicit name: sourcecode.Name) 381 | extends LongStrGenerator[CC] { 382 | def generatorName = f"AllLongStrGenerators.${p.nickname}.${name.value}" 383 | def pkgName = p.nickname 384 | def pkgFull = p.fullyQualified 385 | override def colType = if (ct.isEmpty) super.colType else ct 386 | val instanceExplorer = io.map(iexp(p).tupled) 387 | def ccType = name.value.toString.capitalize 388 | override lazy val eltCC = eltType.dropWhile(_ == '(').reverse.dropWhile(_ == ')').reverse 389 | } 390 | 391 | private val everyoneBuffer = Array.newBuilder[Gen[_, _]] 392 | 393 | def register[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[(Long, String), CC], ct: String = "")(implicit name: sourcecode.Name): Gen[P, CC] = { 394 | val ans = new Gen(p)(iexp, ct)(name) 395 | everyoneBuffer += ans 396 | ans 397 | } 398 | 399 | object ImmKV { 400 | val hashMap = register(io.ImmKV)(_.hashMap()) 401 | val listMap = register(io.ImmKV)(_.listMap()) 402 | val sortedMap = register(io.ImmKV)(_.sortedMap()) 403 | val treeMap = register(io.ImmKV)(_.treeMap()) 404 | val treeSeqMap = register(io.ImmKV)(_.treeSeqMap()) 405 | val vectorMap = register(io.ImmKV)(_.vectorMap()) 406 | } 407 | 408 | object MutKV { 409 | val hashMap = register(io.MutKV)(_.hashMap()) 410 | val listMap = register(io.MutKV)(_.listMap()) 411 | val linkedHashMap = register(io.MutKV)(_.linkedHashMap()) 412 | val openHashMap = register(io.MutKV)(_.openHashMap()) 413 | val sortedMap = register(io.MutKV)(_.sortedMap()) 414 | val treeMap = register(io.MutKV)(_.treeMap()) 415 | val weakHashMap = register(io.MutKV)(_.weakHashMap()) 416 | } 417 | 418 | object MutLongV { 419 | val longMap = register(io.MutLongV)(_.longMap(), "collection.mutable.LongMap[String]") 420 | } 421 | 422 | /** This line is needed to actually perform the registration of all generators! */ 423 | val force = ImmKV :: MutKV :: MutLongV :: Nil 424 | 425 | lazy val all = everyoneBuffer.result() 426 | 427 | def write(targetDir: java.io.File): Map[String, Boolean] = 428 | all.map(g => g.className -> FileIO(new java.io.File(targetDir, g.className + ".scala"), g.code)).toMap 429 | } 430 | 431 | /** Generates all classes that test collections (maps) with `(String, Long)` 432 | * Many commonalities with other generators, but it's a such a pain to make everything generic that it's not worth abstracting. 433 | */ 434 | object AllStrLongGenerators { 435 | val io = InstantiatorsOfStrLong 436 | 437 | class Gen[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[(String, Long), CC], ct: String = "")(implicit name: sourcecode.Name) 438 | extends StrLongGenerator[CC] { 439 | def generatorName = f"AllStrLongGenerators.${p.nickname}.${name.value}" 440 | def pkgName = p.nickname 441 | def pkgFull = p.fullyQualified 442 | override def colType = if (ct.isEmpty) super.colType else ct 443 | val instanceExplorer = io.map(iexp(p).tupled) 444 | def ccType = name.value.toString.capitalize 445 | override lazy val eltCC = eltType.dropWhile(_ == '(').reverse.dropWhile(_ == ')').reverse 446 | } 447 | 448 | private val everyoneBuffer = Array.newBuilder[Gen[_, _]] 449 | 450 | def register[P <: Instance.PackagePath, CC](p: P)(iexp: P => Instance.FromArray[(String, Long), CC], ct: String = "")(implicit name: sourcecode.Name): Gen[P, CC] = { 451 | val ans = new Gen(p)(iexp, ct)(name) 452 | everyoneBuffer += ans 453 | ans 454 | } 455 | 456 | object ImmKV { 457 | val hashMap = register(io.ImmKV)(_.hashMap()) 458 | val listMap = register(io.ImmKV)(_.listMap()) 459 | val sortedMap = register(io.ImmKV)(_.sortedMap()) 460 | val treeMap = register(io.ImmKV)(_.treeMap()) 461 | val treeSeqMap = register(io.ImmKV)(_.treeSeqMap()) 462 | val vectorMap = register(io.ImmKV)(_.vectorMap()) 463 | } 464 | 465 | object MutKV { 466 | val hashMap = register(io.MutKV)(_.hashMap()) 467 | val listMap = register(io.MutKV)(_.listMap()) 468 | val linkedHashMap = register(io.MutKV)(_.linkedHashMap()) 469 | val openHashMap = register(io.MutKV)(_.openHashMap()) 470 | val sortedMap = register(io.MutKV)(_.sortedMap()) 471 | val treeMap = register(io.MutKV)(_.treeMap()) 472 | val weakHashMap = register(io.MutKV)(_.weakHashMap()) 473 | } 474 | 475 | /** This line is needed to actually perform the registration of all generators! */ 476 | val force = ImmKV :: MutKV :: Nil 477 | 478 | lazy val all = everyoneBuffer.result() 479 | 480 | def write(targetDir: java.io.File): Map[String, Boolean] = 481 | all.map(g => g.className -> FileIO(new java.io.File(targetDir, g.className + ".scala"), g.code)).toMap 482 | } 483 | 484 | /** The global generator that runs all particular generators of tests 485 | * and generates supervisory test files that run all the tests. 486 | */ 487 | object GenerateAll { 488 | /** Creates a master test that runs all the tests. 489 | * 490 | * The output is suitable for exploring with `Reports`. 491 | */ 492 | def writeUniversalTest(targetDir: java.io.File, tests: List[String]): (String, Boolean) = { 493 | val name = "Test_Everything" 494 | val target = new java.io.File(targetDir, name + ".scala") 495 | val lines = ( 496 | Array( 497 | f"package laws", 498 | f"", 499 | f"object $name extends AllRunner {", 500 | f" val runners: Array[() => (String, () => Test.Tested)] = Array(", 501 | tests.map{ t => 502 | f""" () => "$t" -> (() => $t.runAll())""" 503 | }.mkString(",\n"), 504 | f" )", 505 | f"}" 506 | ) 507 | ) 508 | (name, FileIO(target, lines.mkString("\n"))) 509 | } 510 | 511 | /** Creates a master jUnit test that marks all the tests for running via jUnit. 512 | * 513 | * Also hooks into the `@BeforeClass` and `@AfterClass` machinery to 514 | * give summary reports. 515 | */ 516 | def writeJUnitAdaptor(targetDir: java.io.File, tests: List[String]): (String, Boolean) = { 517 | val name = "Test_Everything_With_JUnit" 518 | val targetPath = new java.io.File(targetDir, "src/test/scala") 519 | if (!targetPath.exists) targetPath.mkdirs 520 | val target = new java.io.File(targetPath, name + ".scala") 521 | val lines = ( 522 | Array( 523 | f"package laws", 524 | f"", 525 | f"class $name {" 526 | ) ++ 527 | tests.zipWithIndex.flatMap{ case (t, i) => Array( 528 | f" @org.junit.Test", 529 | f" def run_$t: Unit = {", 530 | f" val (n, test) = Test_Everything.runners($i).apply()", 531 | f" $name.result.put(n, test.apply())", 532 | f" }" 533 | )} ++ 534 | Array( 535 | f"}", 536 | f"", 537 | f"object $name {", 538 | f" val result = new java.util.concurrent.ConcurrentHashMap[String, Test.Tested]", 539 | f" @org.junit.BeforeClass def setup: Unit = { result.clear() }", 540 | f" @org.junit.AfterClass def report: Unit = { Report.junitReport(result) }", 541 | f"}", 542 | f"" 543 | ) 544 | ) 545 | (name, FileIO(target, lines.mkString("\n"))) 546 | } 547 | 548 | /** Write all tests and test-supervisors to the given directory. 549 | * 550 | * Returns a map that indicates whether or not each test file 551 | * was actually updated, and a `Vector` that names all the 552 | * collections that were somehow left out of testing. 553 | */ 554 | def write(targetDir: java.io.File): (Map[String, Boolean], Vector[String]) = { 555 | val tests = 556 | AllIntGenerators.write(targetDir) ++ 557 | AllStrGenerators.write(targetDir) ++ 558 | AllLongStrGenerators.write(targetDir) ++ 559 | AllStrLongGenerators.write(targetDir) 560 | val everySource: Vector[Instance.Deployed] = 561 | InstantiatorsOfInt.all ++ 562 | InstantiatorsOfStr.all ++ 563 | InstantiatorsOfLongStr.all ++ 564 | InstantiatorsOfStrLong.all 565 | val testNames = tests.map(_._1).toList.sorted 566 | val universal = writeUniversalTest(targetDir, testNames) 567 | val junit = writeJUnitAdaptor(targetDir, testNames) 568 | val missingSources = everySource.filter(_.accesses == 0).map(_.path) 569 | (tests + universal + junit, missingSources.sorted) 570 | } 571 | 572 | /** Writes all the tests into a named directory (if the argument list is length one), 573 | * or into the default directory (if the argument list is empty). It is an error 574 | * to pass multiple arguments. 575 | * 576 | * Returns `true` if all collections have tests, `false` if any were missed (i.e. 577 | * were registered in `Instantiators` but weren't generated), and throws an 578 | * exception if anything unexpected happens. 579 | */ 580 | def run(args: Array[String]): Boolean = { 581 | val path = args match { 582 | case Array() => "tests" 583 | case Array(f) => f 584 | case _ => throw new IllegalArgumentException(f"Zero or one paths for output only (found ${args.length})") 585 | } 586 | 587 | val (upd, miss) = write(new java.io.File(path)) 588 | 589 | println(f"Generated code for ${upd.size} collection/element combinations") 590 | println(f" ${upd.count(_._2)} updated since last run") 591 | 592 | if (miss.nonEmpty) { 593 | println(f"Generators unimplemented for ${miss.size} collection/element sets:") 594 | miss.foreach(m => println(" " + m)) 595 | false 596 | } 597 | else true 598 | } 599 | 600 | /** Writes all the tests into the default directory (which is `tests`). */ 601 | def default(): Unit = { 602 | run(Array.empty) 603 | } 604 | 605 | /** Writes all the tests into the named (if there is one argument) or default 606 | * directory (if there are no arguments). 607 | * 608 | * Exits with an error code if any collections are completely untested. 609 | */ 610 | def main(args: Array[String]): Unit = { 611 | if (!run(args)) sys.exit(1) 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /laws/src/main/scala/Instance.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | import scala.reflect.runtime.universe.TypeTag 4 | 5 | /** A provider of instances of collections of a particular type with a particular element type. 6 | * Each call to the methods should return the same collection; if mutable, the instance 7 | * should be created afresh each time. If immutable, it shouldn't matter. 8 | * (Laws that check reference identity should cache the value.) 9 | * 10 | * That `A` is actually an element that can be found within `CC` is not enforced. 11 | * 12 | * Do not directly alter `used`; just pass it to the appropriate `Explore`. 13 | */ 14 | class Instance[A, CC: TypeTag] protected ( 15 | a0: A, x0: () => CC, xsize0: Int, y0: () => CC, ysize0: Int, 16 | arrayConverter: Array[A] => CC, 17 | val flags: Set[Flag], implicitMethods: MethodChecker = MethodChecker.empty 18 | ) { 19 | val values = Instance.Values(a0, x0, xsize0, y0, ysize0) 20 | 21 | val used = Array(false, false, false) 22 | 23 | /** An example element of a type that can be found within the collection */ 24 | def a: A = { used(0) = true; a0 } 25 | 26 | /** A particular instance of a collection. */ 27 | def x: CC = { used(1) = true; x0() } 28 | 29 | /** The size of the collection `x` */ 30 | def xsize: Int = { used(1) = true; xsize0 } 31 | 32 | /** Another instance of a collection, which may or may not be the same as `x` */ 33 | def y: CC = { used(2) = true; y0() } 34 | 35 | /** The size of the collection `y` */ 36 | def ysize: Int = { used(2) = true; ysize0 } 37 | 38 | /** The methods available on this collection */ 39 | lazy val methods = MethodChecker.from[CC] | implicitMethods 40 | 41 | /** Allows you to introduce more methods, e.g. those enriched via an implicit class */ 42 | def moreMethods(mc: MethodChecker): Instance[A, CC] = new Instance[A, CC](a0, x0, xsize, y0, ysize, arrayConverter, flags, methods | mc) 43 | 44 | def setUnused(): this.type = { java.util.Arrays.fill(used, false); this } 45 | 46 | def touched = used(0) || used(1) || used(2) 47 | 48 | def newInstanceFrom(aa: Array[A]): CC = arrayConverter(aa) 49 | 50 | /** Override this method to provide a more useful description in case of error 51 | * if the default `toString` is not helpful. 52 | */ 53 | protected def stringify(cc: CC): String = cc match { 54 | case i: Iterator[_] => i.mkString("Iterator(", ", ", ")") 55 | case a: Array[_] => a.mkString("Array(", ", ", ")") 56 | case _ => cc.toString() 57 | } 58 | 59 | override def equals(that: Any) = that match { 60 | case i: Instance[_, _] => (this eq i) || (this.values == i.values) 61 | case _ => false 62 | } 63 | override def hashCode: Int = { 64 | import scala.util.hashing.MurmurHash3._ 65 | finalizeHash(mixLast(mix(x0().##, y0().##), a0.##), 1 + xsize0 + ysize0) 66 | } 67 | override lazy val toString = { 68 | def clip(s: String, n: Int) = if (s.length <= n) s else s.substring(0, n-3)+"..." 69 | f"Provider: singleton $a0 with\n ${clip(stringify(x0()), 61)} ; len $xsize0\n ${clip(stringify(y0()), 61)} ; len $ysize0" 70 | } 71 | } 72 | object Instance { outer => 73 | /** Provides a single element and the appropriate collections, without logging of usage. 74 | * 75 | * Note that `x` and `y` are generators for collections. 76 | */ 77 | final case class Values[A, CC: TypeTag](a: A, x: () => CC, xsize: Int, y: () => CC, ysize: Int) 78 | 79 | /** Information on how to refer to the instance. */ 80 | trait PackagePath { 81 | def nickname: String 82 | def fullyQualified: String 83 | } 84 | 85 | /** Generates an instance from collection generators and flags. */ 86 | def apply[A, CC: TypeTag](a: A)(x: => CC, xsize: Int)(y: => CC, ysize: Int)(gen: Array[A] => CC, flags: Set[Flag] = Set.empty): Instance[A, CC] = 87 | new Instance( 88 | a, 89 | () => x, xsize, 90 | () => y, ysize, 91 | gen, flags 92 | ) 93 | 94 | /** Generates an instance from arrays and a transformation function, regenerating the collection each time. */ 95 | def from[A, CC: TypeTag: Sizable](a: A, x: Array[A], y: Array[A])(ccf: Array[A] => CC)(flags: Set[Flag] = Set.empty): Instance[A, CC] = 96 | new Instance( 97 | a, 98 | () => ccf(x), implicitly[Sizable[CC]].sizeof(ccf(x)), 99 | () => ccf(y), implicitly[Sizable[CC]].sizeof(ccf(y)), 100 | ccf, flags 101 | ) 102 | 103 | /** Generates an instance from arrays and a transformation function, caching the generated collection. */ 104 | def cacheFrom[A, CC: TypeTag: Sizable](a: A, x: Array[A], y: Array[A])(ccf: Array[A] => CC)(flags: Set[Flag] = Set.empty): Instance[A, CC] = 105 | new Instance( 106 | a, 107 | new CachedFn0(() => ccf(x)), implicitly[Sizable[CC]].sizeof(ccf(x)), 108 | new CachedFn0(() => ccf(y)), implicitly[Sizable[CC]].sizeof(ccf(y)), 109 | ccf, flags 110 | ) 111 | 112 | /** Encapsulates the capability of converting elements and arrays of elements into an `Instance` for a collection/element type */ 113 | trait FromArray[A, CC] extends ((A, Array[A], Array[A]) => Instance[A, CC]) with Named { self => 114 | def moreMethods(mc: MethodChecker): FromArray[A, CC] = new FromArray[A, CC] { 115 | def name = self.name 116 | def apply(a: A, x: Array[A], y: Array[A]) = self.apply(a, x, y).moreMethods(mc) 117 | } 118 | override def toString = f"$name from array" 119 | } 120 | 121 | /** Produce a `FromArray` generator for instances given an array-to-collection transformation method. */ 122 | def makeWith[A, CC: TypeTag: Sizable](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name): FromArray[A, CC] = 123 | new FromArray[A, CC] { 124 | def apply(a: A, x: Array[A], y: Array[A]) = from(a, x, y)(ccf)(flags.toSet) 125 | def name = nm.value 126 | } 127 | 128 | /** Produce a `FromArray` generator with caching for instances given an array-to-collection transformation method. */ 129 | def cacheWith[A, CC: TypeTag: Sizable](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name): FromArray[A, CC] = 130 | new FromArray[A, CC] { 131 | def apply(a: A, x: Array[A], y: Array[A]) = cacheFrom(a, x, y)(ccf)(flags.toSet) 132 | def name = nm.value 133 | } 134 | 135 | /** Adds default flags to collection generators */ 136 | class Flagged[A](allFlags: Flag*) { 137 | def makeWith[CC](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name, tt: TypeTag[CC], sz: Sizable[CC]): FromArray[A, CC] = 138 | outer.makeWith(ccf, (allFlags ++ flags): _*) 139 | def cacheWith[CC](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name, tt: TypeTag[CC], sz: Sizable[CC]): FromArray[A, CC] = 140 | outer.cacheWith(ccf, (allFlags ++ flags): _*) 141 | } 142 | def flagged[A](allFlags: Flag*): Flagged[A] = new Flagged[A](allFlags: _*) 143 | 144 | /** Keeps track of whether instances actually get prepared for deployment in tests. 145 | * 146 | * This is necessary because tests have to _manually_ refer to each collection type, in part to 147 | * allow sufficient customization. 148 | */ 149 | trait Deployed { 150 | def accesses: Int 151 | def name: String 152 | def group: String 153 | def path = group + ": " + name 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /laws/src/main/scala/Instantiator.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | import scala.language.higherKinds 4 | 5 | import scala.reflect.ClassTag 6 | import scala.reflect.runtime.universe.TypeTag 7 | 8 | import scala.annotation.nowarn 9 | 10 | /** Provides a source for individual instances we will test. 11 | * The variable names used are ones we can use to select 12 | * tests inside the generator. 13 | * 14 | * Collections that can take any generic type go in here. Collections that can only 15 | * take certain types go in more specific subclasses (or, in the case of maps, an alternate trait) below. 16 | * 17 | * The organization of the instantiators is rather repetitive, but favors transparency of 18 | * how the underlying collection is being generated. 19 | * 20 | * Each collection gets a single line inside a `C` creator utility method which hooks up a function 21 | * that generates the appropriate collection from an array, and sets any appropriate flags. This 22 | * utility method also registers the collection. 23 | * 24 | * To organize the collections by common packages, inner objects are used. These supply both a nickname 25 | * for the package, used in the test file name, and the full package path. Since objects are lazily 26 | * initialized, the package-specific objects then need to be evaluated. This is both an advantage and a 27 | * disadvantage: you can have more instantiators specified than you ever create, but it is also possible 28 | * to miss them entirely by missing the initializer (see `force`). 29 | */ 30 | abstract class InstantiatorsOf[A] 31 | extends Exploratory[(A, Array[A], Array[A])] { 32 | import Flag._ 33 | 34 | protected implicit def orderingOfA: Ordering[A] // Require some kind of ordering (even a dumb one) for all element types 35 | protected implicit def typeTagA: TypeTag[A] // TypeTag that gives us information about the element 36 | protected implicit def classTagA: ClassTag[A] // ClassTag that allows us to do things with arrays 37 | 38 | protected def allFlags: Array[Flag] 39 | protected val inst = Instance.flagged[A](collection.immutable.ArraySeq.unsafeWrapArray(allFlags): _*) // Add all the flags specified in `allFlags` 40 | 41 | // Ways to get sizes of different kinds of collections 42 | protected implicit def sizeOfSeq[A, S[A] <: collection.Seq[A]]: Sizable[S[A]] = 43 | new Sizable[S[A]] { def sizeof(s: S[A]) = s.length } 44 | protected implicit def sizeOfIterable[A, O[A] <: collection.Iterable[A]]: Sizable[O[A]] = 45 | new Sizable[O[A]] { def sizeof(o: O[A]) = o.size } 46 | protected implicit def sizeOfArray[A]: Sizable[Array[A]] = 47 | new Sizable[Array[A]] { def sizeof(a: Array[A]) = a.length } 48 | protected implicit val sizeOfString: Sizable[String] = 49 | new Sizable[String] { def sizeof(s: String) = s.length } 50 | protected implicit def sizeOfIterator[A]: Sizable[Iterator[A]] = 51 | new Sizable[Iterator[A]] { def sizeof(i: Iterator[A]) = i match { 52 | case iks: Root.IteratorKnowsSize[_] => iks.knownSize 53 | case _ => throw new Exception("Actually, `Iterator` hasn't the foggiest idea what its size is.") 54 | }} 55 | 56 | /** Marks when an instantiator is used (for completeness checking) */ 57 | trait Deployed[A, CC] extends Function0[Instance.FromArray[A, CC]] with Instance.Deployed { self => 58 | def secretly: Instance.FromArray[A, CC] 59 | 60 | // Forwarder that allows us to add a source of enriched methods 61 | def moreMethods(mc: MethodChecker): Deployed[A, CC] = new Deployed[A, CC] { 62 | def accesses = self.accesses 63 | def name = self.name 64 | def group = self.group 65 | override def path = self.path 66 | def apply() = self.apply().moreMethods(mc) 67 | def secretly = self.secretly.moreMethods(mc) 68 | } 69 | } 70 | 71 | protected val registry = Vector.newBuilder[Deployed[A, _]] 72 | 73 | object Imm extends Instance.PackagePath { 74 | def nickname = "Imm" 75 | def fullyQualified = "scala.collection.immutable" 76 | def C[CC: TypeTag: Sizable](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[A, CC] = { 77 | val gen = inst.cacheWith(ccf, flags: _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 78 | val ans = new Deployed[A, CC]{ 79 | val secretly = gen 80 | var accesses: Int = 0 81 | val name = nm.value.toString 82 | def group = typeTagA.tpe.toString + " in " + nickname 83 | def apply(): Instance.FromArray[A, CC] = { accesses += 1; secretly } 84 | } 85 | registry += ans 86 | ans 87 | } 88 | 89 | // MUST use lower-camel-cased collection class name for code generator to work properly! 90 | val arraySeq = C(collection.immutable.ArraySeq unsafeWrapArray _, SEQ) 91 | val hashSet = C(_.to(collection.immutable.HashSet), SET) 92 | val indexedSeq = C(_.to(collection.immutable.IndexedSeq), SEQ) 93 | val iterable = C(_.to(collection.immutable.Iterable)) 94 | val lazyList = C( 95 | a => collection.immutable.LazyList.from(0).takeWhile(_ < a.length).map(i => a(i)), 96 | SEQ, INDEF 97 | ) 98 | val linearSeq = C(_.to(collection.immutable.LinearSeq), SEQ) 99 | val list = C(_.toList, SEQ) 100 | val queue = C(_.to(collection.immutable.Queue), SEQ) 101 | val seq = C(_.to(collection.immutable.Seq), SEQ) 102 | val set = C(_.toSet, SET) 103 | val sortedSet = C(_.to(collection.immutable.SortedSet), SET, ORDERLY) 104 | @nowarn("cat=deprecation") 105 | val stream = C( 106 | a => collection.immutable.Stream.from(0).takeWhile(_ < a.length).map(i => a(i)), 107 | SEQ, INDEF 108 | ) 109 | 110 | val traversable = C(_.to(collection.immutable.Traversable)): @nowarn("cat=deprecation") 111 | val treeSet = C(_.to(collection.immutable.TreeSet), SET, ORDERLY) 112 | val vector = C(_.toVector, SEQ) 113 | } 114 | 115 | object Mut extends Instance.PackagePath { 116 | def nickname = "Mut" 117 | def fullyQualified = "scala.collection.mutable" 118 | def C[CC: TypeTag: Sizable](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[A, CC] = { 119 | val gen = inst.makeWith(ccf, flags: _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 120 | val ans = new Deployed[A, CC]{ 121 | val secretly = gen 122 | var accesses: Int = 0 123 | val name = nm.value.toString 124 | def group = typeTagA.tpe.toString + " in " + nickname 125 | def apply(): Instance.FromArray[A, CC] = { accesses += 1; secretly } 126 | } 127 | registry += ans 128 | ans 129 | } 130 | 131 | // MUST use lower-camel-cased collection class name for code generator to work properly! 132 | val array = C(_.clone, SEQ, ARRAY, SORTWITH_INT_CCE).moreMethods(MethodChecker.from[collection.ArrayOps[A]]) 133 | val arrayBuffer = C(_.to(collection.mutable.ArrayBuffer), SEQ) 134 | val arrayDeque = C(_.to(collection.mutable.ArrayDeque), SEQ, QUEUE_SLIDE_11440) 135 | val arraySeq = C(_.to(collection.mutable.ArraySeq), SEQ) 136 | @nowarn("cat=deprecation") 137 | val arrayStack = C(_.to(collection.mutable.ArrayStack), SEQ, QUEUE_SLIDE_11440) 138 | val buffer = C(_.to(collection.mutable.Buffer), SEQ) 139 | val hashSet = C(_.to(collection.mutable.HashSet), SET) 140 | val indexedSeq = C(_.to(collection.mutable.IndexedSeq), SEQ) 141 | val iterable = C(_.to(collection.mutable.Iterable)) 142 | val linkedHashSet= C(_.to(collection.mutable.LinkedHashSet), SET) 143 | val listBuffer = C(_.to(collection.mutable.ListBuffer), SEQ, LISTBUF_PIP_11438) 144 | val priorityQueue= C(_.to(collection.mutable.PriorityQueue), ORDERLY, PRIORITYQUEUE_IS_SPECIAL, PQ_MIP_NPE_11439) 145 | val queue = C(_.to(collection.mutable.Queue), SEQ, QUEUE_SLIDE_11440) 146 | val seq = C(_.to(collection.mutable.Seq), SEQ) 147 | val stack = C(_.to(collection.mutable.Stack), QUEUE_SLIDE_11440) 148 | val treeSet = C(_.to(collection.mutable.TreeSet), SET, ORDERLY) 149 | // val unrolledBuffer = C(_.to(collection.mutable.UnrolledBuffer), SEQ) // Unrolled buffer is weird! 150 | @nowarn("cat=deprecation") 151 | val wrappedArray = C(_.clone: collection.mutable.WrappedArray[A], SEQ) 152 | } 153 | 154 | object Root extends Instance.PackagePath { 155 | def nickname = "Root" 156 | def fullyQualified = "scala.collection" 157 | def C[CC: TypeTag: Sizable](ccf: Array[A] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[A, CC] = { 158 | val gen = inst.makeWith(ccf, flags: _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 159 | val ans = new Deployed[A, CC]{ 160 | val secretly = gen 161 | var accesses: Int = 0 162 | val name = nm.value.toString 163 | def group = typeTagA.tpe.toString + " in " + nickname 164 | def apply(): Instance.FromArray[A, CC] = { accesses += 1; secretly } 165 | } 166 | registry += ans 167 | ans 168 | } 169 | 170 | class IteratorKnowsSize[A](a: Array[A]) extends scala.collection.AbstractIterator[A] { 171 | private[this] var i = 0 172 | override def knownSize = a.length - i 173 | def hasNext = i < a.length 174 | def next() = 175 | if (!hasNext) Iterator.empty.next() 176 | else { 177 | val ans = a(i); 178 | i += 1 179 | ans 180 | } 181 | } 182 | 183 | // MUST use lower-camel-cased collection class name for code generator to work properly! 184 | val iterator = C(a => (new IteratorKnowsSize[A](a)): Iterator[A], ONCE, INDEF) 185 | val view = C(a => a.to(collection.immutable.Vector).view: scala.collection.View[A]) 186 | 187 | // These don't work because they take arguments of a different type than they are themselves 188 | // val indexedSeqView = C(a => a.view: scala.collection.IndexedSeqView[A]) 189 | // val seqView = C(a => a.to(collection.immutable.List).view: scala.collection.SeqView[A]) 190 | } 191 | 192 | def possible_a: Array[A] 193 | def possible_x: Array[Array[A]] 194 | def possible_y: Array[Array[A]] 195 | 196 | val sizes = Array(possible_a.length, possible_x.length, possible_y.length) 197 | 198 | def lookup(ixs: Array[Int]): Option[(A, Array[A], Array[A])] = 199 | if (!validate(ixs)) None 200 | else Some((possible_a(ixs(0)), possible_x(ixs(1)), possible_y(ixs(2)))) 201 | 202 | def force(): Any // Makes sure all the objects that are used are loaded. 203 | 204 | lazy val all = { 205 | force() 206 | registry.result() 207 | } 208 | } 209 | 210 | /** Instantiators for map types where both keys and values can be anything. 211 | * 212 | * Maps with restrictions on keys or values go in more specific subclasses below. 213 | */ 214 | trait InstantiatorsOfKV[K, V] extends Exploratory[((K, V), Array[(K, V)], Array[(K, V)])] { self: InstantiatorsOf[(K, V)] => 215 | import Flag._ 216 | 217 | protected implicit def orderingOfK: Ordering[K] 218 | 219 | protected implicit def typeTagK: TypeTag[K] 220 | protected implicit def typeTagV: TypeTag[V] 221 | protected val kvInst = Instance.flagged[(K, V)](collection.immutable.ArraySeq.unsafeWrapArray(allFlags): _*) 222 | protected implicit def sizeOfMap[K, V, M[K, V] <: collection.Map[K, V]]: Sizable[M[K, V]] = new Sizable[M[K, V]] { def sizeof(m: M[K, V]) = m.size } 223 | 224 | object ImmKV extends Instance.PackagePath { 225 | def nickname = "ImmKV" 226 | def fullyQualified = "scala.collection.immutable" 227 | def C[CC: TypeTag: Sizable](ccf: Array[(K, V)] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[(K, V), CC] = { 228 | val gen = kvInst.cacheWith(ccf, (MAP +: flags): _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 229 | val ans = new Deployed[(K, V), CC]{ 230 | val secretly = gen 231 | var accesses: Int = 0 232 | val name = nm.value.toString 233 | def group = typeTagA.tpe.toString + " in " + nickname 234 | def apply(): Instance.FromArray[(K, V), CC] = { accesses += 1; secretly } 235 | } 236 | registry += ans 237 | ans 238 | } 239 | 240 | // MUST use lower-camel-cased collection class name for code generator to work properly! 241 | val hashMap = C({ a => val mb = collection.immutable.HashMap.newBuilder[K, V]; for (kv <- a) mb += kv; mb.result() }) 242 | val listMap = C({ a => val mb = collection.immutable.ListMap.newBuilder[K, V]; for (kv <- a) mb += kv; mb.result() }) 243 | val sortedMap = C({ a => val mb = collection.immutable.SortedMap.newBuilder[K, V]; for (kv <- a) mb += kv; mb.result() }) 244 | val treeMap = C({ a => val mb = collection.immutable.TreeMap.newBuilder[K, V]; for (kv <- a) mb += kv; mb.result() }) 245 | val treeSeqMap = C({ a => val mb = collection.immutable.TreeSeqMap.newBuilder[K, V]; for (kv <- a) mb += kv; mb.result() }, INSORD) 246 | val vectorMap = C({ a => val mb = collection.immutable.VectorMap.newBuilder[K, V]; for (kv <- a) mb += kv; mb.result() }, INSORD) 247 | } 248 | 249 | object MutKV extends Instance.PackagePath { 250 | def nickname = "MutKV" 251 | def fullyQualified = "scala.collection.mutable" 252 | def C[CC: TypeTag: Sizable](ccf: Array[(K, V)] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[(K, V), CC] = { 253 | val gen = kvInst.makeWith(ccf, (MAP +: flags): _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 254 | val ans = new Deployed[(K, V), CC]{ 255 | val secretly = gen 256 | var accesses: Int = 0 257 | val name = nm.value.toString 258 | def group = typeTagA.tpe.toString + " in " + nickname 259 | def apply(): Instance.FromArray[(K, V), CC] = { accesses += 1; secretly } 260 | } 261 | registry += ans 262 | ans 263 | } 264 | 265 | // MUST use lower-camel-cased collection class name for code generator to work properly! 266 | val hashMap = C({ a => val m = new collection.mutable.HashMap[K, V]; for (kv <- a) m += kv; m }) 267 | @nowarn("cat=deprecation") 268 | val listMap = C({ a => val m = new collection.mutable.ListMap[K, V]; for (kv <- a) m += kv; m }) 269 | @nowarn("cat=deprecation") 270 | val linkedHashMap = C({ a => val m = new collection.mutable.LinkedHashMap[K, V]; for (kv <- a) m += kv; m }, INSORD) 271 | @nowarn("cat=deprecation") 272 | val openHashMap = C({ a => val m = new collection.mutable.OpenHashMap[K, V]; for (kv <- a) m += kv; m }) 273 | val sortedMap = C({ a => val m = collection.mutable.SortedMap.empty[K, V]; for (kv <- a) m += kv; m }) 274 | val treeMap = C({ a => val m = new collection.mutable.TreeMap[K, V]; for (kv <- a) m += kv; m }) 275 | val weakHashMap = C({ a => val m = new collection.mutable.WeakHashMap[K, V]; for (kv <- a) m += kv; m }) 276 | } 277 | } 278 | 279 | /** Default explicit orderings for the element types we have */ 280 | object OrderingSource { 281 | val orderingOfLong = implicitly[Ordering[Long]] 282 | val orderingOfInt = implicitly[Ordering[Int]] 283 | val orderingOfString = implicitly[Ordering[String]] 284 | val orderingOfLongString = implicitly[Ordering[(Long, String)]] 285 | val orderingOfStringLong = implicitly[Ordering[(String, Long)]] 286 | } 287 | 288 | /** Default explicit type tags for the element types we have */ 289 | object TypeTagSource { 290 | val typeTagInt = implicitly[TypeTag[Int]] 291 | val typeTagLong = implicitly[TypeTag[Long]] 292 | val typeTagString = implicitly[TypeTag[String]] 293 | val typeTagLongString = implicitly[TypeTag[(Long, String)]] 294 | val typeTagStringLong = implicitly[TypeTag[(String, Long)]] 295 | } 296 | 297 | /** Default explicit class tags for the element types we have */ 298 | object ClassTagSource { 299 | val classTagInt = implicitly[ClassTag[Int]] 300 | val classTagString = implicitly[ClassTag[String]] 301 | val classTagLongString = implicitly[ClassTag[(Long, String)]] 302 | val classTagStringLong = implicitly[ClassTag[(String, Long)]] 303 | } 304 | 305 | /** Instantiates collections with an `Int` element type.*/ 306 | object InstantiatorsOfInt extends InstantiatorsOf[Int] { 307 | import Flag._ 308 | 309 | protected implicit def orderingOfA: Ordering[Int] = OrderingSource.orderingOfInt 310 | protected implicit def typeTagA: TypeTag[Int] = TypeTagSource.typeTagInt 311 | protected implicit def classTagA: ClassTag[Int] = ClassTagSource.classTagInt 312 | protected def allFlags = Array(INT) 313 | 314 | protected implicit val sizeOfRange: Sizable[collection.immutable.Range] = 315 | new Sizable[collection.immutable.Range] { def sizeof(r: collection.immutable.Range) = r.size } 316 | protected implicit val sizeOfIBitSet: Sizable[collection.immutable.BitSet] = 317 | new Sizable[collection.immutable.BitSet] { def sizeof(s: collection.immutable.BitSet) = s.size } 318 | protected implicit val sizeOfMBitSet: Sizable[collection.mutable.BitSet] = 319 | new Sizable[collection.mutable.BitSet] { def sizeof(s: collection.mutable.BitSet) = s.size } 320 | 321 | /** Extra instantiators specific to Ints in immutable collections */ 322 | object ImmInt extends Instance.PackagePath { 323 | // If we have other (String, _) types, move this out into a trait 324 | def nickname = "ImmInt" 325 | def fullyQualified = "scala.collection.immutable" 326 | def C[CC: TypeTag: Sizable](ccf: Array[Int] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[Int, CC] = { 327 | val gen = inst.cacheWith(ccf, flags: _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 328 | val ans = new Deployed[Int, CC]{ 329 | val secretly = gen 330 | var accesses: Int = 0 331 | val name = nm.value.toString 332 | def group = typeTagA.tpe.toString + " in " + nickname 333 | def apply(): Instance.FromArray[Int, CC] = { accesses += 1; secretly } 334 | } 335 | registry += ans 336 | ans 337 | } 338 | 339 | // MUST use lower-camel-cased collection clasTs name for code generator to work properly! 340 | val bitSet = C( 341 | { a => val b = collection.immutable.BitSet.newBuilder; a.foreach{ x => if (x >= 0) b += x }; b.result() }, 342 | SET, ORDERLY, SPECTYPE, BITSET, BITSET_MAP_AMBIG, BITSET_ZIP_AMBIG 343 | ) 344 | //val range = C({ a => if (a.length % 3 == 0) 0 until a.length else 0 to a.length }) 345 | } 346 | 347 | /** Extra instantiators sepcific to Ints in mutable collections */ 348 | object MutInt extends Instance.PackagePath { 349 | // If we have other (String, _) types, move this out into a trait 350 | def nickname = "MutInt" 351 | def fullyQualified = "scala.collection.mutable" 352 | def C[CC: TypeTag: Sizable](ccf: Array[Int] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[Int, CC] = { 353 | val gen = inst.makeWith(ccf, flags: _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 354 | val ans = new Deployed[Int, CC]{ 355 | val secretly = gen 356 | var accesses: Int = 0 357 | val name = nm.value.toString 358 | def group = typeTagA.tpe.toString + " in " + nickname 359 | def apply(): Instance.FromArray[Int, CC] = { accesses += 1; secretly } 360 | } 361 | registry += ans 362 | ans 363 | } 364 | 365 | // MUST use lower-camel-cased collection class name for code generator to work properly! 366 | val bitSet = C( 367 | { a => val b = new collection.mutable.BitSet; a.foreach{ x => if (x >= 0) b += x }; b }, 368 | SET, ORDERLY, SPECTYPE, BITSET, BITSET_MAP_AMBIG, BITSET_ZIP_AMBIG 369 | ) 370 | } 371 | 372 | /** Singleton `Int` values to test */ 373 | lazy val possible_a = Array(0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 23, 31, 47, 152, 3133, 1294814, -1, -2, -6, -19, -1915, -19298157) 374 | 375 | /** Collection contents to test (for the primary `x` collection) */ 376 | lazy val possible_x = Array( 377 | Array.empty[Int], 378 | Array(0), 379 | Array(125), 380 | Array(-15), 381 | Array(0, 1), 382 | Array(0, 1, 2), 383 | Array(0, 1, 2, 3), 384 | Array(0, 1, 2, 3, 4), 385 | Array(4, 4, 4, 4, 4), 386 | Array(0, 1, 2, 3, 4, 5, 6), 387 | Array(0, 1, 2, 3, 4, 5, 6, 7), 388 | Array(0, 1, 2, 3, 4, 5, 6, 7, 8), 389 | Array(10, 10, 10, 10, 10, 5, 5, 5, 5, 1, 1, 1), 390 | Array(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1), 391 | Array.range(0,31), 392 | Array.range(0,32), 393 | Array.range(0,33), 394 | Array.range(0,192), 395 | Array.fill(254)(42), 396 | Array.range(0,811) 397 | ) 398 | /** The `y` collection can take on the same values as the `x` */ 399 | lazy val possible_y = possible_x 400 | 401 | /** This is important! This registers the collections that you actually want to have available! */ 402 | val force = Imm :: Mut :: Root :: ImmInt :: MutInt :: Nil 403 | } 404 | 405 | /** Instantiates collections with a `String` element type.*/ 406 | object InstantiatorsOfStr extends InstantiatorsOf[String] { 407 | import Flag._ 408 | 409 | protected implicit def orderingOfA: Ordering[String] = OrderingSource.orderingOfString 410 | protected implicit def typeTagA: TypeTag[String] = TypeTagSource.typeTagString 411 | protected implicit def classTagA: ClassTag[String] = ClassTagSource.classTagString 412 | protected def allFlags = Array(STR) 413 | 414 | /** Singleton `String` values to test */ 415 | lazy val possible_a = Array( 416 | "", "0", "one", "salmon", "\u0000\u0000\u0000\u0000", "the quick brown fox jumps over the lazy dog", "\u1517\u1851..!" 417 | ) 418 | 419 | /** Collection contents to test (for the primary `x` collection) */ 420 | lazy val possible_x = Array( 421 | Array.empty[String], 422 | Array(possible_a(1)), 423 | Array(possible_a(3)), 424 | possible_a, 425 | Array("0", "1", "0", "1", "0", "1", "0", "1", "0", "1", "0", "1"), 426 | Array.range(-44, 45).map(_.toString), 427 | Array.fill(184)("herring") 428 | ) 429 | /** The `y` collection can take on the same values as the `x` */ 430 | lazy val possible_y = possible_x 431 | 432 | /** This is important! This registers the collections that you actually want to have available! */ 433 | val force = Imm :: Mut :: Root :: Nil 434 | } 435 | 436 | /** Instantiates `(Long, String)` pairs for use with maps. 437 | * 438 | * Other collections could be tested with this tuple as the element type, but they're not. 439 | */ 440 | object InstantiatorsOfLongStr extends InstantiatorsOf[(Long, String)] with InstantiatorsOfKV[Long, String] { 441 | import Flag._ 442 | 443 | protected implicit def orderingOfA: Ordering[(Long, String)] = OrderingSource.orderingOfLongString 444 | protected implicit def orderingOfK: Ordering[Long] = OrderingSource.orderingOfLong 445 | protected implicit def typeTagA: TypeTag[(Long, String)] = TypeTagSource.typeTagLongString 446 | protected implicit def typeTagK: TypeTag[Long] = TypeTagSource.typeTagLong 447 | protected implicit def typeTagV: TypeTag[String] = TypeTagSource.typeTagString 448 | protected implicit def classTagA: ClassTag[(Long, String)] = ClassTagSource.classTagLongString 449 | protected def allFlags = Array[Flag]() 450 | 451 | protected implicit val sizeOfLongMap_Long_String: Sizable[collection.mutable.LongMap[String]] = 452 | new Sizable[collection.mutable.LongMap[String]] { 453 | def sizeof(m: collection.mutable.LongMap[String]) = m.size 454 | } 455 | 456 | /** Instantiators for special mutable maps requiring a `Long` key type */ 457 | object MutLongV extends Instance.PackagePath { 458 | // If we have other (String, _) types, move this out into a trait 459 | def nickname = "MutLongV" 460 | def fullyQualified = "scala.collection.mutable" 461 | def C[CC: TypeTag: Sizable](ccf: Array[(Long, String)] => CC, flags: Flag*)(implicit nm: sourcecode.Name): Deployed[(Long, String), CC] = { 462 | val gen = kvInst.makeWith(ccf, (MAP +: flags): _*)(nm, implicitly[TypeTag[CC]], implicitly[Sizable[CC]]) 463 | val ans = new Deployed[(Long, String), CC]{ 464 | val secretly = gen 465 | var accesses: Int = 0 466 | val name = nm.value.toString 467 | def group = typeTagA.tpe.toString + " in " + nickname 468 | def apply(): Instance.FromArray[(Long, String), CC] = { accesses += 1; secretly } 469 | } 470 | registry += ans 471 | ans 472 | } 473 | val longMap = C({ a => val m = new collection.mutable.LongMap[String]; for (kv <- a) m += kv; m }, SPECTYPE) 474 | } 475 | 476 | /** Very limited set of possible singletons */ 477 | lazy val possible_a = Array(3L -> "wish") 478 | 479 | /** Very limited set of possible values for arrays. */ 480 | lazy val possible_x = Array( 481 | Array.empty[(Long, String)], 482 | possible_a, 483 | Array(1L -> "herring", 2L -> "cod", 3L -> "salmon"), 484 | Array(9L -> "nine", 3L -> "three", 7L -> "seven", -1L -> "negative", 42L -> "Adams", 6L -> "vi"), 485 | (0 to 44).map(i => i.toLong -> i.toString).toArray 486 | ) 487 | /** The `y` collection can take on the same values as the `x` */ 488 | lazy val possible_y = possible_x 489 | 490 | /** This is important! This registers the collections that you actually want to have available! 491 | * 492 | * In particular, notice that we're only taking the key-value instantiators, so we only register maps, not all collections. 493 | */ 494 | val force = ImmKV :: MutKV :: MutLongV :: Nil 495 | } 496 | 497 | /** Instantiates `(Long, String)` pairs for use with maps. 498 | * 499 | * Other collections could be tested with this tuple as the element type, but they're not. 500 | */ 501 | object InstantiatorsOfStrLong extends InstantiatorsOf[(String, Long)] with InstantiatorsOfKV[String, Long] { 502 | import Flag._ 503 | 504 | protected implicit def orderingOfA: Ordering[(String, Long)] = OrderingSource.orderingOfStringLong 505 | protected implicit def orderingOfK: Ordering[String] = OrderingSource.orderingOfString 506 | protected implicit def typeTagA: TypeTag[(String, Long)] = TypeTagSource.typeTagStringLong 507 | protected implicit def typeTagK: TypeTag[String] = TypeTagSource.typeTagString 508 | protected implicit def typeTagV: TypeTag[Long] = TypeTagSource.typeTagLong 509 | protected implicit def classTagA: ClassTag[(String, Long)] = ClassTagSource.classTagStringLong 510 | protected def allFlags = Array[Flag]() 511 | 512 | lazy val possible_a = Array("wish" -> 3L) 513 | lazy val possible_x = Array( 514 | Array.empty[(String, Long)], 515 | possible_a, 516 | Array("herring" -> 1L, "cod" -> 2L, "salmon" -> 3L), 517 | Array("nine" -> 9L, "three" -> 3L, "seven" -> 7L, "negative" -> -1L, "Adams" -> 42L, "vi" -> 6L), 518 | (0 to 44).map(i => i.toString -> i.toLong).toArray 519 | ) 520 | lazy val possible_y = possible_x 521 | 522 | /** This is important! This registers the collections that you actually want to have available! 523 | * 524 | * In particular, notice that we're only taking the key-value instantiators, so we only register maps, not all collections. 525 | */ 526 | val force = ImmKV :: MutKV :: Nil 527 | } 528 | -------------------------------------------------------------------------------- /laws/src/main/scala/Laws.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /** An individual law that should be followed by (some) collections. */ 4 | case class Law(name: String, tags: Tags, code: String, disabled: Boolean = false)(implicit file: sourcecode.File, line: sourcecode.Line) { 5 | /** Constructor with code only (no name or tags) */ 6 | def this(code: String)(implicit file: sourcecode.File, line: sourcecode.Line) = 7 | this("", Tags.empty, code)(file, line) 8 | 9 | /** Constructor with no tags (but with name) */ 10 | def this(name: String, code: String)(implicit file: sourcecode.File, line: sourcecode.Line) = 11 | this(name, Tags.empty, code)(file, line) 12 | 13 | /** Constructor with tags (but no name) */ 14 | def this(tags: Tags, code: String)(implicit file: sourcecode.File, line: sourcecode.Line) = 15 | this("", tags, code)(file, line) 16 | 17 | /** Extracts the named methods within the code block of this law */ 18 | private[this] def findMyMethods: Either[Law.FormatErr, Set[String]] = { 19 | val b = Array.newBuilder[String] 20 | var i = 0 21 | while (i >= 0 && i < code.length) { 22 | i = code.indexOf('`', i) 23 | if (i >= 0) { 24 | val j = code.indexOf('`', i+1) 25 | if (j > i+1) b += code.substring(i+1, j) 26 | else return Left(Law.FormatErr("Unclosed method quotes", code, i, code.substring(i))) 27 | i = j+1 28 | } 29 | } 30 | Right(b.result().toSet) 31 | } 32 | 33 | /** Methods in the law that are backtick-quoted, indicating that the collection should only be used if it has those methods */ 34 | val methods = findMyMethods 35 | 36 | /** Function that checks whether a set of methods contains the methods needed to run this law */ 37 | val checker = findMyMethods.fold(_ => MethodChecker.empty, s => new MethodChecker(s)) 38 | 39 | val cleanCode = findMyMethods. 40 | map(ms => ms.foldLeft(code)((c, m) => c.replace(f"`$m`": CharSequence, m: CharSequence))). 41 | getOrElse(code) 42 | 43 | /** The line on which this law was defined; we assume for now that they're all from the same file */ 44 | val lineNumber = line.value 45 | 46 | override def toString = 47 | (if (name.isEmpty) "" else f"// $name\n") + 48 | code + 49 | (if (tags.isEmpty) "" else "\n// # " + tags.toString) + 50 | "\n// @ " + Sourced.local(file, line) + "\n" 51 | } 52 | object Law { 53 | /** FormatErr represents an error in the formatting of a collection law. Presently, 54 | * the only thing that can go wrong is an unclosed method name (the checked methods 55 | * should be enclosed in backticks). 56 | */ 57 | case class FormatErr(description: String, context: String, position: Int, focus: String) { 58 | override def toString = f"$description. At $position found $focus. In $context" 59 | } 60 | 61 | def apply(code: String)(implicit file: sourcecode.File, line: sourcecode.Line) = new Law(code)(file, line) 62 | def apply(name: String, code: String)(implicit file: sourcecode.File, line: sourcecode.Line) = new Law(name, code)(file, line) 63 | def apply(tags: Tags, code: String)(implicit file: sourcecode.File, line: sourcecode.Line) = new Law(tags, code)(file, line) 64 | } 65 | 66 | /** Specifies all the individual laws that should hold for collections. 67 | * 68 | * Flags are used to indicate which laws are appropriate for which collections (if they are not universally applicable). 69 | * 70 | * Flags do not need to be used to select collections on the basis of available methods; just quote the methods 71 | * in backticks and the code generator will figure it out. 72 | * 73 | * Do NOT ignore warnings about ignored values! If you forget `.law` on the end of a code string, you 74 | * will get one of these. All the law generation/registration extension methods return `Unit` so' 75 | * that these warnings are meaningful. 76 | */ 77 | object Laws { 78 | import Flag._ 79 | import Tags.Implicits._ 80 | 81 | /** Implements runtime filters for various values that tests may use. 82 | * 83 | * Make sure you filter out, at runtime, only things where other alternatives exist! 84 | * For instance, if you want `n` to be greater than 3, you should first filter to make sure 85 | * that `xsize` is greater than 3; otherwise a smaller collection will be chosen, the 86 | * filtering will reject everything, and the algorithm will not explore any further because 87 | * that collection size was never used. 88 | */ 89 | object Filt { 90 | def n(q: Int => Boolean) = (ti: TestInfo) => if (q(ti.num.values.n)) None else Some(Outcome.Skip.n) 91 | def nn(q: Int => Boolean) = (ti: TestInfo) => if (q(ti.num.values.nn)) None else Some(Outcome.Skip.nn) 92 | def m(q: Int => Boolean) = (ti: TestInfo) => if (q(ti.num.values.m)) None else Some(Outcome.Skip.m) 93 | def mm(q: Int => Boolean) = (ti: TestInfo) => if (q(ti.num.values.mm)) None else Some(Outcome.Skip.mm) 94 | def r(q: Int => Boolean) = (ti: TestInfo) => if (q(ti.num.values.r)) None else Some(Outcome.Skip.r) 95 | 96 | def f(q: (_ ===> _) => Boolean) = (ti: TestInfo) => if (q(ti.oper.values.f)) None else Some(Outcome.Skip.f) 97 | def g(q: (_ ===> _) => Boolean) = (ti: TestInfo) => if (q(ti.oper.values.g)) None else Some(Outcome.Skip.g) 98 | def op(q: OpFn[_] => Boolean) = (ti: TestInfo) => if (q(ti.oper.values.op)) None else Some(Outcome.Skip.op) 99 | def pf(q: ParFn[_] => Boolean) = (ti: TestInfo) => if (q(ti.oper.values.pf)) None else Some(Outcome.Skip.pf) 100 | def q(q: (_ ===> Boolean) => Boolean) = (ti: TestInfo) => if (q(ti.oper.values.p)) None else Some(Outcome.Skip.p) 101 | 102 | def xsize(q: Int => Boolean) = (ti: TestInfo) => if (q(ti.inst.values.xsize)) None else Some(Outcome.Skip.x) 103 | def ysize(q: Int => Boolean) = (ti: TestInfo) => if (q(ti.inst.values.ysize)) None else Some(Outcome.Skip.y) 104 | 105 | val zero = (ti: TestInfo) => if (ti.oper.values.op.zero.isDefined) None else Some(Outcome.Skip.op) 106 | val assoc = (ti: TestInfo) => if (ti.oper.values.op.assoc == OpFn.Associative) None else Some(Outcome.Skip.op) 107 | val sym = (ti: TestInfo) => if (ti.oper.values.op.sym == OpFn.Symmetric) None else Some(Outcome.Skip.op) 108 | } 109 | 110 | private val b = Array.newBuilder[Law] 111 | 112 | /** Implicits used to make the definition of laws as low-boilerplate as possible */ 113 | implicit class LawMaker(rawText: String)(implicit file: sourcecode.File, line: sourcecode.Line) { 114 | /** Used to force implicit resolution of this class if it's otherwise ambiguous */ 115 | def make: this.type = this 116 | 117 | /** The text used for code generation (slightly cleaned up from the string literal) */ 118 | lazy val text: String = { 119 | var i = 0; 120 | while (i < rawText.length && (rawText(i) == '\n' || rawText(i) == '\r')) i += 1; 121 | var j = rawText.length - 1; 122 | while (j > 0 && (rawText(j) == '\n' || rawText(j) == '\r')) j -= 1; 123 | if (i > j) rawText 124 | else if (i > 0 && j < rawText.length-1) rawText.substring(i, j+1) 125 | else if (i > 0) rawText.substring(i) 126 | else if (j < rawText.length-1) rawText.substring(0, j+1) 127 | else rawText 128 | } 129 | 130 | /** This law should not presently be used */ 131 | def skip: Unit = { 132 | b += new Law("", Tags.empty, text, disabled = true) 133 | } 134 | 135 | /** A law with no tags */ 136 | def law: Unit = { 137 | b += new Law(text) 138 | } 139 | 140 | /** A law with tags */ 141 | def law(tag: Tags.Taggish, more: Tags.Taggish*): Unit = { 142 | b += new Law(Tags(tag, more: _*), text) 143 | } 144 | 145 | /** A named law with no tags */ 146 | def named(name: String): Unit = { 147 | b += new Law(name, text) 148 | } 149 | 150 | /** A named law with tags */ 151 | def named(name: String, tag: Tags.Taggish, more: Tags.Taggish*): Unit = { 152 | b += new Law(name, Tags(tag, more: _*), text) 153 | } 154 | } 155 | 156 | ////////////////////////////// 157 | // Individual laws begin. // 158 | // Indentation removed for // 159 | // greater clarity but this // 160 | // is still inside the // 161 | // Laws object! // 162 | ////////////////////////////// 163 | 164 | /* 165 | ***************************************************************************** 166 | A guide to tags 167 | 168 | Various string tags are used to indicate properties that hold for only 169 | certain types of colletions. These include: 170 | 171 | selfs - only holds for collections that can maintain the same type after 172 | elements are dropped or scrambled (but have the same type) 173 | seq - only holds for collections that have a well-defined consistent order 174 | set - only holds for collections that remove duplicates 175 | ***************************************************************************** 176 | */ 177 | 178 | /******** Basic capabilities ********/ 179 | 180 | "x.`map`(f) sameAs { val y = collection.mutable.ArrayBuffer.empty[A]; x.foreach(xi => y += f(xi)); y }".law(SEQ) 181 | 182 | "x.`map`(f) sameAs { val y = collection.mutable.HashSet.empty[A]; x.foreach(y += _); y.map(f) }".law(SET, BITSET.!) 183 | 184 | "x.`map`(bitset_f) sameAs { val y = collection.mutable.HashSet.empty[A]; x.foreach(y += _); y.map(bitset_f) }".law(BITSET) 185 | 186 | "x sameType x.`map`(f)".law(ORDERLY.!) 187 | 188 | """{ 189 | val flat = x.`flatMap`(xi => y.toList.take(intFrom(xi))) 190 | val ref = collection.mutable.ArrayBuffer.empty[A] 191 | x.foreach(xi => ref ++= y.toArray.take(intFrom(xi))) 192 | flat sameAs ref 193 | }""".law(SEQ) 194 | 195 | """{ 196 | val flat = x.`flatMap`(xi => y.toList.take(intFrom(xi))) 197 | val ref = collection.mutable.HashSet.empty[A] 198 | x.foreach(xi => ref ++= y.toArray.take(intFrom(xi))) 199 | flat sameAs ref 200 | }""".law(SET) 201 | 202 | "x sameType x.`flatMap`(xi => y.toList.take(intFrom(xi)%3))".law(ORDERLY.!) 203 | 204 | "x.`exists`(p) == x.`find`(p).isDefined".law 205 | 206 | "x.`forall`(p) implies (x.`isEmpty` || x.`exists`(p))".law 207 | 208 | "{ var y = false; x.`foreach`(xi => y |= p(xi)); y == x.`exists`(p) }".law 209 | 210 | "x.`toIterator` sameAs x".law 211 | 212 | "x.`toStream` sameAs x".law 213 | 214 | "x.`toTraversable` sameAs x".law 215 | 216 | /******** String generation ********/ 217 | 218 | "x.`mkString` == { val sb = new StringBuilder; x.foreach(sb ++= _.toString); sb.result() }".law(SEQ) 219 | 220 | "x.`mkString` == { val sb = new StringBuilder; x.`addString`(sb); sb.result() }".law 221 | 222 | """x.`mkString` == x.mkString("", "", "")""".law 223 | 224 | """x.`mkString`("!") == x.mkString("", "!", "")""".law 225 | 226 | """{ 227 | (x.`size` == 0) || ( 228 | x.`mkString`("!", "@", "#") == { 229 | val sb = new StringBuilder 230 | sb ++= "!" 231 | x.foreach(sb ++= _.toString + "@") 232 | sb.result().dropRight(1) + "#" 233 | } 234 | ) 235 | }""".law(SEQ) 236 | 237 | """x.`mkString`("*") == { val sb = new StringBuilder; x.`addString`(sb, "*"); sb.result() }""".law(SEQ) 238 | 239 | """x.`mkString`("[", "|", "]") == { val sb = new StringBuilder; x.`addString`(sb, "[", "|", "]"); sb.result() }""".law(SEQ) 240 | 241 | 242 | /******** Everything else (roughly alphabetical) ********/ 243 | 244 | "x.`aggregate`(zero)((b,a) => b, (b1,b2) => b1) == zero".law(Filt.zero) 245 | 246 | "x.`aggregate`(zero)((b,a) => b, (b1,b2) => b2) == zero".law(Filt.zero) 247 | 248 | "x.`array`.take(x.`size`).toVector == x.toVector".law 249 | 250 | """ 251 | val arr = new Array[A](nn) 252 | x.`copyToArray`(arr, n, m) 253 | (x.toList zip arr.drop(n).take(m)).forall{ case (x, y) => x == y } 254 | """.law(SET.!, MAP.!, Filt.xsize(_ > 0)) 255 | 256 | 257 | """ 258 | val arr = new Array[A](nn) 259 | x.`copyToArray`(arr, n, m) 260 | val c = arr.drop(n).take(m min x.`size`) 261 | val cs = collectionFrom(c) 262 | c.size == cs.size && (cs subsetOf x.toSet) 263 | """.law(SET, Filt.xsize(_ > 0)) 264 | 265 | """ 266 | val arr = new Array[A](nn) 267 | val x0 = x 268 | x0.`copyToArray`(arr, n, m) 269 | (n until ((n+m) min x.`size`)).forall(i => x0.get(arr(i)._1).exists(_ == arr(i)._2)) 270 | """.law(MAP, Filt.xsize(_ > 0)) 271 | 272 | 273 | "x.`collectFirst`(pf).isDefined == x.`exists`(pf.isDefinedAt)".law 274 | 275 | "x.`collectFirst`(pf) == x.`dropWhile`(xi => !pf.isDefinedAt(xi)).`headOption`.map(pf)".law(SEQ) 276 | 277 | """ 278 | val b = new collection.mutable.ArrayBuffer[A] 279 | x.`copyToBuffer`(b) 280 | b sameAs x 281 | """.law 282 | 283 | "x sameAs x.`companion`(x.toList: _*)".law 284 | 285 | "x.`count`(p) > 0 == x.`exists`(p)".law 286 | 287 | "(x.`count`(p) == x.`size`) == x.`forall`(p)".law 288 | 289 | "x.`count`(p) == { var k=0; x.`foreach`(xi => if (p(xi)) k += 1); k }".law 290 | 291 | "x.`empty`.`isEmpty`".law 292 | 293 | "x sameType x.`empty`".law 294 | 295 | "x.filter(p).`size` == x.`count`(p)".law 296 | 297 | "x.filter(p).`forall`(p) == true".law 298 | 299 | "x sameType x.`filter`(p)".law 300 | 301 | """ 302 | val oneStep = x.flatMap(xi => y.toList.take(intFrom(xi) % 3 max 0)) 303 | val twoStep = x.map(xi => y.toList.take(intFrom(xi) % 3 max 0)).`flatten` 304 | oneStep sameAs twoStep 305 | """.law(MAP.!, ORDERLY.!, BITSET_MAP_AMBIG.!) 306 | 307 | """ 308 | val oneStep = x.flatMap(xi => y.toList.take(intFrom(xi) % 3 max 0)) 309 | val twoStep = x.`unsorted`.map(xi => y.toList.take(intFrom(xi) % 3 max 0)).`flatten` 310 | oneStep sameAs twoStep 311 | """.law(MAP.!, ORDERLY) 312 | 313 | 314 | """ 315 | val oneStep = x.flatMap(xi => y.toList.take(intFrom(xi) % 3 max 0)) 316 | val twoStep = collectionFrom(x.toArray.map(xi => y.toList.take(intFrom(xi) % 3 max 0).toArray).`flatten`) 317 | oneStep sameAs twoStep 318 | """.law(MAP.!) 319 | 320 | "x.`foldLeft`(a)(op) == x.`foldRight`(a)(op)".law(Filt.sym) 321 | 322 | "x.`foldLeft`(a)(op) == x.`/:`(a)(op)".law 323 | 324 | """x.`foldRight`(a)(op) == x.`:\`(a)(op)""".law 325 | 326 | "x.`isDefinedAt`(r) == (0 <= r && r < x.`size`)".law(SEQ) 327 | 328 | "x.`isDefinedAt`(a._1) == x.`get`(a._1).isDefined".law(MAP) 329 | 330 | "x.`isTraversableAgain`".law(ONCE.!) 331 | 332 | "!x.`isTraversableAgain`".law(ONCE) 333 | 334 | "tryO{x.`max`} == tryO{ x.`reduce`(maxOf) }".law 335 | 336 | "tryO{x.`maxBy`(f)} == tryO{ val fx = x.map(f).`max`; x.find(xi => f(xi)==fx).get }".law(BITSET.!) 337 | 338 | "tryO{x.`maxBy`(bitset_f)} == tryO{ val fx = x.map(bitset_f).`max`; x.find(xi => bitset_f(xi)==fx).get }".law(BITSET) 339 | 340 | "tryO{ x.`max` } == x.`maxOption`".law 341 | 342 | "tryO{ x.`maxBy`(f) } == x.`maxByOption`(f)".law(BITSET.!) 343 | 344 | "tryO{ x.`maxBy`(bitset_f) } == x.`maxByOption`(bitset_f)".law(BITSET) 345 | 346 | "tryO{x.`min`} == tryO{ x.`reduce`(minOf) }".law 347 | 348 | "tryO{x.`minBy`(f)} == tryO{ val fx = x.map(f).`min`; x.find(xi => f(xi)==fx).get }".law(BITSET.!) 349 | 350 | "tryO{x.`minBy`(bitset_f)} == tryO{ val fx = x.map(bitset_f).`min`; x.find(xi => bitset_f(xi)==fx).get }".law(BITSET) 351 | 352 | "tryO{ x.`min` } == x.`minOption`".law 353 | 354 | "tryO{ x.`minBy`(f) } == x.`minByOption`(f)".law(BITSET.!) 355 | 356 | "tryO{ x.`minBy`(bitset_f) } == x.`minByOption`(bitset_f)".law(BITSET) 357 | 358 | 359 | 360 | "x.`nonEmpty` == x.`exists`(_ => true)".law 361 | 362 | "x.`product` == x.`fold`(1)(_ * _)".law(INT) 363 | 364 | "x.`sum` == x.`fold`(0)(_ + _)".law(INT) 365 | 366 | """ 367 | Set( 368 | tryO{x.`reduce`(op)}, tryO{x.`reduceLeft`(op)}, tryO{x.`reduceRight`(op)}, 369 | x.`reduceOption`(op), x.`reduceLeftOption`(op), x.`reduceRightOption`(op) 370 | ).size == 1 371 | """.law(Filt.assoc) 372 | 373 | 374 | "x.`size` == x.`count`(_ => true)".law 375 | 376 | "math.signum(x.`sizeCompare`(y)) == math.signum(x.`size`.compare(y.`size`))".law(ARRAY.!) 377 | "math.signum(x.`sizeCompare`(y.`size`)) == math.signum(x.`size`.compare(y.`size`))".law 378 | 379 | "x.`sizeIs` < m == x.`size` < m".law 380 | 381 | "x.`sizeIs` > m == x.`size` > m".law 382 | 383 | "(x.`sizeIs` == m) == (x.`size` == m)".law 384 | 385 | "x.`sizeIs` < m == x.`lengthIs` < m".law 386 | 387 | "x.`sizeIs` > m == x.`lengthIs` > m".law 388 | 389 | "(x.`sizeIs` == m) == (x.`lengthIs` == m)".law 390 | 391 | 392 | "x sameAs x.`to`(List)".law 393 | 394 | "x.`toArray` sameAs x".law 395 | 396 | "x.`toBuffer` sameAs x".law 397 | 398 | "x.`toIndexedSeq` sameAs x".law 399 | 400 | "x.`toIterable` sameAs x".law 401 | 402 | "x.`toList` sameAs x".law 403 | 404 | """ 405 | val tomap = x.map(xi => (xi,xi)).`toMap` 406 | val canon = { 407 | val hm = new collection.mutable.HashMap[A,A] 408 | x.foreach(xi => hm += xi -> xi) 409 | hm 410 | } 411 | tomap samePieces canon 412 | """.law(BITSET_MAP_AMBIG.!) 413 | 414 | "x.`toSeq` sameAs x".law 415 | 416 | "x.`toSet` partOf x".law 417 | 418 | "x.`toVector` sameAs x".law 419 | 420 | """ 421 | val c = new collection.mutable.ArrayBuffer[A]; 422 | x.`withFilter`(p).foreach(c += _); 423 | c sameAs x.filter(p)""".law(SET.!, MAP.!) 424 | 425 | """ 426 | val c = new collection.mutable.HashSet[A]; 427 | x.`withFilter`(p).foreach(c += _); 428 | c sameAs x.filter(p)""".law(SET) 429 | 430 | "x.`hasNext` == x.`nonEmpty`".law 431 | 432 | "x.`hasNext` implies tryO{ x.`next`() }.isDefined".law 433 | 434 | "x.`nextOption`() == tryO{ x.`next`() }".law 435 | 436 | "x.`++`(y).`size` == x.size + y.size".law(SEQ) 437 | 438 | "x.`++`(y).`size` <= x.size + y.size".law(SEQ.!) 439 | 440 | "x.`++`(y).`size` == x.size + y.filter(yi => !x.`contains`(yi)).size".law(SET) 441 | 442 | "x.`++`(y).`size` == x.size + y.filter(yi => !x.`contains`(yi._1)).size".law(MAP) 443 | 444 | "(x.`++`(y)).`take`(x.`size`) sameAs x".law(SEQ) 445 | 446 | "(x.`++`(y)).`drop`(x.`size`) sameAs y".law(SEQ) 447 | 448 | "x.`++`(y) sameAs (x.toSeq ++ y.toSeq).toMap".law(MAP) 449 | 450 | "x.`++`(y) sameAs (x.toSeq ++ y.toSeq).toSet".law(SET) 451 | 452 | "x.`--`(y).`size` >= x.size - y.size".law(MAP.!) 453 | 454 | 455 | "x.`--`(y.map(_._1)).`size` >= x.size - y.size".law(MAP) 456 | 457 | "(x.`--`(y)) partOf x".law(MAP.!) 458 | 459 | "x.`--`(y.map(_._1)) partOf x".law(MAP) 460 | 461 | """ 462 | val List(q, qq, qqq) = List(x, y, x.`--`(y)).map(_.groupBy(identity).mapValues(_.size)); 463 | qqq.forall{ case (k,v) => v == math.max(0, q.getOrElse(k,0) - qq.getOrElse(k,0)) }""".law(MAP.!) 464 | 465 | """ 466 | val yy = y.map(_._1).toSet 467 | val xx = x.`--`(yy) 468 | x.forall{ case (k, v) => 469 | (yy(k) && ! xx.contains(k)) ^ (xx.get(k) == Some(v)) 470 | } 471 | """.law(MAP) 472 | 473 | "x sameType x.`++`(y)".law 474 | 475 | "x.`buffered` sameAs x".law 476 | 477 | "x.`collect`(pf) sameAs x.`filter`(pf.isDefinedAt).`map`(pf)".law(BITSET_MAP_AMBIG.!) 478 | 479 | "x sameType x.`collect`(pf)".law(ORDERLY.!, BITSET_MAP_AMBIG.!) 480 | 481 | "x.`contains`(a) == x.`exists`(_ == a)".law(MAP.!) 482 | 483 | "x.`contains`(a._1) == x.`exists`(_._1 == a._1)".law(MAP) 484 | 485 | "x.`corresponds`(x)(_ == _)".law 486 | 487 | "(x.`size` != y.size) implies !x.`corresponds`(y)(_ == _)".law 488 | 489 | "x.`corresponds`(y)((_,_) => false) implies !x.`nonEmpty` && !y.`nonEmpty`".law 490 | 491 | "x.`drop`(n).`size` == (0 max (x.size-(0 max n)))".law 492 | 493 | "x.`drop`(n) partOf x".law 494 | 495 | "x sameType x.`drop`(n)".law/*(CAMEL_WEAKMAP_SUPER.!)*/ 496 | 497 | """ 498 | val c = x.`dropWhile`(p); 499 | c.`take`(1).toList match { 500 | case Nil => true 501 | case List(xi) => !p(xi) 502 | case _ => false 503 | }""".law(SEQ) 504 | 505 | """ 506 | val y = x.`dropWhile`(p) 507 | y.nonEmpty implies y.exists(yi => !p(yi)) 508 | """.law 509 | 510 | """x.`dropWhile`(p) partOf x""".law 511 | 512 | "x sameType x.`dropWhile`(p)".law/*(CAMEL_WEAKMAP_SUPER.!)*/ 513 | 514 | """ 515 | val (x1,x2) = x.`duplicate` 516 | x1.`corresponds`(x)(_ == _) && x2.corresponds(x)(_ == _) 517 | """.law 518 | 519 | """ 520 | val (x1,x2) = x.`duplicate` 521 | (x sameType x1) && (x1 sameType x2) 522 | """.law 523 | 524 | "x.`filterNot`(p) sameAs x.`filter`(xi => !p(xi))".law 525 | 526 | "x sameType x.`filterNot`(p)".law/*(CAMEL_WEAKMAP_SUPER.!)*/ 527 | 528 | "x.`getOrElse`(a._1, a._2) == x.`get`(a._1).getOrElse(a._2)".law(MAP) 529 | 530 | "n <= 0 || x.`grouped`(n).map(_.size).`sum` == x.`size`".law 531 | 532 | """n <= 0 || m < 0 || { 533 | var y = 0; x.`grouped`(n).`drop`(m).take(1).map(_.`size`).foreach(y = _); (y < n) implies !x.grouped(n).drop(m+1).`nonEmpty` 534 | }""".law 535 | 536 | "x.`grouped`((1 max n)).flatMap(xi => xi) sameAs x".law 537 | 538 | "x.`hasDefiniteSize`".law(INDEF.!, Filt.xsize(_ > 0)) 539 | 540 | "!x.`hasDefiniteSize`".law(INDEF, Filt.xsize(_ > 0)) 541 | 542 | "x.`isEmpty` == !x.`nonEmpty`".law 543 | 544 | "x sameType x.`iterableFactory`.apply(a)".law(SPECTYPE.!, MAP.!, ORDERLY.!) 545 | 546 | "x.`iterableFactory`.apply(a).`iterator`.toList == List(a)".law 547 | 548 | "x.`length` == x.`size`".law 549 | 550 | "x.`mapResult`(_.`map`(f)).`addOne`(a).`result`() sameAs x.`addOne`(a).`result`().`map`(f)".law(BITSET.!) 551 | 552 | "x.`mapResult`(_.`map`(bitset_f)).`addOne`(a).`result`() sameAs x.`addOne`(a).`result`().`map`(bitset_f)".law(BITSET) 553 | 554 | "x.`padTo`(n, a).`size` == (n max x.size)".law 555 | 556 | "x.`padTo`(n, a).`drop`(x.`size`).`forall`(_ == a)".law 557 | 558 | "(n <= x.`size`) implies (x.`padTo`(n, a) sameAs x)".law 559 | 560 | "x sameType x.`padTo`(n,a)".law 561 | 562 | """ 563 | val (t,f) = x.`partition`(p) 564 | (t sameAs x.`filter`(p)) && (f sameAs x.`filterNot`(p)) 565 | """.law 566 | 567 | """ 568 | val (t,f) = x.`partition`(p) 569 | (t sameType f) && (t sameType x) 570 | """.law 571 | 572 | """ 573 | val (l, r) = x.`partitionMap`(xi => if (p(xi)) Left(f(xi)) else Right(xi)) 574 | val ll = x.`filter`(p).`map`(f) 575 | val rr = x.`filterNot`(p) 576 | (l sameAs ll) && (r sameAs rr) 577 | """.law(BITSET.!) 578 | 579 | """ 580 | val (l, r) = x.`partitionMap`(xi => if (p(xi)) Left(bitset_f(xi)) else Right(xi)) 581 | val ll = x.`filter`(p).`map`(bitset_f) 582 | val rr = x.`filterNot`(p) 583 | (l sameAs ll) && (r sameAs rr) 584 | """.law(BITSET) 585 | 586 | """ 587 | val (l, r) = x.`partitionMap`(xi => if (p(xi)) Left(f(xi)) else Right(xi)) 588 | (x sameType l) && (x sameType r) 589 | """.law(MAP.!, SPECTYPE.!, ORDERLY.!) 590 | 591 | "(n <= x.`size`) implies (x.`patch`(n, y, m).`take`(n) sameAs x.take(n))".law 592 | 593 | "(n <= x.`size`) implies (x.`patch`(n, y, m).`drop`(n).`take`(y.`size`) sameAs y)".law 594 | 595 | "(n <= x.`size`) implies (x.`patch`(n, y, m).`drop`((0 max n)+y.`size`) sameAs x.`drop`((0 max n)+(0 max m)))".law 596 | 597 | "(x `sameElements` y) == (x sameAs y)".law 598 | 599 | "n < 0 || m >= x.size || { x.`slice`(n, m).`size` == (0 max ((0 max m)-n)) }".law 600 | 601 | "n < 0 || m >= x.size || { x.`slice`(n, m) sameAs x.`drop`(n).`take`((0 max m)-n) }".law(SET.!) 602 | 603 | "n < 0 || m >= x.size || (x sameType x.`slice`(n, m))".law/*(CAMEL_WEAKMAP_SUPER.!)*/ 604 | 605 | "x.`span`(p)._1.`forall`(p)".law 606 | 607 | "(x.span(p)._2.`size`) > 0 implies !x.`span`(p)._2.`take`(1).`exists`(p)".law(SEQ) 608 | 609 | "(x.span(p)._2.`size`) > 0 implies !x.`span`(p)._2.`forall`(p)".law 610 | 611 | "val (x1, x2) = x.`span`(p); val n = x.`span`(p)._1.`size`; (x1 sameAs x.`take`(n)) && (x2 sameAs x.`drop`(n))".law 612 | 613 | """ 614 | val (x1, x2) = x.`span`(p) 615 | (x1 sameType x2) && (x sameType x1) 616 | """.law/*(CAMEL_WEAKMAP_SUPER.!)*/ 617 | 618 | "x.`take`(n).`size` == ((0 max n) min x.size)".law 619 | 620 | "x.`take`(n) partOf x".law 621 | 622 | "x sameType x.`take`(n)".law 623 | 624 | "x.`takeWhile`(p).`forall`(p)".law 625 | 626 | "x.`takeWhile`(p) partOf x".law 627 | 628 | "x.`takeWhile`(p).`size` + x.`dropWhile`(p).size == x.size".law 629 | 630 | "x sameType x.`takeWhile`(p)".law 631 | 632 | "x.`zip`(y).map(_._1) sameAs x.take(x.size min y.size)".law(BITSET_ZIP_AMBIG.!) 633 | 634 | "x.`zip`(y).map(_._2) sameAs y.take(x.size min y.size)".law(BITSET_ZIP_AMBIG.!) 635 | 636 | "x sameType x.`zip`(y).map(_._1)".law(MAP.!, ORDERLY.!) 637 | 638 | "x.`zipAll`(y, a, f(a)).map(_._1) sameAs x.`padTo`(x.`size` max y.size, a)".law 639 | 640 | "x.`size` < y.size implies x.`zipAll`(y, a, f(a)).map(_._1).`exists`(_ == a)".law 641 | 642 | "x.`zipAll`(y, a, f(a)).map(_._2) sameAs y.`padTo`(x.`size` max y.size, f(a))".law 643 | 644 | "x.`size` > y.size implies x.`zipAll`(y, a, f(a)).map(_._2).`exists`(_ == f(a))".law 645 | 646 | """ 647 | val z = x.`zipAll`(y, a, f(a)) 648 | val x0 = x.`++`(Vector.fill((y.size - x.`size`) max 0)(a)) 649 | val y0 = y.`++`(Vector.fill((x.`size` - y.size) max 0)(f(a))) 650 | z sameAs x0.`zip`(y0) 651 | """.law(SEQ) 652 | 653 | """ 654 | val xlen = x.`size` 655 | val ylen = y.`size` 656 | val zip = 657 | if (xlen == ylen) x.zip(y) 658 | else if (xlen < ylen) (x.`++`(Iterator.continually(a).take(ylen-xlen))).zip(y) 659 | else x.`zip`(y ++ Iterator.continually(f(a)).take(xlen-ylen)) 660 | x.`zipAll`(y, a, f(a)) sameAs zip 661 | """.law(SEQ) 662 | 663 | "x sameType x.`zipAll`(y, a, f(a)).map(_._1)".law(MAP.!, ORDERLY.!) 664 | 665 | "x.`zipWithIndex`.map(_._1) sameAs x".law 666 | 667 | "x.`zipWithIndex`.map(_._2) sameAs (0 until x.`size`)".law 668 | 669 | "x sameType x.`zipWithIndex`.map(_._1)".law(MAP.!, ORDERLY.!) 670 | 671 | /* 672 | // The :++ method does not actually exist 673 | "x.`:++`(y) sameAs x.`++`(y)".law 674 | */ 675 | 676 | "x.`++:`(y) sameAs y.`++`(x)".law(SEQ) 677 | 678 | "x.`++:`(y) sameType y.`++`(x)".law(SEQ) 679 | 680 | "x.`::`(a).`size` == x.size+1".law 681 | 682 | "x.`::`(a).`head` == a".law 683 | 684 | "x sameType x.`::`(a)".law 685 | 686 | "x.`+:`(a).`size` == x.size+1".law 687 | 688 | "x.`+:`(a).`head` == a".law 689 | 690 | "x sameType x.`+:`(a)".law 691 | 692 | "x.`:+`(a).`size` == x.size+1".law 693 | 694 | "x.`:+`(a).`last` == a".law 695 | 696 | "x sameType x.`:+`(a)".law 697 | 698 | "val s = x.`+`(a).`size` - x.size; 0 <= s && s <= 1".law 699 | 700 | "x.`+`(a).`contains`(a)".law(MAP.!) 701 | 702 | "x.`+`(a).`contains`(a._1)".law(MAP) 703 | 704 | "x sameType x.`+`(a)".law 705 | 706 | "x.`:::`(y) sameAs y.`++`(x)".law 707 | 708 | "x sameType x.`:::`(y)".law 709 | 710 | "x.`apply`(n) == x.`drop`(n).`head`".law(SEQ, Filt.xsize(_ > 0)) 711 | 712 | "x.`apply`(a) == x.`contains`(a)".law(SET) 713 | 714 | "tryO{ x.`apply`(a._1) } == x.`get`(a._1)".law(MAP) 715 | 716 | """ 717 | val n = x.`combinations`(r).size; 718 | ((r > x.size) implies (n == 0)) && x.combinations(r).toSet.size == n 719 | """.law(Filt.xsize(_ <= 8)) 720 | 721 | """r == 0 || r > x.size || { 722 | val s = x.toVector 723 | x.`combinations`(r).forall(_ partOf s) 724 | }""".law(Filt.xsize(_ <= 8)) 725 | 726 | """ 727 | x.`containsSlice`(y) implies ( 728 | y.`size` == 0 || 729 | ( Iterator.from(0). 730 | map(x.`drop`). 731 | takeWhile(_.`nonEmpty`). 732 | exists(_.`take`(y.`size`) sameAs y) 733 | ) 734 | ) 735 | """.law 736 | 737 | """ 738 | val List(xg,yg,xdyg) = 739 | List(x,y,x.`diff`(y)).map(_.`groupBy`(identity).mapValues(_.size).toMap) 740 | 741 | xg.forall{ case (k, n) => 742 | val m = n - yg.getOrElse(k,0) 743 | (m > 0) implies (xdyg.getOrElse(k,0) == m) 744 | } 745 | """.law 746 | 747 | """ 748 | val s = x.`toSet` 749 | x.`distinct`.`size` == s.size && x.forall(s) 750 | """.law 751 | 752 | "x.`dropRight`(nn) sameAs x.`take`(x.`size` - math.max(0, nn))".law(SET.!) 753 | 754 | "x sameType x.`reverse`".law 755 | 756 | "x.`endsWith`(y) == (x.`drop`(math.max(0, x.`size`-y.size)) sameAs y)".law 757 | 758 | "x.`groupBy`(g).keySet == x.map(g).toSet".law(BITSET_MAP_AMBIG.!) 759 | 760 | "x.`groupBy`(g).toMap.forall{ case (k,vs) => x.filter(xi => g(xi)==k) sameAs vs }".law 761 | 762 | "x.`nonEmpty` implies x.`take`(1).nonEmpty".law 763 | 764 | "x.`nonEmpty` implies { val h = x.`head`; x.`exists`(_ == h) }".law 765 | 766 | "x.`nonEmpty` implies x.`take`(1).`forall`(_ == x.`head`)".law(SEQ) 767 | 768 | "x.`headOption` == tryO{ x.`head` }".law(SEQ) 769 | 770 | """ 771 | x.`headOption` match { 772 | case None => x.`size` == 0 773 | case Some(y) => x.`exists`(_ == y) 774 | } 775 | """.law 776 | 777 | """ 778 | val k = x.`drop`(n).`takeWhile`(_ != a) 779 | x.`indexOf`(a,n) match { 780 | case -1 => k.`size` == (0 max x.size-n) 781 | case kk => n+k.size == kk && x.`drop`(kk).`head` == a 782 | } 783 | """.law(SEQ, Filt.xsize(_ > 0)) 784 | 785 | 786 | "x.`indexOf`(a) == x.`indexOf`(a, 0)".law 787 | 788 | """ 789 | val i = x.`indexOfSlice`(y) 790 | !x.`take`(math.max(0,i-1+y.size)).`containsSlice`(y) && ((i >= 0) implies (x.`drop`(i).`take`(y.size) sameAs y)) 791 | """.law(Filt.ysize(_ > 0)) 792 | 793 | """ 794 | val i = x.`indexOfSlice`(y, n) 795 | !x.`drop`(n).`take`(math.max(0,i-1-n+y.size)).`containsSlice`(y) && ((i >= 0) implies (i >= n && (x.`drop`(i).`take`(y.size) sameAs y))) 796 | """.law(Filt.ysize(_ > 0)) 797 | 798 | """ 799 | val k = x.`drop`(n).`takeWhile`(xi => !p(xi)) 800 | x.`indexWhere`(p,n) match { 801 | case -1 => k.`size` == (0 max x.size-n) 802 | case kk => n+k.size == kk && p(x.`drop`(kk).`head`) 803 | } 804 | """.law(Filt.xsize(_ > 0)) 805 | 806 | "x.`indexWhere`(p) == x.`indexWhere`(p, 0)".law 807 | 808 | """ 809 | val k = x.`takeWhile`(xi => !p(xi)).`size` 810 | x.`indexWhere`(p) match { 811 | case -1 => k == x.`size` 812 | case q => q == k 813 | } 814 | """.law 815 | 816 | "x.`indices` == (0 until x.`size`)".law 817 | 818 | "x.`size` > 0 implies (x.`init` sameAs x.`dropRight`(1))".law 819 | 820 | "x.`size` > 0 implies (x.`init`.size == x.size - 1)".law 821 | 822 | "x.`size` > 0 implies (x.`init` partOf x)".law 823 | 824 | """ 825 | x.`size` > 0 implies { 826 | val xx = x.`inits`.toVector.map(_.toVector) 827 | (xx zip xx.`tail`).`forall`{ 828 | case (a,b) => a.`size` - 1 == b.size && (b partOf a) 829 | } 830 | } 831 | """.law(SEQ, Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 832 | 833 | """ 834 | val xx = x.`inits`.toVector.map(_.toVector) 835 | (xx zip xx.`drop`(1)).`forall`{ case (a,b) => a.`init` sameAs b } 836 | """.law(SEQ, Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 837 | 838 | "x.`inits`.`size` == 1 + x.size".law(Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 839 | 840 | """ 841 | val xx = x.`inits`.toVector 842 | (xx zip xx.drop(1)).forall{ case (a, b) => a.`size` == b.`size` + 1 } 843 | """.law(Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 844 | 845 | """ 846 | val xx = x.`inits`.toVector 847 | (xx zip xx.drop(1)).forall{ case (a, b) => 848 | (b partOf a) && !(a partOf b) 849 | } 850 | """.law(Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 851 | 852 | "x.`intersect`(y).`toSet` == (x.toSet & y.toSet)".law 853 | 854 | "x sameType x.`intersect`(y)".law 855 | 856 | "x.`iterator` sameAs x".law 857 | 858 | "x.`size` > 0 implies (x.`takeRight`(1).`count`(_ == x.`last`) == 1)".law(SEQ) 859 | 860 | """ 861 | x.`lastIndexOf`(a,n) match { 862 | case -1 => x.`take`(n+1).`indexOf`(a) == -1 863 | case k => k == (n min x.`size`-1) - x.take(n+1).reverse.indexOf(a) 864 | } 865 | """.law 866 | 867 | 868 | "x.`lastIndexOf`(a) == x.lastIndexOf(a,x.`size`-1)".law 869 | 870 | """ 871 | y.`size` > 0 implies { 872 | val i = x.`lastIndexOfSlice`(y) 873 | !x.`drop`(math.max(0,i+1)).`containsSlice`(y) && 874 | ((i >= 0) implies (x.`drop`(i).`take`(y.size) sameAs y)) 875 | } 876 | """.law 877 | 878 | """ 879 | y.`size` > 0 implies { 880 | val i = x.`lastIndexOfSlice`(y,r) 881 | !x.`take`(r).`drop`(math.max(0,i+1)).`containsSlice`(y) && 882 | ((i >= 0) implies (i <= r && (x.`drop`(i).`take`(y.size) sameAs y))) 883 | } 884 | """.law 885 | 886 | """ 887 | x.`lastIndexWhere`(p,n) match { 888 | case -1 => x.`take`(n+1).`indexWhere`(p) == -1 889 | case k => k == (n min x.`size`-1) - x.take(n+1).reverse.indexWhere(p) 890 | } 891 | """.law 892 | 893 | "x.`lastIndexWhere`(p) == x.lastIndexWhere(p,x.`size`-1)".law 894 | 895 | "x.`lastOption` == tryO{ x.`last` }".law 896 | 897 | "x.`lengthCompare`(n).signum == (x.`size` compare n).signum".law 898 | 899 | """ 900 | val xx = x.map(_.toString) 901 | xx eq xx.`mapConserve`(identity) 902 | """.law 903 | 904 | """ 905 | val size = 906 | x.toVector.groupBy(identity). 907 | map{ case (_,vs) => vs.size }. 908 | scanLeft((x.size,0)){ (c,i) => (c._1 - c._2, i) }. 909 | map{ case (n,k) => (1L to k).map(n+1 - _).product / (1L to k).product }. 910 | product 911 | x.`permutations`.size == size 912 | """.law(Filt.xsize(_ <= 8)) // Gets WAY too big above 8! 913 | 914 | "x.`permutations`.size == x.`permutations`.toSet.size".law(Filt.xsize(_ <= 8)) // Too big above 8! 915 | 916 | """ 917 | val xs = x.toSet 918 | x.`permutations`.forall(_.forall(xs)) 919 | """.law(Filt.xsize(_ <= 8)) // Too big above 8! 920 | 921 | """x.`prefixLength`(p) == x.`takeWhile`(p).`size`""".law 922 | 923 | """ 924 | val ix = x.`toIndexedSeq` 925 | val k = x.`size` 926 | var ki = 0 927 | x.`reverse`.`forall`{ xi => ki += 1; xi == ix(k - ki) } 928 | """.law(SEQ) 929 | 930 | "x.`reverseIterator` sameAs x.`reverse`".law(SEQ) 931 | 932 | "x.`reverseMap`(f) sameAs x.`reverse`.map(f)".law(SEQ) 933 | 934 | "x.`reverseMap`(f) sameAs x.map(f).`reverse`".law(SEQ) 935 | 936 | "x sameType x.`reverseMap`(f)".law 937 | 938 | "x.`reverse_:::`(y) sameAs x.`:::`(y.`reverse`)".law(SEQ) 939 | 940 | "x sameType x.`reverse_:::`(y)".law 941 | 942 | "x.`scan`(a)((l, r) => op(l, r)) sameAs x.toList.scanLeft(a)((l, r) => op(l, r))".law(SET.!, MAP.!) 943 | 944 | """ 945 | val ab = collection.mutable.ArrayBuffer(a) 946 | x.foreach(xi => ab += (op(ab.last, xi))) 947 | x.`scanLeft`(a)((acc, xi) => op(acc, xi)) sameAs ab 948 | """.law(SET.!, MAP.!) 949 | 950 | "x.`scanRight`(a)((xi, acc) => op(acc, xi)) sameAs x.toList.reverse.scanLeft(a)((acc, xi) => op(acc, xi)).reverse".law(SET.!, MAP.!) 951 | 952 | """ 953 | val temp = x.`scan`("")((s,xi) => s.toString + xi.toString).toList.sortBy(_.toString.length) 954 | (temp zip temp.tail).forall{ case (a,b) => b.toString.startsWith(a.toString) } 955 | """.law 956 | 957 | """ 958 | x.`scan`("")((s,xi) => s.toString + xi.toString).toList. 959 | sortBy(_.toString.length).lastOption. 960 | forall{ yN => yN.toString.toSeq.groupBy(identity) == x.map(_.toString).mkString.toSeq.groupBy(identity) } 961 | """.law(BITSET_MAP_AMBIG.!) 962 | 963 | """ 964 | val temp = x.`scanLeft`(Set[A]())((s,xi) => s + xi).toList.sortBy(_.size) 965 | (temp zip temp.tail).forall{ case (a,b) => a subsetOf b} 966 | """.law 967 | 968 | "x.`scanLeft`(Set[A]())((s,xi) => s + xi).toList.sortBy(_.size).lastOption.forall(_ sameAs x.toSet)".law(SET) 969 | 970 | """ 971 | val temp = x.`scanRight`(Set[A]())((xi,s) => s + xi).toList.sortBy(_.size) 972 | (temp zip temp.tail).forall{ case (a,b) => a subsetOf b} && temp.lastOption.forall(_ sameAs x.toSet) 973 | """.law(SET) 974 | 975 | "x.`scanRight`(Set[A]())((xi,s) => s + xi).toList.sortBy(_.size).lastOption.forall(_ sameAs x.toSet)".law(SET) 976 | 977 | "n <= 0 || x.`segmentLength`(p,n) == x.`drop`(n).`takeWhile`(p).`size`".law 978 | 979 | "r <= 0 || x.`sliding`(r).`size` == (if (x.nonEmpty) math.max(0,x.`size`-r)+1 else 0)".law(QUEUE_SLIDE_11440.!) 980 | 981 | """ 982 | r <= 0 || 983 | x.`size` <= 0 || 984 | (r >= x.`size` && { x.`sliding`(r).map(_.toVector).toVector sameAs Vector(x.toVector) }) || 985 | { val vx = x.toVector; x.`sliding`(r).toVector.map(_.toVector) sameAs Vector.range(0, 1+vx.size-r).map(i => vx.slice(i,i+r)) } 986 | """.law(SEQ) 987 | 988 | """ 989 | var last: Option[A] = None 990 | val i = x.`sorted`.`iterator` 991 | var ordered = true 992 | while (ordered && i.hasNext) { 993 | val a = i.next() 994 | ordered = last.forall(_ <= a) 995 | last = Some(a) 996 | } 997 | ordered 998 | """.law 999 | 1000 | "val x0 = x; x0.`sortInPlace`(); x0 sameAs x.`sorted`".law 1001 | 1002 | "x.`sortBy`(f) sameAs x.`sortWith`((a,b) => f(a) < f(b))".law(SORTWITH_INT_CCE.!) 1003 | 1004 | "val x0 = x; x0.`sortInPlaceBy`(f); x0 sameAs x.`sortBy`(f)".law 1005 | 1006 | "val x0 = x; x0.`sortInPlaceWith`((a, b) => f(a) < f(b)); x0 sameAs x.`sortWith`((a, b) => f(a) < f(b))".law 1007 | 1008 | """ 1009 | val xx = x.`sortWith`((a,b) => f(a) > f(b)).toList 1010 | (xx zip xx.drop(1)).forall{ case (a,b) => !(f(a) < f(b)) } 1011 | """.law(SORTWITH_INT_CCE.!) 1012 | 1013 | "x.`sorted` sameAs x.`toArray`.sorted".law // Need to add a custom ordering here 1014 | 1015 | """ 1016 | val x0 = x.`sorted` 1017 | val lt_n = x0.`takeWhile`(_ < a).`size` 1018 | val lteq_n = x0.takeWhile(_ <= a).size 1019 | x0.`search`(a) match { 1020 | case scala.collection.Searching.Found(i) => i >= lt_n && i < lteq_n 1021 | case scala.collection.Searching.InsertionPoint(i) => i == lt_n && i == lteq_n 1022 | } 1023 | """.law 1024 | 1025 | """ 1026 | val (x1,x2) = x.`splitAt`(n) 1027 | (x1 sameAs x.`take`(n)) && (x2 sameAs x.`drop`(n)) 1028 | """.law 1029 | 1030 | """ 1031 | val (x1,x2) = x.`splitAt`(n) 1032 | (x1 sameType x2) && (x sameType x1) 1033 | """.law 1034 | 1035 | "x.`startsWith`(y,n) implies (x.`drop`(n).`take`(y.`size`) sameAs y)".law(Filt.xsize(_ > 0)) 1036 | 1037 | "x.`startsWith`(y) == x.startsWith(y,0)".law 1038 | 1039 | "xsize < 1 || { x.`tail` sameAs x.`drop`(1) }".law 1040 | 1041 | """ 1042 | x.`tails`.toList.map(_.toList) match { 1043 | case z => (z zip z.drop(1)).forall{ 1044 | case (a :: b :: _, c :: _) => b == c 1045 | case (a, b) => b.isEmpty && (a.isEmpty || a.size == 1) 1046 | } 1047 | } 1048 | """.law(SEQ, Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 1049 | 1050 | """ 1051 | val xtl = x.`tails`.toList 1052 | (xtl zip xtl.drop(1)).forall{ case (a,b) => (b subsetOf a) && b.size == a.size-1 } 1053 | """.law(SET, Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 1054 | 1055 | """ 1056 | val xtl = x.`tails`.toList 1057 | (xtl zip xtl.drop(1)).forall{ case (a,b) => (b.toSet subsetOf a.toSet) && b.size == a.size-1 } 1058 | """.law(SEQ.!, SET.!, Tags.SelectTag(ti => if (ti.inst.values.xsize <= 10) None else Some(Outcome.Skip.x))) 1059 | 1060 | "x.`takeRight`(n) sameAs { val m = x.`size` - math.max(0, n); x.`drop`(m) }".law 1061 | 1062 | "x sameType x.`takeRight`(n)".law 1063 | 1064 | """ 1065 | x.map(a => collectionFrom(Array.fill(n)(a))).`transpose`.`forall`(_ sameAs x) 1066 | """.law(ORDERLY.!, BITSET_MAP_AMBIG.!, Filt.xsize(_ > 1), Filt.n(_ > 0)) 1067 | 1068 | """ 1069 | x.`unsorted`.map(a => collectionFrom(Array.fill(n)(a))).`transpose`.`forall`(_ sameAs x) 1070 | """.law(ORDERLY, Filt.xsize(_ > 1), Filt.n(_ > 0)) 1071 | 1072 | "x.`union`(y).`toSet` == (x.toSet union y.toSet)".law 1073 | 1074 | "x sameType x.`union`(y)".law 1075 | 1076 | """ 1077 | val xa = x.`zip`(y).`unzip`._1.`toArray` 1078 | x.`take`(xa.size) sameAs xa 1079 | """.law(BITSET_ZIP_AMBIG.!) 1080 | 1081 | """ 1082 | val xb = x.`zip`(y).`unzip`._2.`toArray` 1083 | y.`take`(xb.size) sameAs xb 1084 | """.law(BITSET_ZIP_AMBIG.!) 1085 | 1086 | """ 1087 | x.map(a => (a,a)).`unzip` match { 1088 | case (y1, y2) => List(y1, y2).forall(_ sameAs x) 1089 | case _ => false 1090 | } 1091 | """.law(BITSET_MAP_AMBIG.!) 1092 | 1093 | """ 1094 | x.map(a => (a,a,a)).`unzip3` match { 1095 | case (y1, y2, y3) => List(y1, y2, y3).forall(_ sameAs x) 1096 | case _ => false 1097 | } 1098 | """.law(BITSET_MAP_AMBIG.!) 1099 | 1100 | """ 1101 | val v = Vector.newBuilder[A] 1102 | x.`withFilter`(p).foreach(v += _) 1103 | v.result() sameAs x.`filter`(p).toVector 1104 | """.law(SEQ) 1105 | 1106 | """ 1107 | val v = Vector.newBuilder[A] 1108 | x.`withFilter`(p).foreach(v += _) 1109 | v.result() samePieces x.`filter`(p).toVector 1110 | """.law(SEQ.!) 1111 | 1112 | """ 1113 | val z = x.`zip`(y).toArray 1114 | val k = math.min(x.`size`, y.size) 1115 | var i, j = -1 1116 | ( 1117 | x.`forall`{ xi => i += 1; i >= k || z(i)._1 == xi } && 1118 | y.`forall`{ yj => j += 1; j >= k || z(j)._2 == yj } 1119 | ) 1120 | """.law(SET.!, MAP.!) 1121 | 1122 | """ 1123 | val z = x.`zip`(y).toArray 1124 | ( 1125 | (z.map(_._1).toSet subsetOf x) && 1126 | (z.map(_._2).toSet subsetOf y) && 1127 | z.forall{ case (a,b) => (x contains a) && (y contains b) } 1128 | ) 1129 | """.law(SET, BITSET_ZIP_AMBIG.!) 1130 | 1131 | """ 1132 | val xSet = x.toSet 1133 | val ySet = y.toSet 1134 | val z = x.`zip`(y) 1135 | z.forall{ case (xi, yi) => xSet(xi) && ySet(yi) } 1136 | """.law(MAP) 1137 | 1138 | "x sameAs x.`view`".law 1139 | 1140 | "val x0 = x; x0.`++=`(y); x0 sameAs (x.`++`(y))".law 1141 | 1142 | "val x0 = x; x0.`++=:`(y); x0 sameAs (y.`++`(x))".law 1143 | 1144 | "val x0 = x; x0.`+=`(a); x0 sameAs (x.`:+`(a))".law 1145 | 1146 | "val x0 = x; x0.`+=`(a); x0 sameAs (x.`+`(a))".law 1147 | 1148 | "val x0 = x; x0.`+=:`(a); x0 sameAs (x.`+:`(a))".law 1149 | 1150 | "(x.`-`(a)).`count`(_ == a) == (0 max x.count(_ == a) - 1)".law(MAP.!) 1151 | 1152 | "(x.`-`(a._1)).`count`(_ == a) == 0".law(MAP) 1153 | 1154 | "(x.`-`(a)).`size` == x.size - (if (x.`contains`(a)) 1 else 0)".law(MAP.!) 1155 | 1156 | "(x.`-`(a._1)).`size` == x.size - (if (x.`contains`(a._1)) 1 else 0)".law(MAP) 1157 | 1158 | "(x.`-`(a)) partOf x".law(MAP.!) 1159 | 1160 | "(x.`-`(a._1)) partOf x".law(MAP) 1161 | 1162 | "x sameType x.`-`(a)".law(MAP.!) 1163 | 1164 | "x sameType x.`-`(a._1)".law(MAP) 1165 | 1166 | "val x0 = x; x0.`-=`(a) sameAs x.`-`(a)".law(MAP.!) 1167 | 1168 | "val x0 = x; x0.`-=`(a._1) sameAs x.`-`(a._1)".law(MAP) 1169 | 1170 | "val x0 = x; x0.`--=`(y); val x1 = x; y.foreach(yi => x1.`-=`(yi)); x0 sameAs x1".law(MAP.!) 1171 | 1172 | "val x0 = x; x0.`--=`(y.map(_._1)); val x1 = x; for ((k,_) <- y) { x1.`-=`(k) }; x0 sameAs x1".law(MAP) 1173 | 1174 | "{ val z = x; z.`append`(a); z } == x.`+=`(a)".law 1175 | 1176 | "{ val z = x; z.`appendAll`(y); z } == x.`++=`(y)".law 1177 | 1178 | "{ val z = x; z.`prepend`(a); z } == x.`+=:`(a)".law 1179 | 1180 | "{ val z = x; z.`prependAll`(y); z } == x.`++=:`(y)".law 1181 | 1182 | """ 1183 | tryE{ val x0 = x; x0.`remove`(n, nn); x0 } match { 1184 | case Left(_: IllegalArgumentException) => nn < 0 1185 | case Left(_: IndexOutOfBoundsException) => n > (x.`size` - nn) 1186 | case Right(x0) => x0 sameAs (x.`take`(n).`++`(x.`drop`(n+(0 max nn)))) 1187 | case _ => false 1188 | } 1189 | """.law(SET.!, MAP.!, Filt.xsize(_ > 0)) 1190 | 1191 | """ 1192 | val x0 = x 1193 | val a0 = x0.`remove`(n) 1194 | x0.`size` == x.`size`-1 && x.`apply`(n) == a0 1195 | """.law(SET.!, MAP.!, Filt.xsize(_ > 0)) 1196 | 1197 | """ 1198 | val x0, x1 = x 1199 | x0.`remove`(n) 1200 | x1.`remove`(n,1) 1201 | x0 sameAs x1 1202 | """.law(SET.!, MAP.!, Filt.xsize(_ > 0)) 1203 | 1204 | "x.`filterKeys`(k => p((k, a._2))) sameAs x.toVector.filter{ case (k, v) => p((k, a._2)) }.toMap".law 1205 | 1206 | "x.`keySet` sameAs x.`iterator`.map{ case (k, v) => k }.toSet".law 1207 | 1208 | "x.`keySet` sameAs x.`keys`".law 1209 | 1210 | "x.`keysIterator`.toSet sameAs x.`keySet`".law 1211 | 1212 | "x.`values` samePieces x.`iterator`.map{ case(k, v) => v }.toVector".law 1213 | 1214 | "x.`valuesIterator`.toVector samePieces x.`iterator`.map{ case (k, v) => v }.toVector".law 1215 | 1216 | "(!x.`contains`(a._1) && !y.`contains`(a._1)) implies x.`withDefaultValue`(a._2).apply(a._1) == a._2".law 1217 | 1218 | /* 1219 | "x0.`remove`(a) == x0.`contains`(a)".law(SET) 1220 | 1221 | "x0.`remove`(a._1) == x0.`contains`(a._1)".law(MAP) 1222 | 1223 | """ 1224 | val x0 = x 1225 | val had = x0.`remove`(a) 1226 | if (had) (x0.`+`(a)) sameAs x else x0 sameAs x 1227 | """.law(SET) 1228 | 1229 | """ 1230 | val x0 = x 1231 | val had = x0.`remove`(a._1) 1232 | if (had) x0.`size` + 1 == x.`size` && (x0 partOf x) else x0 sameAs x 1233 | """.law(MAP) 1234 | */ 1235 | 1236 | 1237 | 1238 | "x.`result`() sameAs x".law 1239 | 1240 | "val x0 = x; x0.`transform`(f); x0 sameAs x.map(f)".law(MAP.!) 1241 | 1242 | """ 1243 | val x0 = x 1244 | x0.`transform`((a, b) => f((a, b))._2) 1245 | x0 sameAs x.map{ case (a, b) => a -> f((a, b))._2 } 1246 | """.law(MAP) 1247 | 1248 | "x.`updated`(n,a) sameAs (x.`take`(n).`:+`(a).`++`(x.`drop`(n+1)))".law(SEQ, Filt.xsize(_ > 0)) 1249 | 1250 | "x.`updated`(a._1, a._2).`forall`{ case (k,v) => if (k == a._1) v == a._2 else x.`get`(k).exists(_ == v) }".law(MAP) 1251 | 1252 | "x sameType x.`updated`(n,a)".law(SEQ, Filt.xsize(_ > 0)) 1253 | 1254 | "x sameType x.`updated`(a._1, a._2)".law(MAP, Filt.xsize(_ > 0)) 1255 | 1256 | "{ val x0 = x; x0.`update`(n, a); x0 sameAs x.`updated`(n,a) }".law(MAP.!, Filt.xsize(_ > 0)) 1257 | 1258 | "{ val x0 = x; x0.`update`(a._1, a._2); x0 sameAs x.`updated`(a._1, a._2) }".law(MAP) 1259 | 1260 | "x.`&`(y) partOf x".law 1261 | 1262 | "x.`forall`(xi => y.`contains`(xi) == x.`&`(y).contains(xi))".law 1263 | 1264 | "x sameType x.`&`(y)".law 1265 | 1266 | "x.`&~`(y) partOf x".law 1267 | 1268 | "x.`forall`(xi => y.`contains`(xi) != x.`&~`(y).contains(xi))".law 1269 | 1270 | "x sameType x.`&~`(y)".law 1271 | 1272 | "x.`^`(y) == x.`xor`(y)".law 1273 | 1274 | "val z = x.`|`(y); x.`forall`(z contains _) && y.`forall`(z contains _)".law 1275 | 1276 | "val z = x.`|`(y); z.`forall`(i => x(i) || y(i))".law 1277 | 1278 | 1279 | /////////////////////////////////////// 1280 | // New laws for strawman collections // 1281 | /////////////////////////////////////// 1282 | 1283 | "(x.`toVector` sameAs x.toVector.reverse) == (x.iterator sameAs x.toVector.reverse.iterator)".law(INSORD) 1284 | 1285 | "(!x.`contains`(a._1) && !y.contains(a._1)) implies (x.`+`(a).`++`(y).iterator.toVector.`drop`(x.`size`).headOption == Some(a))".law(INSORD, MAP) 1286 | 1287 | "x.`:++`(y) sameAs x.`++`(y)".law 1288 | 1289 | "val x0 = x; val x1 = x; x0.`addOne`(a); x1.`+=`(a); x0 sameAs x1".law 1290 | 1291 | "val x0 = x; val x1 = x; x0.`addAll`(y); x1.`++=`(y); x0 sameAs x1".law 1292 | 1293 | "x.`appended`(a) sameAs x.`:+`(a)".law 1294 | 1295 | "x.`appendedAll`(y) sameAs x.`:++`(y)".law 1296 | 1297 | "val x0 = x; x0.`clear`(); x0.`size` == 0".law 1298 | 1299 | "x.`concat`(y) sameAs x.`++`(y)".law 1300 | 1301 | "x.`distinctBy`(f).`size` == x.`map`(f).`toSet`.size".law 1302 | 1303 | """ 1304 | val direct = x.`distinctBy`(f) 1305 | val built = Array.newBuilder[A] 1306 | val check = collection.mutable.HashSet.empty[A] 1307 | x.foreach{ xi => 1308 | val key = f(xi) 1309 | if (!check(key)) { 1310 | check += key 1311 | built += xi 1312 | } 1313 | } 1314 | direct samePieces built.result() 1315 | """.law 1316 | 1317 | "val x0 = x; x0.`dropInPlace`(n max 0); x0 sameAs x.`drop`(n)".law 1318 | 1319 | "val x0 = x; x0.`dropRightInPlace`(n max 0); x0 sameAs x.`dropRight`(n)".law 1320 | 1321 | "val x0 = x; x0.`dropWhileInPlace`(p); x0 sameAs x.`dropWhile`(p)".law 1322 | 1323 | "val x0 = x; x0.`filterInPlace`(p); x0 sameAs x.`filter`(p)".law(MAP.!) 1324 | 1325 | "val x0 = x; x0.`filterInPlace`((k, v) => p((k, v))); x0 sameAs x.`filter`(p)".law(MAP) 1326 | 1327 | "val x0 = x; x0.`flatMapInPlace`(xi => y.toList.take(intFrom(xi))); x0 sameAs x.`flatMap`(xi => y.toList.take(intFrom(xi)))".law 1328 | 1329 | "x.`groupMap`(f)(g).map(_._2.size).sum == x.`size`".law 1330 | 1331 | """ 1332 | val m = collection.mutable.HashMap.empty[A, List[B]] 1333 | x.foreach{ xi => val key = f(xi); m(key) = g(xi) :: m.getOrElse(key, Nil) } 1334 | val gm = x.`groupMap`(f)(g) 1335 | m.size == gm.size && 1336 | m.forall{ case (k, vs) => gm(k) sameAs vs.reverse } && 1337 | gm.forall{ case (k, vs) => m(k).reverse sameAs vs } 1338 | """.law 1339 | 1340 | "x.`groupMap`(g)(f).map{ case (k, vs) => k -> vs.`reduceLeft`(op) } samePieces x.`groupMapReduce`(g)(f)(op)".law(Filt.sym) 1341 | 1342 | "val x0 = x; x0.`insert`(n max 0, a); x0 sameAs collectionFrom(x.toArray.patch(n max 0, Array(a), 0))".law(MAP.!, SET.!) 1343 | 1344 | "val x0 = x; val x1 = x; x0.`insertAll`(n max 0, y); y.`reverse`.foreach(yi => x1.`insert`(n max 0, yi)); x0 sameAs x1".law(MAP.!) 1345 | 1346 | "val x0 = x; x0.`mapInPlace`(f); x0 sameAs x.`map`(f)".law(PQ_MIP_NPE_11439.!) 1347 | 1348 | "val x0 = x; x0.`mapValuesInPlace`((k: K, v: V) => f((k, v))._2); x0 sameAs x.`map`{ case (k, v) => k -> f((a._1, v))._2 }".law(MAP) 1349 | 1350 | "x.`mapValues`(v => f((a._1, v))._2).toMap sameAs x.`map`{ case (k, v) => k -> f((a._1, v))._2 }".law(MAP) 1351 | 1352 | "val x0 = x; x0.`padToInPlace`(n, a); x0 sameAs x.`padTo`(n, a)".law 1353 | 1354 | "val x0 = x; x0.`patchInPlace`(n max 0, y, m); x0 sameAs x.`patch`(n max 0, y, m)".law(LISTBUF_PIP_11438.!) 1355 | 1356 | "x.`prepended`(a) sameAs x.`+:`(a)".law 1357 | 1358 | "x.`prependedAll`(y) sameAs x.`++:`(y)".law 1359 | 1360 | "m > x.`size` || n < 0 || m < 0 || { val x0 = x; x0.`sliceInPlace`(n, m); x0 sameAs x.`slice`(n, m) }".law 1361 | 1362 | "val x0 = x; x0.`subtractOne`(a); x0.`size` == xsize - (if (x.`contains`(a)) 1 else 0)".law(MAP.!) 1363 | 1364 | "val x0 = x; x0.`subtractOne`(a); x0 sameAs (x.`takeWhile`(_ != a) ++ x.`dropWhile`(_ != a).drop(1))".law(MAP.!) 1365 | 1366 | "val x0 = x; val x1 = x; x0.`subtractAll`(y); y.foreach(yi => x1.`subtractOne`(yi)); x0 sameAs x1".law(MAP.!) 1367 | 1368 | "val x0 = x; x0.`takeInPlace`(n max 0); x0 sameAs x.`take`(n)".law 1369 | 1370 | "val x0 = x; x0.`takeRightInPlace`(n max 0); x0 sameAs x.`takeRight`(n)".law 1371 | 1372 | "val x0 = x; x0.`takeWhileInPlace`(p); x0 sameAs x.`takeWhile`(p)".law 1373 | 1374 | "val x0 = x; val x1 = x; if (n >= 0) { x0.`trimStart`(n); x1.`dropInPlace`(n) }; x0 sameAs x1".law 1375 | 1376 | "val x0 = x; val x1 = x; if (n >= 0) { x0.`trimEnd`(n); x1.`dropRightInPlace`(n) }; x0 sameAs x1".law 1377 | 1378 | "x.`knownSize` match { case n if n >= 0 => n == x.`size`; case _ => true }".law 1379 | 1380 | 1381 | ///////////////////////////// 1382 | // End of individual laws. // 1383 | // Still inside Laws // 1384 | // object. Normal // 1385 | // indentation resumes. // 1386 | ///////////////////////////// 1387 | 1388 | /** Complete listing of laws (including disabled ones that aren't used for code generation) */ 1389 | val complete = b.result() 1390 | 1391 | /** Complete listing of laws that are not disabled */ 1392 | val all = complete.filterNot(_.disabled) 1393 | 1394 | /** Map from line numbers to the corresponding law. */ 1395 | lazy val byLineNumber = complete.map(law => law.lineNumber -> law).toMap 1396 | } 1397 | -------------------------------------------------------------------------------- /laws/src/main/scala/Numbers.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /** Sets of integer values used to check individual cases in certain tests. 4 | * 5 | * In general, we expect but do not enforce here that `n` and `m` are positive 6 | * and bounded by the size of collections `x` and `y`, and that `nn` and `mm` 7 | * are both positive. `r` can be anything. 8 | * 9 | * Do _not_ set `used` yourself! Just pass it in to `Explore` 10 | */ 11 | final class Numbers(n0: Int, nn0: Int, m0: Int, mm0: Int, r0: Int) { 12 | val values = Numbers.Values(n0, nn0, m0, mm0, r0) 13 | 14 | val used = Array(false, false, false, false, false) 15 | 16 | /** A value that can be used to index a collection `x` (-1 if the collection is empty) */ 17 | def n: Int = { used(0) = true; values.n } 18 | 19 | /** A positive integer */ 20 | def nn: Int = { used(1) = true; values.nn } 21 | 22 | /** A value that can be used to index a collection `y` (-1 if the collection is empty) */ 23 | def m: Int = { used(2) = true; values.m } 24 | 25 | 26 | def mm: Int = { used(3) = true; values.mm } 27 | def r: Int = { used(4) = true; values.r } 28 | 29 | def setUnused(): this.type = { java.util.Arrays.fill(used, false); this } 30 | 31 | def touched = used(0) || used(1) || used(2) || used(3) || used(4) 32 | 33 | override def equals(that: Any) = that match { 34 | case nm: Numbers => values == nm.values 35 | case _ => false 36 | } 37 | 38 | override def hashCode = values.hashCode 39 | 40 | override def toString = f"Numbers: ${values.n}, ${values.nn}, ${values.m}, ${values.mm}, ${values.r}" 41 | } 42 | object Numbers { 43 | /** The underlying, unchecked values whose usage is also not monitored */ 44 | final case class Values(n: Int, nn: Int, m: Int, mm: Int, r: Int) {} 45 | 46 | /** The equivalent to "empty"--the most boring set of numbers */ 47 | val simplest: Numbers = new Numbers(0, 0, 0, 0, 0) 48 | 49 | val possible_n = Array(0, 1, 2, 3, 8, 9, 15, 17, 30, 31, 32, 47, 49, 50, 132, 5100, 5102) 50 | val possible_nn = possible_n 51 | val possible_m = possible_n 52 | val possible_mm = possible_n 53 | val possible_r = Array(0, 1, 2, 5, 7, 8, 9, 15, 16, 17, 31, 59, 132, 5101, 91347, -1, -24, -1001) 54 | 55 | /** Directly creates an instance of `Numbers`. You should probably use a `Restricted` instead 56 | * to limit the values of `n` and `m` by the collection instance. 57 | */ 58 | def apply(n: Int, nn: Int, m: Int, mm: Int, r: Int): Numbers = new Numbers(n, nn, m, mm, r) 59 | 60 | /** Explores number parameter space but restricts based upon the size of the 61 | * `x` and `y` collections in use. 62 | */ 63 | class Restricted(xsize: Int, ysize: Int) 64 | extends Exploratory[Numbers] { 65 | val actual_n = possible_n.takeWhile(_ < xsize) match { case ns if ns.length == 0 => Array(-1); case ns => ns } 66 | val actual_m = possible_m.takeWhile(_ < ysize) match { case ms if ms.length == 0 => Array(-1); case ms => ms } 67 | 68 | val sizes = Array(actual_n.length, possible_nn.length, actual_m.length, possible_mm.length, possible_r.length) 69 | 70 | def lookup(ixs: Array[Int]): Option[Numbers] = 71 | if (!validate(ixs)) None 72 | else Some(Numbers(actual_n(ixs(0)), possible_nn(ixs(1)), actual_m(ixs(2)), possible_mm(ixs(3)), possible_r(ixs(4)))) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /laws/src/main/scala/Ops.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /********************************************************************** 4 | ** This file contains the operations to apply to collection tests of ** 5 | ** various types. That way you can plug in different operations. ** 6 | **********************************************************************/ 7 | 8 | 9 | ///////////////////////////////////////////////////////////////// 10 | // Location-logging wrapper classes for various function forms // 11 | ///////////////////////////////////////////////////////////////// 12 | 13 | /** Wrapper class around a function that lets you tell where it came from */ 14 | class ===>[X, Y](val fn: X => Y)(implicit file: sourcecode.File, line: sourcecode.Line, nm: sourcecode.Name) 15 | extends Named { 16 | def name = nm.value.toString 17 | override val toString = nm.value.toString + " @ " + Sourced.implicitly 18 | override def equals(that: Any) = that match { 19 | case x: ===>[_, _] => toString == x.toString 20 | case _ => false 21 | } 22 | override val hashCode = scala.util.hashing.MurmurHash3.stringHash(toString) 23 | } 24 | 25 | /** Wrapper class around a binary operation that lets you tell where it came from, and whether you 26 | * expect it to be associative or symmetric (i.e. commutative). No effort is made to check whether 27 | * the operation actually is associative or symmetric. 28 | */ 29 | class OpFn[X]( 30 | val ofn: (X, X) => X, val zero: Option[X], val assoc: OpFn.Associativity, val sym: OpFn.Symmetry 31 | )(implicit file: sourcecode.File, line: sourcecode.Line, nm: sourcecode.Name) 32 | extends Named { 33 | def name = nm.value.toString 34 | override val toString = nm.value.toString + " @ " + Sourced.implicitly 35 | override def equals(that: Any) = that match { 36 | case x: OpFn[_] => toString == x.toString 37 | case _ => false 38 | } 39 | override val hashCode = scala.util.hashing.MurmurHash3.stringHash(toString) 40 | } 41 | object OpFn { 42 | sealed trait Associativity {} 43 | final case object Associative extends Associativity {} 44 | final case object Nonassociative extends Associativity {} 45 | sealed trait Symmetry {} 46 | final case object Symmetric extends Symmetry {} 47 | final case object Asymmetric extends Symmetry {} 48 | } 49 | 50 | /** Wrapper class around a partial function that lets you tell where it came from */ 51 | class ParFn[X](val pfn: PartialFunction[X, X])(implicit file: sourcecode.File, line: sourcecode.Line, nm: sourcecode.Name) 52 | extends Named { 53 | def name = nm.value.toString 54 | override val toString = nm.value.toString + " @ " + Sourced.implicitly 55 | override def equals(that: Any) = that match { 56 | case x: ParFn[_] => toString == x.toString 57 | case _ => false 58 | } 59 | override val hashCode = scala.util.hashing.MurmurHash3.stringHash(toString) 60 | } 61 | 62 | 63 | /////////////////////////////////////////////// 64 | // The main class holding all the operations // 65 | /////////////////////////////////////////////// 66 | 67 | /** Class that represents the ways we can transform and select data for a given element type. 68 | * 69 | * Do _not_ set `used` yourself! Just pass it in to `Explore` 70 | */ 71 | final class Ops[A, B](f0: A ===> A, g0: A ===> B, op0: OpFn[A], p0: A ===> Boolean, pf0: ParFn[A]) { 72 | val values = Ops.Values[A, B](f0, g0, op0, p0, pf0) 73 | 74 | val used = Array(false, false, false, false, false) 75 | 76 | /** A function that changes an element to another of the same type */ 77 | def f: A => A = { used(0) = true; values.f.fn } 78 | 79 | /** A function that changes an element to another of a different type */ 80 | def g: A => B = { used(1) = true; values.g.fn } 81 | 82 | /** A function that, given two elements of a type, produces a single element of that type */ 83 | def op: (A, A) => A = { used(2) = true; values.op.ofn } 84 | 85 | /** A predicate that gives a true/false answer for an element */ 86 | def p: A => Boolean = { used(3) = true; values.p.fn } 87 | 88 | /** A partial function that changes some elements to another of the same type */ 89 | def pf: PartialFunction[A, A] = { used(4) = true; values.pf.pfn } 90 | 91 | /** The zero of `op`; throws an exception if there is no zero. Filter out tests using `z` with Law#filter! 92 | * 93 | * Note: we're doing it this way since it's not practical to instrument the usage of something inside `Option`. 94 | */ 95 | def z: A = { used(4) = true; op0.zero.get } 96 | 97 | def setUnused(): this.type = { java.util.Arrays.fill(used, false); this } 98 | 99 | def touched = used(0) || used(1) || used(2) || used(3) || used(4) 100 | 101 | override def equals(that: Any) = that match { 102 | case o: Ops[_, _] => values == o.values 103 | case _ => false 104 | } 105 | 106 | override def hashCode = values.hashCode 107 | 108 | override lazy val toString = { 109 | val parts = Array(values.f.toString, values.g.toString, values.op.toString, values.p.toString, values.pf.toString) 110 | val pad = parts.map(_.indexOf('@')).max 111 | val paddedParts = 112 | if (pad < 0) parts 113 | else parts.map{ s => 114 | val i = s.indexOf('@') 115 | if (i <= 0 || i >= pad) s 116 | else s.take(i-1) + (" "*(pad - i)) + s.drop(i-1) 117 | } 118 | paddedParts.mkString("Ops\n ", "\n ", "") 119 | } 120 | } 121 | object Ops { 122 | /** Contains functions to use in tests but without any instrumentation for usage. */ 123 | final case class Values[A, B](f: A ===> A, g: A ===> B, op: OpFn[A], p: A ===> Boolean, pf: ParFn[A]) { self => 124 | object unwrap { 125 | def f = self.f.fn 126 | def g = self.g.fn 127 | def op = self.op.ofn 128 | def p = self.p.fn 129 | def pf = self.pf.pfn 130 | } 131 | } 132 | 133 | def apply[A, B](f: A ===> A, g: A ===> B, op: OpFn[A], p: A ===> Boolean, pf: ParFn[A]): Ops[A, B] = new Ops(f, g, op, p, pf) 134 | 135 | object IntFns extends Variants[Int ===> Int] { 136 | val plusOne = this has new Item(_ + 1) 137 | val quadratic = this has new Item(i => i*i - 3*i + 1) 138 | } 139 | 140 | object StrFns extends Variants[String ===> String] { 141 | val upper = this has new Item(_.toUpperCase) 142 | val fishy = this has new Item(s => f"<$s-<") 143 | } 144 | 145 | object IntToLongs extends Variants[Int ===> Long] { 146 | val bit33 = this has new Item(i => 0x200000000L | i) 147 | val cast = this has new Item(i => i.toLong) 148 | } 149 | 150 | object StrToOpts extends Variants[String ===> Option[String]] { 151 | val natural = this has new Item(s => Option(s).filter(_.length > 0)) 152 | val letter = this has new Item(s => Option(s.filter(_.isLetter)).filter(_.length > 0)) 153 | } 154 | 155 | object IntOpFns extends Variants[OpFn[Int]] { 156 | val summation = this has new Item(_ + _, Some(0), OpFn.Associative, OpFn.Symmetric) 157 | val multiply = this has new Item((i, j) => i*j - 2*i - 3*j + 4, None, OpFn.Nonassociative, OpFn.Asymmetric) 158 | } 159 | 160 | object StrOpFns extends Variants[OpFn[String]] { 161 | val concat = this has new Item(_ + _, Some(""), OpFn.Associative, OpFn.Asymmetric) 162 | val interleave = this has new Item((s, t) => (s zip t).map{ case (l,r) => f"$l$r" }.mkString, None, OpFn.Nonassociative, OpFn.Asymmetric) 163 | } 164 | 165 | object IntPreds extends Variants[Int ===> Boolean] { 166 | val mod3 = this has new Item(i => (i%3) == 0) 167 | val always = this has new Item(_ => true) 168 | val never = this has new Item(_ => false) 169 | } 170 | 171 | object StrPreds extends Variants[String ===> Boolean] { 172 | val increasing = this has new Item(s => s.length < 2 || s(0) <= s(s.length-1)) 173 | val always = this has new Item(_ => true) 174 | val never = this has new Item(_ => false) 175 | } 176 | 177 | object IntParts extends Variants[ParFn[Int]] { 178 | val halfEven = this has new Item({ case x if (x % 2) == 0 => x / 2 }) 179 | val identical = this has new Item({ case x => x }) 180 | val uninhabited = this has new Item(Function.unlift((i: Int) => None: Option[Int])) 181 | } 182 | 183 | object StrParts extends Variants[ParFn[String]] { 184 | val oddMirror = this has new Item({ case x if (x.length % 2) == 1 => x.reverse }) 185 | val identical = this has new Item({ case x => x }) 186 | val uninhabited = this has new Item(Function.unlift((s: String) => None: Option[String])) 187 | } 188 | 189 | /** Steps through varitions of operations that can be applied to collections */ 190 | class Explorer[A, B]( 191 | varFns: Variants[A ===> A], 192 | varToBs: Variants[A ===> B], 193 | varOpFns: Variants[OpFn[A]], 194 | varPreds: Variants[A ===> Boolean], 195 | varParts: Variants[ParFn[A]] 196 | ) 197 | extends Exploratory[Ops[A, B]] { 198 | val sizes = Array(varFns.all.length, varToBs.all.length, varOpFns.all.length, varPreds.all.length, varParts.all.length) 199 | 200 | def lookup(ixs: Array[Int]): Option[Ops[A, B]] = 201 | if (!validate(ixs)) None 202 | else Some(Ops(varFns.index(ixs(0)), varToBs.index(ixs(1)), varOpFns.index(ixs(2)), varPreds.index(ixs(3)), varParts.index(ixs(4)))) 203 | } 204 | 205 | object IntExplorer extends Explorer[Int, Long](IntFns, IntToLongs, IntOpFns, IntPreds, IntParts) {} 206 | 207 | object StrExplorer extends Explorer[String, Option[String]](StrFns, StrToOpts, StrOpFns, StrPreds, StrParts) {} 208 | 209 | /** Really simple (non-diverse) set of operations for maps with `Long` keys. 210 | * 211 | * Be careful to avoid `Long` operations that might make keys collide! 212 | * Tests are generally written assuming that there won't be collsions after mapping. 213 | */ 214 | object LongStrExplorer extends Explorer[(Long, String), (String, Long)]( 215 | new Variants[(Long, String) ===> (Long, String)] { private[this] val inc1 = this has new Item(kv => (kv._1+1, kv._2)) }, 216 | new Variants[(Long, String) ===> (String, Long)] { private[this] val swap = this has new Item(kv => (kv._2, kv._1)) }, 217 | new Variants[OpFn[(Long, String)]] { private[this] val sums = this has new Item((kv, cu) => (kv._1 + cu._1, kv._2 + cu._2), None, OpFn.Nonassociative, OpFn.Asymmetric) }, 218 | new Variants[(Long, String) ===> Boolean] { private[this] val high = this has new Item(kv => kv._1 > kv._2.length) }, 219 | new Variants[ParFn[(Long, String)]] { private[this] val akin = this has new Item({ case (k, v) if ((k ^ v.length) & 1) == 0 => (k-2, v) })} 220 | ){} 221 | 222 | /** Really simple (non-diverse) set of operations for maps with `String` keys. 223 | * 224 | * Be careful to avoid string operations that might make keys collide! 225 | * Tests are generally written assuming that there won't be collsions after mapping. 226 | */ 227 | object StrLongExplorer extends Explorer[(String, Long), (Long, String)]( 228 | new Variants[(String, Long) ===> (String, Long)] { private[this] val dots = this has new Item(kv => (kv._1 + "..", kv._2)) }, 229 | new Variants[(String, Long) ===> (Long, String)] { private[this] val swap = this has new Item(kv => (kv._2, kv._1)) }, 230 | new Variants[OpFn[(String, Long)]] { private[this] val sums = this has new Item((kv, cu) => (kv._1 + cu._1, kv._2 + cu._2), None, OpFn.Nonassociative, OpFn.Asymmetric) }, 231 | new Variants[(String, Long) ===> Boolean] { private[this] val high = this has new Item(kv => kv._1.length < kv._2) }, 232 | new Variants[ParFn[(String, Long)]] { private[this] val akin = this has new Item({ case (k, v) if ((k.length ^ v) & 1) == 0 => (k + "!", v) })} 233 | ){} 234 | } -------------------------------------------------------------------------------- /laws/src/main/scala/Outcome.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /** Marker trait for results of running a test of a law */ 4 | sealed trait Outcome { 5 | def hasTest: Boolean = false 6 | def hasException: Boolean = false 7 | } 8 | 9 | /** Handles results of tests including, most importantly, 10 | * capturing relevant information about failed tests. 11 | */ 12 | object Outcome { 13 | /** Takes the deepest part of the stack trace up to the point where it hits a test */ 14 | def partialStackTrace(error: Throwable, prefix: String = ""): String = { 15 | val st = error.getStackTrace 16 | if (st.isEmpty || st.forall(_.toString.trim.isEmpty)) { 17 | val e2 = error.getCause 18 | if (e2 == null) prefix + error.toString + "\n(no stack trace information)\n" 19 | else partialStackTrace(e2, prefix + error.toString + " because\n") 20 | } 21 | else { 22 | val part = st.reverse.dropWhile{ elt => 23 | val s = elt.toString 24 | !s.startsWith("laws.") || s.contains("AllRunner") || s.contains("Test_Everything") || s.contains("runAll") 25 | } 26 | if (part.length == st.length) st.mkString(prefix + error.toString + "\n", "\n", "\n") 27 | else part.reverse.mkString(prefix + error.toString + "\n", "\n", "\n...\n") 28 | } 29 | } 30 | 31 | /** Indicates that a law passed a test. */ 32 | final case object Success extends Outcome {} 33 | 34 | /** Indicates that the test values were not appropriate for this law, 35 | * and suggests what value to change to try to make them appropriate. 36 | * 37 | * These handle things that can only be known at runtime because not 38 | * _all_ possible combinations of values are out of bounds. 39 | */ 40 | sealed abstract class Skip extends Outcome { def accessed: Array[Boolean] } 41 | object Skip { 42 | /** Indicates that the `Number` values were inappropriate for this law. */ 43 | final class Num(n: Boolean, nn: Boolean, m: Boolean, mm: Boolean, r: Boolean) extends Skip { 44 | val accessed = Array(n, nn, m, mm, r) 45 | } 46 | object Num { 47 | def unapply(num: Num): Option[Array[Boolean]] = Some(num.accessed) 48 | } 49 | 50 | /** Indicates that the functions/operators were inappropriate for this law. */ 51 | final class Oper(f: Boolean, g: Boolean, op: Boolean, p: Boolean, pf: Boolean) extends Skip { 52 | val accessed = Array(f, g, op, p, pf) 53 | } 54 | object Oper { 55 | def unapply(oper: Oper): Option[Array[Boolean]] = Some(oper.accessed) 56 | } 57 | 58 | /** Indicates that the particular element or collection was inappropriate for this law. 59 | * 60 | * Note that this is normally taken care of using flags, so the test isn't even generated. 61 | */ 62 | final class Inst(a: Boolean, x: Boolean, y: Boolean) extends Skip { 63 | val accessed = Array(a, x, y) 64 | } 65 | object Inst { 66 | def unapply(inst: Inst): Option[Array[Boolean]] = Some(inst.accessed) 67 | } 68 | 69 | /** Vary `Numbers`' `n` parameter to try to find an acceptable value */ 70 | val n = new Num(true, false, false, false, false) 71 | 72 | /** Vary `Numbers`' `nn` parameter to try to find an acceptable value */ 73 | val nn = new Num(false, true, false, false, false) 74 | 75 | /** Vary `Numbers`' `m` parameter to try to find an acceptable value */ 76 | val m = new Num(false, false, true, false, false) 77 | 78 | /** Vary `Numbers`' `mm` parameter to try to find an acceptable value */ 79 | val mm = new Num(false, false, false, true, false) 80 | 81 | /** Vary `Numbers`' `r` parameter to try to find an acceptable value */ 82 | val r = new Num(false, false, false, false, true) 83 | 84 | 85 | /** Vary `Ops`' `f` parameter to try to find an acceptable operation */ 86 | val f = new Oper(true, false, false, false, false) 87 | 88 | /** Vary `Ops`' `g` parameter to try to find an acceptable operation */ 89 | val g = new Oper(false, true, false, false, false) 90 | 91 | /** Vary `Ops`' `op` parameter to try to find an acceptable operation */ 92 | val op = new Oper(false, false, true, false, false) 93 | 94 | /** Vary `Ops`' `p` parameter to try to find an acceptable operation */ 95 | val p = new Oper(false, false, false, true, false) 96 | 97 | /** Vary `Ops`' `pf` parameter to try to find an acceptable operation */ 98 | val pf = new Oper(false, false, false, false, true) 99 | 100 | /** Vary `Instance`'s `a` parameter to try to find an acceptable element */ 101 | val a = new Inst(true, false, false) 102 | 103 | /** Vary `Instance`'s `x` parameter to try to find an acceptable collection */ 104 | val x = new Inst(false, true, false) 105 | 106 | /** Vary `Instance`'s `y` parameter to try to find an acceptable collection */ 107 | val y = new Inst(false, false, true) 108 | } 109 | 110 | /** The requested law did not run successfully because it does not even 111 | * exist on the specified line! 112 | */ 113 | final case class Missing(lawLine: Int) extends Outcome {} 114 | 115 | /** The test ran, but failed. */ 116 | final case class Failed[T](test: T) extends Outcome { 117 | override def hasTest = true 118 | } 119 | 120 | /** The test was attempted but it threw an exception while it was running. */ 121 | final case class Threw[T](test: T, error: Throwable) extends Outcome { 122 | override def hasTest = true 123 | override def hasException = true 124 | override def toString = f"Threw($test, ${partialStackTrace(error)})" 125 | } 126 | 127 | /** An exception was thrown while getting ready to run a test, but the test didn't actually run. 128 | * 129 | * This should never happen unless there's a bug in the testing framework (or a very bad library bug). 130 | */ 131 | final case class Error(error: Throwable) extends Outcome { 132 | override def hasException = true 133 | override def toString = f"Error(${partialStackTrace(error)})"; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /laws/src/main/scala/Report.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /** Reports on the results of tests. */ 4 | object Report { 5 | private implicit class ClipStringWithDots(s: String) { 6 | def tldr(n: Int) = if (s.length <= n) s else s.take(n-3) + "..." 7 | } 8 | 9 | /** Counts how many times each law is used, given a map with all test results. 10 | * 11 | * Returns, for each line number of a valid law, the number of collections 12 | * for which the law passed and failed, respectively. 13 | */ 14 | def countLawUsage(ran: Map[String, Test.Tested]): Map[Int, (Int, Int)] = { 15 | val score = laws.Laws.all.map(law => law.lineNumber -> (N(0), N(0))).toMap 16 | ran.foreach{ case (_, t) => 17 | t.succeeded.keys.foreach(k => score(k)._1.++()) 18 | t.failed.keys.foreach(k => score(k)._2.++()) 19 | } 20 | score.map{ case (k, (ns, nf)) => (k, (ns.count, nf.count)) } 21 | } 22 | 23 | /** Counts how many times each method was used on each collection. 24 | * 25 | * Returns a map from test names to counts. Each count is a map from method names to 26 | * a pair: number of laws using that method that succeeed, number of laws using that 27 | * method that failed. 28 | */ 29 | def countMethodUsage(ran: Map[String, Test.Tested]): Map[String, Map[String, (Int, Int)]] = 30 | ran.map{ case (coll, result) => 31 | coll -> result.findMethodUsage.map{ case (k, (ls, lf)) => (k, (ls.size, lf.size)) } 32 | } 33 | 34 | /** Produces a report on which laws weren't used in any tests. */ 35 | def reportUnusedLaws(ran: Map[String, Test.Tested]): Vector[String] = { 36 | val counts = countLawUsage(ran) 37 | val unused = counts.filter{ case (_, (ns, nf)) => ns == 0 && nf == 0 } 38 | val onlyFailed = counts.filter{ case (_, (ns, nf)) => ns == 0 && nf > 0 } 39 | if (unused.size == 0 && onlyFailed.size == 0) 40 | Vector(f"All laws successfully used (${counts.map(_._2._1).sum} times)") 41 | else { 42 | ( 43 | if (unused.size == 0) Vector.empty[String] 44 | else ( 45 | f"${unused.size} law${if (unused.size==1) "" else "s"} never used" +: 46 | unused.toVector.sortBy(_._1).map{ case (k, _) => 47 | f" #$k%-6d${Laws.byLineNumber(k).code.linesIterator.mkString("\u21B5 ").tldr(70)}" 48 | } 49 | ) 50 | ) ++ 51 | ( 52 | if (onlyFailed.size == 0) Vector.empty[String] 53 | else ( 54 | f"${onlyFailed.size} laws never succeeded on line${if (onlyFailed.size==1) "" else "s"}" +: 55 | onlyFailed.toVector.sortBy(_._1).map{ case (k, (_, nf)) => 56 | f" #$k%-4d failed $nf time${if (nf == 1) "" else "s" }".padTo(28, ' ') + Laws.byLineNumber(k).code.linesIterator.mkString("\u21B5 ").tldr(50) 57 | } 58 | ) 59 | ) 60 | } 61 | } 62 | 63 | /** Produces a report on which methods were never tested (for each collection and overall) */ 64 | def reportUnusedMethods(ran: Map[String, Test.Tested]): (Vector[String], Vector[String]) = { 65 | val misses = ran. 66 | groupBy(_._1.split('_').drop(1).take(2).mkString("_")). 67 | toVector.sortBy(_._1). 68 | flatMap{ case (title, tests) => 69 | val missing = 70 | tests. 71 | map(_._2.findMethodUsage.map{ case (k, (ls, lf)) => (k, ls.size + lf.size) }). 72 | reduceLeft{ (l, r) => 73 | val keys = l.keys ++ r.keys 74 | keys.toArray. 75 | map{ k => k -> (l.getOrElse(k, 0) + r.getOrElse(k, 0)) }. 76 | toMap 77 | }. 78 | collect{ case (k, n) if n == 0 => k }. 79 | toVector.sorted 80 | if (missing.isEmpty) None 81 | else Some(title -> missing) 82 | } 83 | 84 | val counts = misses.flatMap(_._2). 85 | groupBy(identity).map{ case (k, vs) => k -> vs.length }. 86 | toVector.sortBy{ case (meth, n) => (-n, meth) } 87 | 88 | val summary = 89 | if (counts.length > 0) 90 | Vector(f"${counts.length} different methods untested in at least one class") ++ 91 | counts.map{ case (method, count) => f" $method missing in $count classes" } 92 | else Vector.empty[String] 93 | 94 | val byClass = misses.flatMap{ case (title, methods) => 95 | if (methods.isEmpty) Vector.empty 96 | else { 97 | val longest = methods.map(_.length).foldLeft(1)(_ max _) 98 | f"Untested methods in ${title}:" +: 99 | methods.grouped(77/(longest+1) max 1).toVector.map{ ms => 100 | " " + ms.map(_.padTo(longest+1, ' ')).mkString 101 | } 102 | } 103 | } 104 | 105 | (summary, byClass) 106 | } 107 | 108 | /** Produces a report on what laws failed. 109 | * 110 | * The result is empty if all laws succeeded. 111 | */ 112 | def reportFailedLaws(ran: Map[String, Test.Tested]): Vector[String] = { 113 | val broken = collection.mutable.LongMap.empty[Mu[List[(String, Test.Fail)]]] 114 | ran.foreach{ case (name, tested) => 115 | tested.failed.foreach{ case (lln, failure) => 116 | broken.getOrElseUpdate(lln, Mu(List.empty[(String, Test.Fail)])).mutf((name, failure) :: _) 117 | } 118 | } 119 | if (broken.isEmpty) Vector.empty 120 | else { 121 | broken.toVector.sortBy(_._1).flatMap{ case (lln, errs) => 122 | Vector( 123 | f"*****************************************", 124 | f"********** Failures in line $lln", 125 | f"*****************************************", 126 | ) ++ 127 | errs.value. 128 | map{ case (name, failure) => f"****** $name ******\n$failure\n" }. 129 | mkString("\n").linesIterator.toVector ++ 130 | Vector("", "") 131 | } ++ 132 | Vector( 133 | f"************************************", 134 | f"************************************", 135 | f"************** ${broken.map(_._2.value.size).sum} errors", 136 | f"************************************", 137 | f"************************************" 138 | ) 139 | } 140 | } 141 | 142 | /** If we ran the tests with jUnit, the results are in a `ConcurrentHashMap`. 143 | * Print out the results, throwing an error afterwards if any laws failed. 144 | */ 145 | def junitReport(ran: java.util.concurrent.ConcurrentHashMap[String, Test.Tested]): Unit = { 146 | import scala.jdk.CollectionConverters._ 147 | val m = ran.asScala.toMap 148 | reportUnusedMethods(m) match { case (summary, details) => 149 | if (summary.nonEmpty) { 150 | summary.foreach(println) 151 | println() 152 | } 153 | if (details.nonEmpty) { 154 | details.foreach(println) 155 | println() 156 | } 157 | } 158 | reportUnusedLaws(m).foreach(println) 159 | val fails = reportFailedLaws(m) 160 | fails.foreach(println) 161 | assert(fails.isEmpty) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /laws/src/main/scala/Runner.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | import scala.util.control.NonFatal 4 | 5 | /** Runs a particular law across all relevant different parameter sets. 6 | * 7 | * In particular, the parameters are instrumented; if a parameter is 8 | * never used, alternate values of that parameter are not tested. 9 | * 10 | * The search strategy is a three-tiered hierarchy. The innermost 11 | * traversal is across the numeric values `n`, `nn`, `m`, `mm`, and 12 | * `r`. If any of these are used, alternate values are selected and 13 | * the test is run again. The central traversal is across the 14 | * operations `f`, `g`, `op`, `p`, and `pf`. If any of these are 15 | * used for any combination of the numeric values tried, alternates 16 | * are selected and all (relevant) numeric values are tried again. 17 | * The outermost traversal is across elements and collections. If 18 | * `a`, `x`, `xsize`, `y`, or `ysize` are used for any of the 19 | * tested combinations of numeric values and operations, alternates 20 | * are chosen and the tests are run again. 21 | * 22 | * Generation of alternates continues until all meaningful alternatives 23 | * have been exhausted. 24 | */ 25 | class Runner[A, B, CC, T <: Test[A, B, CC, T]]( 26 | lawLine: Int, 27 | exploreInst: () => Exploratory[Instance[A, CC]], 28 | exploreOps: () => Exploratory[Ops[A, B]], 29 | testGen: (Int, Instance[A, CC], Ops[A, B], Numbers) => T 30 | ) { 31 | /** The law being tested */ 32 | lazy val law = Laws.byLineNumber(lawLine) 33 | 34 | /** Picks appropriate numeric values to traverse depending on the collections chosen. */ 35 | def exploreNum(inst: Instance[A, CC]) = 36 | new Numbers.Restricted(inst.values.xsize, inst.values.ysize) 37 | 38 | /** Runs one test of a law with a particular set of values. */ 39 | def runOne(inst: Instance[A, CC], oper: Ops[A, B], num: Numbers): Outcome = { 40 | val t: T = testGen(lawLine, inst, oper, num) 41 | try { 42 | val law = Laws.byLineNumber.get(lawLine).getOrElse(return Outcome.Missing(lawLine)) 43 | law.tags.validate(t).foreach{ skip => return skip } 44 | if (t.run) Outcome.Success else Outcome.Failed(t) 45 | } 46 | catch { case e if NonFatal(e) => Outcome.Threw(t, e) } 47 | } 48 | 49 | /** Runs tests of the appropriate law generating all relevant diversity in numeric values. 50 | * 51 | * On success, returns the number of tests run. On failure, returns the failure `Outcome`. 52 | */ 53 | def runNums(inst: Instance[A, CC], oper: Ops[A, B]): Either[Outcome, Int] = { 54 | val mkNum = exploreNum(inst) 55 | val exNum = mkNum.explore() 56 | var progress = true 57 | var n = 0 58 | while (progress) { 59 | mkNum.lookup(exNum) match { 60 | case Some(num) => 61 | num.setUnused() 62 | runOne(inst, oper, num) match { 63 | case Outcome.Success => 64 | exNum.advance(num.used) 65 | n += 1 66 | case Outcome.Skip.Num(blame) => 67 | // We skipped this value, but the test didnt' fail; we just need to pick something else 68 | exNum.advance(blame) 69 | case o => 70 | return Left(o) 71 | } 72 | case None => 73 | progress = false 74 | } 75 | } 76 | Right(n) 77 | } 78 | 79 | /** Runs tests of the appropriate law generating all relevant diversity in 80 | * operations and numeric values. 81 | 82 | * On success of all tests, returns the number of tests run. On failure, returns 83 | * the failure `Outcome`. 84 | */ 85 | def runOps(inst: Instance[A, CC]): Either[Outcome, Long] = { 86 | val mkOps = exploreOps() 87 | val exOps = mkOps.explore() 88 | var progress = true 89 | var n = 0L 90 | while (progress) { 91 | mkOps.lookup(exOps) match { 92 | case Some(oper) => 93 | oper.setUnused() 94 | runNums(inst, oper) match { 95 | case Right(k) => 96 | exOps.advance(oper.used) 97 | n += k 98 | case Left(Outcome.Skip.Oper(blame)) => 99 | // We skipped this operation but the test didn't fail; we just need to pick something else 100 | exOps.advance(blame) 101 | case Left(err) => 102 | return Left(err) 103 | } 104 | case None => 105 | progress = false 106 | } 107 | } 108 | Right(n) 109 | } 110 | 111 | /** Runs a test of the appropriate law generating all relevant diversity in 112 | * collection contents, operations, and numeric values. 113 | 114 | * On success of all tests, returns the number of tests run. On failure, returns 115 | * the failure `Outcome`. 116 | */ 117 | def run: Either[Outcome, Long] = { 118 | val mkInst = exploreInst() 119 | val exInst = mkInst.explore() 120 | var progress = true 121 | var n = 0L 122 | while (progress) { 123 | mkInst.lookup(exInst) match { 124 | case Some(inst) => 125 | inst.setUnused() 126 | runOps(inst) match { 127 | case Right(k) => 128 | exInst.advance(inst.used) 129 | n += k 130 | case Left(Outcome.Skip.Inst(blame)) => 131 | // We skipped this particular collection/element combination but the test didn't fail; we just need to pick something else 132 | exInst.advance(blame) 133 | case Left(err) => 134 | return Left(err) 135 | } 136 | case None => 137 | progress = false 138 | } 139 | } 140 | Right(n) 141 | } 142 | } 143 | 144 | /** Runs all tests on all collections. (Code generator creates a class than inherits from this one.) */ 145 | trait AllRunner { 146 | /** Array containing all tests in function form. */ 147 | def runners: Array[() => (String, () => Test.Tested)] 148 | 149 | /** Runs all the tests, returning maps of test names to test results. */ 150 | def runAll(quiet: Boolean = false ): Map[String, Test.Tested] = 151 | runners.map{ f => 152 | val (s, t) = f() 153 | if (!quiet) println(f"Running $s") 154 | (s, t()) 155 | }.toMap 156 | 157 | /** Runs all the tests in parallel using futures, with the specified parallelism. 158 | * 159 | * Returns a map from test names to test results. 160 | */ 161 | def runPar(atOnce: Int = 4, quiet: Boolean = true): Map[String, Test.Tested] = { 162 | import scala.util._ 163 | import scala.concurrent.{Await, Future} 164 | import scala.concurrent.duration._ 165 | import scala.concurrent.ExecutionContext.Implicits.global // Maybe make a fixed-size thread pool? 166 | val toRun = runners.map(_()) 167 | val ans = collection.mutable.Map.empty[String, Test.Tested] 168 | val slots = new Array[(String, Future[(String, Test.Tested)])](1 max (atOnce min toRun.length)) 169 | var i = 0; 170 | var it = toRun.iterator 171 | while (i < slots.length && it.hasNext) { 172 | val (name, test) = it.next() 173 | slots(i) = name -> Future{ (name, test()) } 174 | if (!quiet) println(name + " started...") 175 | i += 1 176 | } 177 | while (i > 0) { 178 | Await.ready( 179 | Future.firstCompletedOf((if (i < slots.length) slots take i else slots).map(_._2)), 180 | Duration.create(5, MINUTES) 181 | ) 182 | var j = 0 183 | var found = false 184 | while (j < i) slots(j)._2.value match { 185 | case None => j += 1 186 | case Some(result) => 187 | result match { 188 | case Success((name, tt)) => 189 | ans(name) = tt 190 | if (!quiet) println("..." + name + " complete!") 191 | case Failure(e) => throw e 192 | } 193 | found = true 194 | if (it.hasNext) { 195 | val (name, test) = it.next() 196 | slots(j) = name -> Future{ (name, test()) } 197 | if (!quiet) println(name + " started...") 198 | j += 1 199 | } 200 | else { 201 | if (i-1 > j) slots(j) = slots(i-1) 202 | i -= 1 203 | } 204 | } 205 | if (!found) throw new Exception("Await timed out while running: " + slots.take(i).map(_._1)) 206 | } 207 | ans.toMap 208 | } 209 | } 210 | 211 | -------------------------------------------------------------------------------- /laws/src/main/scala/Tags.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | import scala.language.implicitConversions 4 | 5 | /** Tags provide a way to select which laws are applicable for a given run. For instance, 6 | * if you are testing collections with `Int`s and with `String`s, some tests may be 7 | * specific to the collection type; in that case you would tag the appropriate laws 8 | * using a string that helps you distinguish them. (E.g. `Tags("Int")`.) 9 | * 10 | * Additional restriction of the set may be achieved by including tests in `select`. All 11 | * such tests must pass for a particular set of parameters to be included in a test. 12 | */ 13 | case class Tags(positive: Set[Flag], negative: Set[Flag], select: Vector[TestInfo => Option[Outcome.Skip]]) { 14 | /** Tests whether any tags are present (either boolean or string-valued) */ 15 | val isEmpty = positive.isEmpty && negative.isEmpty && select.isEmpty 16 | 17 | /** Checks whether a certain set of flags is compatible for code generation (compile-time compatible) */ 18 | def compatible(flags: Set[Flag]) = 19 | positive.forall(f => f.disabled || flags.contains(f)) && !flags.exists(f => !f.disabled && negative.contains(f)) 20 | 21 | /** Checks whether a certain choice of parameters is suitable for testing at runtime */ 22 | def validate(t: TestInfo): Option[Outcome.Skip] = select.iterator.map(p => p(t)).find(_.isDefined).flatten 23 | 24 | /** Sets a boolean tag that must be present */ 25 | def need(t: Flag): Tags = 26 | if (positive contains t) this 27 | else if (negative contains t) new Tags(positive + t, negative - t, select) 28 | else new Tags(positive + t, negative, select) 29 | 30 | /** Requires that a particular tag be absent */ 31 | def shun(t: Flag): Tags = 32 | if (negative contains t) this 33 | else if (positive contains t) new Tags(positive - t, negative + t, select) 34 | else new Tags(positive, negative + t, select) 35 | 36 | /** Adds an extra selector that checks test info */ 37 | def filter(p: TestInfo => Option[Outcome.Skip]): Tags = new Tags(positive, negative, select :+ p) 38 | 39 | override lazy val toString = { 40 | val named = positive.toList.sorted ::: negative.toList.map("!" + _).sorted 41 | val pred = select.length match { 42 | case 0 => "" 43 | case 1 => "(1 filter)" 44 | case n => f"($n filters)" 45 | } 46 | (if (pred.nonEmpty) named.toVector :+ pred else named.toVector).mkString(" ") 47 | } 48 | } 49 | object Tags { 50 | /** Taggish represents values that can be tags: either a key alone, or a key-value pair (all strings). */ 51 | sealed trait Taggish {} 52 | final case class PosTag(flag: Flag) extends Taggish {} 53 | final case class NegTag(flag: Flag) extends Taggish {} 54 | final case class SelectTag(p: TestInfo => Option[Outcome.Skip]) extends Taggish {} 55 | //final case class SelectTag(p: List[String] => Boolean) extends Taggish {} 56 | /** Implicits contains implicit conversions to values that can be tags. */ 57 | object Implicits { 58 | implicit class TagIsTaggish(flag: Flag) { 59 | def y: PosTag = PosTag(flag) 60 | def n: NegTag = NegTag(flag) 61 | def ! : NegTag = NegTag(flag) 62 | } 63 | implicit def flagIsPositiveByDefault(flag: Flag): PosTag = PosTag(flag) 64 | implicit def selectWrapsFunction(p: TestInfo => Option[Outcome.Skip]): SelectTag = SelectTag(p) 65 | } 66 | 67 | /** Canonical empty set of tags. (That is, no tags.) */ 68 | val empty = new Tags(Set.empty[Flag], Set.empty[Flag], Vector.empty[TestInfo => Option[Outcome.Skip]]) 69 | 70 | /** Create a mixed set of boolean and predicate tags. 71 | * 72 | * First, `import laws.Tags.Implicits._`. Then use `"seq".y, "set".n, select(_.hasZero)` to set, 73 | * in this example, a tag that must be present, mustn't be present, and a test that must pass, respectively. 74 | * 75 | * Note: this is the best place to alter the code to ignore CAMEL tags (used to suppress errors with strawman collections) 76 | */ 77 | def apply(key: Taggish, keys: Taggish*) = { 78 | val all = key :: keys.toList 79 | val positive = all.collect{ case PosTag(s) => s }.toSet 80 | new Tags( 81 | positive, 82 | all.collect{ case NegTag(s) /* if !s.isCamel */ => s }.toSet &~ positive, // Comment in/out !s.isCamel to suppress known strawman errors 83 | all.collect{ case SelectTag(p) => p }.toVector 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /laws/src/main/scala/Test.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | import scala.language.implicitConversions 4 | import scala.language.higherKinds 5 | 6 | import scala.util._ 7 | 8 | /** The collection to be tested: this provides all elements and collections 9 | * and mappings thereon which are available inside the tests. 10 | * 11 | * Tests are immutable; if you want a different set of values, 12 | * create a new test. You can use the `renumber`, `reoperate`, and 13 | * `reinstance` methods to create a new test with the corresponding 14 | * values changed. 15 | * 16 | * Subclasses are intended to implement tests for _every_ valid 17 | * law, so a `relaw` method, to switch the law line number, 18 | * is provided as well. This method is not safe in that the 19 | * line number could be invalid. The `run` method will then 20 | * throw an exception. 21 | */ 22 | abstract class Test[A, B, CC, T <: Test[A, B, CC, T]]( 23 | val num: Numbers, 24 | val ops: Ops[A, B], 25 | val instance: Instance[A, CC], 26 | val lawLine: Int 27 | )(implicit file: sourcecode.File, line: sourcecode.Line, nm: sourcecode.Name) 28 | extends Sourced 29 | with Named 30 | with TestInfo { 31 | /** Arbitrary element of the type in the collection */ 32 | def a: A = instance.values.a 33 | 34 | /** An arbitrary element of a type not in the basic collection. Should match g(a). */ 35 | lazy val b: B = ops.values.g.fn(instance.values.a) 36 | 37 | /** Some function that preserves the type of the elements */ 38 | def f: A => A = ops.f 39 | 40 | /** Some function that changes the type of the elements. Note that g(a) == b should be true. */ 41 | def g: A => B = ops.g 42 | 43 | /** A positive number that is supposedly no bigger than the length of `x` */ 44 | def n: Int = num.n 45 | 46 | /** A positive number that may be bigger than the length of `x` */ 47 | def nn: Int = num.nn 48 | 49 | /** A positive number that is no bigger than the length of `y` */ 50 | def m: Int = num.m 51 | 52 | /** A positive number that may be bigger than the length of `y` **/ 53 | def mm: Int = num.mm 54 | 55 | /** Some integer. May be positive or negative. Could really be anything. */ 56 | def r: Int = num.r 57 | 58 | /** A binary operation on the collection elements. 59 | * 60 | * If no such operation exists, leave it as `Coll.noOp` and tests requiring this operator will be skipped. 61 | */ 62 | def op: (A, A) => A = ops.op 63 | 64 | /** A predicate on the type of the elements */ 65 | def p: A => Boolean = ops.p 66 | 67 | /** A partial function that preserves the type of the elements */ 68 | def pf: PartialFunction[A, A] = ops.pf 69 | 70 | /** A specific collection of some size */ 71 | def x: CC = instance.x 72 | 73 | /** The known size of the collection `x` */ 74 | def xsize: Int = instance.xsize 75 | 76 | /** Another specific collection, not necessarily the same as `x` */ 77 | def y: CC = instance.y 78 | 79 | /** The known size of collection `y` */ 80 | def ysize: Int = instance.ysize 81 | 82 | /** Element of type `A` that is a zero with respect to `op`, if `op` exists and a zero exists */ 83 | def zero: A = ops.z 84 | 85 | /** Converts a value to a characteristic integer. (Default is just hashcode.) */ 86 | def intFrom(a: A): Int = a.## 87 | 88 | /** Builds a fresh instance of the collection type being tested using an array as a source */ 89 | def collectionFrom(aa: Array[A]): CC = instance.newInstanceFrom(aa) 90 | 91 | def flags: Set[Flag] = instance.flags 92 | 93 | def oper: Ops[_, _] = ops 94 | 95 | def inst: Instance[_, _] = instance 96 | 97 | protected def boxedRuntime: Class[_] = a.getClass 98 | 99 | def runtimeColl: Class[_] = x.getClass 100 | 101 | def name = nm.value.toString 102 | 103 | val bitset_f = (i: Int) => i/2 + 999 104 | 105 | override lazy val toString = 106 | nm.value.toString + " @ " + source + 107 | f"\n $num\n" + 108 | instance.toString.split("\n").map(" " + _).mkString("", "\n", "\n") + 109 | ops.toString.split("\n").map(" " + _).mkString("", "\n", "\n") 110 | 111 | def tryE[A](f: => A): Either[Throwable, A] = try { Right(f) } catch { case e: Throwable => Left(e) } 112 | 113 | def tryO[A](f: => A): Option[A] = try { Some(f) } catch { case _: Throwable => None } 114 | 115 | def renumber(numb: Numbers): T 116 | 117 | def reinstance(inst: Instance[A, CC]): T 118 | 119 | def reoperate(ops: Ops[A, B]): T 120 | 121 | def relaw(lln: Int): T 122 | 123 | /** This is the actual test which runs a law and returns `Some(true)` if it 124 | * passes with these parameters, `Some(false)` if it fails, or `None` if 125 | * the law is not valid for this collection. It does NOT keep track of whether 126 | * the law has been run or not. 127 | */ 128 | def runLaw(n: Int): Option[Boolean] 129 | 130 | /** Runs on the default law given by the `lawLine` parameter. Throws an exception if the law number doesn't exist. */ 131 | def run: Boolean = runLaw(lawLine).get 132 | } 133 | object Test { 134 | /** Information about a test that has passed its law for every combination of parameters */ 135 | case class Pass(law: Law, iterations: Long, time: Double) {} 136 | 137 | /** Information about a test that has failed its law for some combination of parameters */ 138 | case class Fail(law: Law, outcome: Outcome, test: Option[Test[_, _, _, _]], exception: Option[Throwable]) { 139 | override def toString = { 140 | val testString = if (outcome.hasTest && test.isDefined) "Some(...)" else test.toString 141 | val exceptionString = if (outcome.hasException && exception.isDefined) "Some(...)" else exception.map(e => Outcome.partialStackTrace(e)).toString 142 | f"Fail($law, $outcome, ${}, $testString, $exceptionString}" 143 | } 144 | } 145 | 146 | /** Information about the test results for all relevant laws on a particular collection */ 147 | case class Tested(succeeded: Map[Int, Pass], failed: Map[Int, Fail], missed: Set[Int], methods: Set[String]) { 148 | /** For each method name, lists the line numbers of all laws that passed all attempted values (first list) 149 | * and the line numbers of laws that failed some value (second list) 150 | */ 151 | def findMethodUsage: Map[String, (List[Int], List[Int])] = { 152 | val usage = methods.toArray.map{ m => (m, (Mu(List.empty[Int]), Mu(List.empty[Int]))) }.toMap 153 | for { 154 | (_, Pass(law, _, _)) <- succeeded 155 | m <- law.methods.getOrElse(Set.empty[String]) 156 | } usage.get(m).foreach(_._1.mutf(law.lineNumber :: _)) 157 | for { 158 | (_, Fail(law, _, _, _)) <- failed 159 | m <- law.methods.getOrElse(Set.empty[String]) 160 | } usage.get(m).foreach(_._2.mutf(law.lineNumber :: _)) 161 | usage.map{ case (m, (mu1, mu2)) => (m, (mu1.value.sorted, mu2.value.sorted)) } 162 | } 163 | } 164 | 165 | /** A generic companion to test classes. 166 | * 167 | * Some of the methods are abstract and are filled in by the code generator. 168 | */ 169 | trait Companion extends Named { 170 | /** The laws tested by this (kind of) test */ 171 | def obeys: Map[Int, Law] 172 | 173 | /** The methods possessed by the collection in this kind of test */ 174 | def methods: Set[String] 175 | 176 | /** Keeps track of which laws were actually run */ 177 | lazy val ran = new collection.mutable.HashSet[Int] 178 | 179 | /** Runs a single test */ 180 | def run(lln: Int): Either[Outcome, Long] 181 | 182 | /** Runs the tests on all the laws */ 183 | def runAll(): Tested = { 184 | val good = new collection.mutable.HashMap[Int, Pass] 185 | val bad = new collection.mutable.HashMap[Int, Fail] 186 | val missed = new collection.mutable.HashSet[Int] 187 | obeys.foreach{ case (lln, law) => 188 | val before = System.nanoTime 189 | val result = Try{ run(lln) } 190 | val elapsed = 1e-9*(System.nanoTime - before) 191 | result match { 192 | case Failure(e) => bad(lln) = Fail(law, Outcome.Error(e), None, Some(e)) 193 | case Success(x) => x match { 194 | case Right(n) => if (n > 0) good(lln) = Pass(law, n, elapsed) else missed += lln 195 | case Left(o) => o match { 196 | case Outcome.Missing(n) => missed += lln 197 | case Outcome.Failed(t: Test[_, _, _, _]) => bad(lln) = Fail(law, o, Some(t), None) 198 | case Outcome.Threw(t: Test[_, _, _, _], e) => bad(lln) = Fail(law, o, Some(t), Some(e)) 199 | case Outcome.Error(e) => bad(lln) = Fail(law, o, None, Some(e)) 200 | case _ => bad(lln) = Fail(law, o, None, None) 201 | } 202 | } 203 | } 204 | } 205 | Tested(good.toMap, bad.toMap, missed.toSet, methods) 206 | } 207 | } 208 | 209 | /** Abstracts over traversing over a collection so that we can 210 | use different collections libraries and be less sensitive to 211 | which one we're comparing to which (all should convert to this) */ 212 | abstract class Once[A] { 213 | def step[U](f: A => U): Boolean 214 | 215 | final def foreach[U](f: A => U): Unit = 216 | while(step(f)) {} 217 | 218 | final def forallWith[B](that: Once[B])(p: (A, B) => Boolean): Boolean = { 219 | var aok, bok, ok = true 220 | while (ok) { 221 | var a: A = null.asInstanceOf[A] 222 | var b: B = null.asInstanceOf[B] 223 | aok = this.step(a = _) 224 | bok = that.step(b = _) 225 | ok = aok && bok && p(a, b) 226 | } 227 | !aok && !bok && !ok 228 | } 229 | } 230 | object Once { 231 | def from(string: String): Once[Char] = new Once[Char] { 232 | private[this] var i = 0 233 | def step[U](f: Char => U) = (i < string.length) && { f(string(i)); i += 1; true } 234 | } 235 | 236 | def from[A](array: Array[A]): Once[A] = new Once[A] { 237 | private[this] var i = 0 238 | def step[U](f: A => U): Boolean = (i < array.length) && { f(array(i)); i += 1; true } 239 | } 240 | 241 | def from[A](iterator: Iterator[A]): Once[A] = new Once[A] { 242 | def step[U](f: A => U): Boolean = iterator.hasNext && { f(iterator.next()); true } 243 | } 244 | def from[A](iterable: Iterable[A]): Once[A] = from(iterable.iterator) 245 | 246 | object Conversions { 247 | implicit def onceViaString(string: String): Once[Char] = Once from string 248 | implicit def onceViaArray[A](array: Array[A]): Once[A] = Once from array 249 | implicit def onceViaIterableOnce[A, CC[A] <: collection.IterableOnce[A]](me: CC[A]): Once[A] = 250 | me match { 251 | case iterator: Iterator[_] => Once from iterator.asInstanceOf[Iterator[A]] 252 | case iterable: Iterable[_] => Once from iterable.asInstanceOf[Iterable[A]] 253 | case _ => Once from me.iterator.to(List) 254 | } 255 | implicit def onceViaIterableTuple[K, V, CC[K, V] <: collection.Iterable[(K, V)]](me: CC[K, V]): Once[(K, V)] = Once from me 256 | } 257 | } 258 | 259 | 260 | /** Tests whether the compiler believes the left-hand and right-hand types are the same */ 261 | implicit class SameCompilerType[A](me: A) { 262 | def sameType[B](you: B)(implicit ev: A =:= B) = true 263 | } 264 | 265 | /** Tests whether two collections have the same elements in the same order */ 266 | implicit class EqualInOrder[A, CC](me: CC)(implicit onceCC: CC => Once[A]) { 267 | def sameAs[DD](you: DD)(implicit onceDD: DD => Once[A]) = 268 | onceCC(me).forallWith(onceDD(you))(_ == _) 269 | } 270 | 271 | /** Tests whether two collections have the same number of each element. */ 272 | implicit class EqualInCount[A, CC](me: CC)(implicit onceCC: CC => Once[A]) { 273 | def sameAs[DD](you: DD)(implicit onceDD: DD => Once[A]) = { 274 | val meM, youM = collection.mutable.HashMap.empty[A, N] 275 | onceCC(me).foreach(a => meM.getOrElseUpdate(a, new N).++()) 276 | onceDD(you).foreach(a => youM.getOrElseUpdate(a, new N).++()) 277 | meM.forall{ case (a, n) => youM.get(a).exists(_.count == n.count) } && 278 | youM.forall{ case (a, n) => meM contains a } 279 | } 280 | } 281 | 282 | /** Tests whether the right-hand collection has every element the left-hand collection does (it may have more). */ 283 | implicit class SubsetInCount[A, CC](me: CC)(implicit onceCC: CC => Once[A]) { 284 | def samePieces[DD](you: DD)(implicit onceDD: DD => Once[A]) = { 285 | val meM, youM = collection.mutable.HashMap.empty[A, N] 286 | onceCC(me).foreach(a => meM.getOrElseUpdate(a, new N).++()) 287 | onceDD(you).foreach(a => youM.getOrElseUpdate(a, new N).++()) 288 | meM.forall{ case (a, n) => youM.get(a).exists(_.count == n.count) } && 289 | youM.forall{ case (a, n) => meM contains a } 290 | } 291 | def partOf[DD](you: DD)(implicit onceDD: DD => Once[A]) = { 292 | val meM, youM = collection.mutable.HashMap.empty[A, N] 293 | onceCC(me).foreach(a => meM.getOrElseUpdate(a, new N).++()) 294 | onceDD(you).foreach(a => youM.getOrElseUpdate(a, new N).++()) 295 | meM.forall{ case (a, n) => youM.get(a).exists(_.count >= n.count) } 296 | } 297 | } 298 | 299 | /** Shorthand short-circuiting logic operators */ 300 | implicit class Logic(lhs: Boolean) { 301 | def implies(rhs: => Boolean) = !lhs || rhs 302 | def impliedBy(rhs: => Boolean) = lhs || !rhs 303 | } 304 | } 305 | 306 | 307 | ///////////////////////////////////////// 308 | // Selection of element type for tests // 309 | ///////////////////////////////////////// 310 | 311 | /** Tests that use `Int` values extend this class. */ 312 | abstract class IntTest[CC, T <: IntTest[CC, T]]( 313 | numb: Numbers, oper: Ops[Int, Long], inst: Instance[Int, CC], lln: Int 314 | )( 315 | implicit file: sourcecode.File, line: sourcecode.Line, name: sourcecode.Name 316 | ) 317 | extends Test[Int, Long, CC, T](numb, oper, inst, lln)(file, line, name) { 318 | type A = Int 319 | type B = Long 320 | type Inst = Instance[Int, CC] 321 | type Oper = Ops[Int, Long] 322 | def maxOf(a: Int, aa: Int) = a max aa 323 | def minOf(a: Int, aa: Int) = a min aa 324 | override def intFrom(a: Int) = a 325 | } 326 | 327 | /** Tests that use `String` values extend this class. */ 328 | abstract class StrTest[CC, T <: StrTest[CC, T]]( 329 | numb: Numbers, oper: Ops[String, Option[String]], inst: Instance[String, CC], lln: Int 330 | )( 331 | implicit file: sourcecode.File, line: sourcecode.Line, name: sourcecode.Name 332 | ) 333 | extends Test[String, Option[String], CC, T](numb, oper, inst, lln)(file, line, name) { 334 | type A = String 335 | type B = Option[String] 336 | type Inst = Instance[String, CC] 337 | type Oper = Ops[String, Option[String]] 338 | def maxOf(a: String, aa: String) = { val o = implicitly[Ordering[String]]; o.max(a, aa) } 339 | def minOf(a: String, aa: String) = { val o = implicitly[Ordering[String]]; o.min(a, aa) } 340 | override def intFrom(s: String) = s.length 341 | } 342 | 343 | /** Tests (for maps) that use `(Long, String)` values extend this class. */ 344 | abstract class LongStrTest[CC, T <: LongStrTest[CC, T]]( 345 | numb: Numbers, oper: Ops[(Long, String), (String, Long)], inst: Instance[(Long, String), CC], lln: Int 346 | )( 347 | implicit file: sourcecode.File, line: sourcecode.Line, name: sourcecode.Name 348 | ) 349 | extends Test[(Long, String), (String, Long), CC, T](numb, oper, inst, lln)(file, line, name) { 350 | type K = Long 351 | type V = String 352 | type A = (K, V) 353 | type B = (V, K) 354 | type Inst = Instance[(K, V), CC] 355 | type Oper = Ops[(K, V), (V, K)] 356 | 357 | def maxOf(a: (Long, String), aa: (Long, String)) = { val o = implicitly[Ordering[(Long, String)]]; o.max(a, aa) } 358 | def minOf(a: (Long, String), aa: (Long, String)) = { val o = implicitly[Ordering[(Long, String)]]; o.min(a, aa) } 359 | override def intFrom(kv: (Long, String)) = kv._1.toInt 360 | } 361 | 362 | /** Tests (for maps) that use `(String, Long)` values extend this class. */ 363 | abstract class StrLongTest[CC, T <: StrLongTest[CC, T]]( 364 | numb: Numbers, oper: Ops[(String, Long), (Long, String)], inst: Instance[(String, Long), CC], lln: Int 365 | )( 366 | implicit file: sourcecode.File, line: sourcecode.Line, name: sourcecode.Name 367 | ) 368 | extends Test[(String, Long), (Long, String), CC, T](numb, oper, inst, lln)(file, line, name) { 369 | type K = String 370 | type V = Long 371 | type A = (K, V) 372 | type B = (V, K) 373 | type Inst = Instance[(K, V), CC] 374 | type Oper = Ops[(K, V), (V, K)] 375 | 376 | def maxOf(a: (String, Long), aa: (String, Long)) = { val o = implicitly[Ordering[(String, Long)]]; o.max(a, aa) } 377 | def minOf(a: (String, Long), aa: (String, Long)) = { val o = implicitly[Ordering[(String, Long)]]; o.min(a, aa) } 378 | override def intFrom(kv: (String, Long)) = kv._2.toInt 379 | } 380 | 381 | -------------------------------------------------------------------------------- /laws/src/main/scala/TestInfo.scala: -------------------------------------------------------------------------------- 1 | package laws 2 | 3 | /** Information that you can use to filter tests at runtime. */ 4 | trait TestInfo { 5 | /** The particular numbers used for this test */ 6 | def num: Numbers 7 | 8 | /** The particular operations used for this test */ 9 | def oper: Ops[_, _] 10 | 11 | /** The particular collections and singleton element used for this test */ 12 | def inst: Instance[_, _] 13 | 14 | /** The runtime type of the element. */ 15 | final def runtimeElt: java.lang.Class[_] = { 16 | val r = boxedRuntime 17 | if (r == classOf[java.lang.Integer]) classOf[Int] 18 | else if (r == classOf[java.lang.Long]) classOf[Long] 19 | else if (r == classOf[java.lang.Double]) classOf[Double] 20 | else if (r == classOf[java.lang.Float]) classOf[Float] 21 | else if (r == classOf[java.lang.Character]) classOf[Char] 22 | else if (r == classOf[java.lang.Byte]) classOf[Byte] 23 | else if (r == classOf[java.lang.Short]) classOf[Short] 24 | else if (r == classOf[scala.runtime.BoxedUnit]) classOf[Unit] 25 | else r 26 | } 27 | 28 | /** Subclasses need to implement this method to get the runtime class type correct */ 29 | protected def boxedRuntime: java.lang.Class[_] 30 | 31 | /** The runtime collection type. (Subclasses implement this explicitly.) */ 32 | def runtimeColl: java.lang.Class[_] 33 | } 34 | 35 | -------------------------------------------------------------------------------- /laws/src/main/scala/Util.scala: -------------------------------------------------------------------------------- 1 | /** 2 | Utils contains various small utility classes and has no dependencies. 3 | */ 4 | 5 | package laws 6 | 7 | /** N is a mutable counter */ 8 | class N(var count: Int = 0) { 9 | /** Increment and return new value */ 10 | def ++(): Int = { 11 | count += 1 12 | count 13 | } 14 | } 15 | object N { 16 | def apply(count: Int = 0) = new N(count) 17 | } 18 | 19 | 20 | /** Mu holds an arbitrary mutable value */ 21 | class Mu[A](var value: A) { 22 | def mutf(f: A => A): this.type = { 23 | value = f(value) 24 | this 25 | } 26 | } 27 | object Mu { 28 | def apply[A](value: A) = new Mu[A](value) 29 | } 30 | 31 | 32 | /** Trait that captures the idea of having a source from which one is generated. 33 | */ 34 | trait Sourced { 35 | final def source(implicit file: sourcecode.File, line: sourcecode.Line) = Sourced.local(file, line) 36 | } 37 | object Sourced { 38 | /** A text description of the file and line some source came from */ 39 | def local(file: sourcecode.File, line: sourcecode.Line): String = 40 | (new java.io.File(file.value)).getName + ", line " + line.value 41 | 42 | /** A text description of file and line using implicit values in scope (or the current line) */ 43 | def implicitly(implicit file: sourcecode.File, line: sourcecode.Line): String = 44 | local(file, line) 45 | } 46 | 47 | 48 | /** Trait that captures the idea of having a known variable name */ 49 | trait Named { 50 | def name: String 51 | } 52 | 53 | 54 | /** Caches a computation that we expect to generate something immutable (so a cache is fine) */ 55 | final class CachedFn0[A](val underlying: () => A) extends (() => A) { 56 | private lazy val cache = underlying() 57 | def apply(): A = cache 58 | } 59 | 60 | 61 | /** A typeclass that provides the ability to check the size of something. 62 | * 63 | * Intended for use with collections. 64 | */ 65 | trait Sizable[CC] { 66 | def sizeof(c: CC): Int 67 | } 68 | 69 | 70 | /** Checks to make sure a set of methods are available */ 71 | class MethodChecker(val methods: Set[String]) { 72 | import scala.reflect._ 73 | import runtime.universe._ 74 | def passes(available: Set[String]) = methods.forall(available) 75 | def passes(available: MethodChecker) = methods.forall(available.methods) 76 | def |(that: MethodChecker): MethodChecker = new MethodChecker(methods | that.methods) 77 | } 78 | object MethodChecker { 79 | import scala.reflect._ 80 | import runtime.universe._ 81 | 82 | val empty = new MethodChecker(Set.empty) 83 | 84 | private val anyRefMethods = 85 | implicitly[TypeTag[AnyRef]].tpe.members. 86 | collect{ case x if x.isMethod => x.asMethod }. 87 | filter(_.isPublic). 88 | map(_.name.decodedName.toString). 89 | toSet 90 | 91 | private val ignoredMethods = Set( 92 | "$init$", 93 | "canEqual", 94 | "clone", 95 | "par", 96 | "seq", 97 | // General methods from functions (usually the default implementations are used) 98 | "andThen", 99 | "compose", 100 | // General methods from partial functions (usually the default implementations are used) 101 | "applyOrElse", 102 | "lift", 103 | "orElse", 104 | "runWith" 105 | ) 106 | 107 | private val assumedMethods = Set( 108 | "filter", 109 | "flatMap", 110 | "map" 111 | ) 112 | 113 | def from[C: TypeTag]: MethodChecker = { 114 | val tp = implicitly[TypeTag[C]].tpe 115 | val meths = tp.members.collect{ case x if x.isMethod => x.asMethod }.filter(_.isPublic) 116 | new MethodChecker( 117 | meths.map(_.name.decodedName.toString).filterNot(_.contains("$default")).toSet 118 | -- anyRefMethods 119 | -- ignoredMethods 120 | ++ assumedMethods 121 | ) 122 | } 123 | } 124 | 125 | 126 | /** Builds and caches an array of values. (Not thread-safe.) 127 | * 128 | * It just acts like a buffer that fixes its contents once in use. 129 | * 130 | * It is useful for cases where you want to list names that are automatically picked up by the `sourcecode` package. 131 | */ 132 | trait Variants[A] { 133 | type Item = A 134 | private[this] val myRegistered = collection.mutable.ArrayBuffer.empty[Item] 135 | private[this] var myCachedArray: Option[Array[A]] = None 136 | 137 | final def has(item: Item): Item = { myRegistered += item; item } 138 | 139 | final def all(implicit ev: reflect.ClassTag[Item]): Array[Item] = 140 | myCachedArray match { 141 | case None => 142 | val a = new Array[Item](myRegistered.length) 143 | var i = 0; while (i < a.length) { a(i) = myRegistered(i); i += 1 } 144 | myCachedArray = Some(a) 145 | a 146 | case Some(a) => 147 | a 148 | } 149 | 150 | final def index(i: Int)(implicit ev: reflect.ClassTag[Item]): Item = all(ev).apply(i) 151 | } 152 | 153 | 154 | /** Utility methods to perform file I/O in the context of code generation, where there 155 | * is some name-mangling, and you don't want to write the file if you haven't changed 156 | * the contents. 157 | */ 158 | object FileIO { 159 | /** Removes all the test files in a particular directory; throws an exception if anything goes wrong. */ 160 | def desource(dir: java.io.File): Unit = { 161 | val oops = 162 | dir.listFiles. 163 | filter(f => f.getName.startsWith("Test") && f.getName.endsWith(".scala")). 164 | find(f => !f.delete()) 165 | oops.foreach{ f => 166 | println(s"Failed to delete $f") 167 | throw new Exception(s"Could not remove source of $f") 168 | } 169 | } 170 | 171 | private[this] def trimRight(s: String): String = { 172 | var i = s.length - 1 173 | while (i >= 0 && java.lang.Character.isWhitespace(s.charAt(i))) i -= 1; 174 | if (i+1 < s.length) s.substring(0, i+1) else s 175 | } 176 | 177 | /** Replaces a text file with new text if the new text is different (trailing whitespace ignored). 178 | * Returns true if replacement occured, false if not, and throws an exception if any I/O failed. 179 | */ 180 | def apply(target: java.io.File, content: String): Boolean = { 181 | val myLines = collection.immutable.ArraySeq.from(content.linesIterator) 182 | val different = 183 | if (target.exists) { 184 | val src = scala.io.Source.fromFile(target) 185 | try { 186 | val lines = src.getLines().toVector.map(trimRight) 187 | lines != myLines.map(trimRight) 188 | } 189 | finally src.close 190 | } 191 | else true 192 | if (different) java.nio.file.Files.write(target.toPath, java.util.Arrays.asList(myLines: _*)) 193 | different 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | sbt laws/package laws/run 3 | sbt -J-Xmx6G -J-XX:MaxMetaspaceSize=4G -J-XX:-OmitStackTraceInFastThrow tests/test 4 | echo 'All laws succeeded to the expected extent.' 5 | -------------------------------------------------------------------------------- /tests/build.sbt: -------------------------------------------------------------------------------- 1 | name := "collections-laws-tests" 2 | 3 | libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.3" 4 | 5 | /* 6 | // Old version. Needs to be tweaked to work. 7 | (sourceGenerators in Compile) += Def.task { 8 | val out = (sourceManaged in Compile).value 9 | if (!out.exists) IO.createDirectory(out) 10 | val args = out.getAbsolutePath :: ("--versionID=" + (scalaVersion in Compile).value) :: Nil 11 | val runTarget = (mainClass in Compile in inst).value getOrElse "No main class defined for tests generator" 12 | val classPath = (fullClasspath in Compile in inst).value 13 | toError(runner.value.run(runTarget, classPath.files, args, streams.value.log)) 14 | (out ** "*.scala").get 15 | }.taskValue 16 | */ 17 | --------------------------------------------------------------------------------