├── .clj-kondo └── config.edn ├── .github └── workflows │ ├── clojure-linting.yaml │ ├── mend.yaml │ └── pr-testing.yaml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── classpath-test └── does-not-exist-anywhere-else ├── dev-resources └── fixtures │ ├── plain-text.txt │ ├── plain-text.txt.gz │ └── tar-contents.tar ├── doc └── intro.md ├── project.clj ├── src └── puppetlabs │ └── kitchensink │ ├── classpath.clj │ ├── core.clj │ ├── file.clj │ ├── json.clj │ └── time.clj └── test └── puppetlabs └── kitchensink ├── classpath_test.clj ├── core_test.clj ├── file_test.clj ├── json_test.clj ├── testutils.clj ├── testutils └── fixtures.clj └── time_test.clj /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:refer-all {:exclude [clojure.test slingshot.test]} 2 | :deprecated-var {:exclude {puppetlabs.kitchensink.core/cn-for-dn {:namespaces [puppetlabs.kitchensink.core puppetlabs.kitchensink.core-test]}}}} 3 | :output {:linter-name true}} 4 | -------------------------------------------------------------------------------- /.github/workflows/clojure-linting.yaml: -------------------------------------------------------------------------------- 1 | name: Clojure Linting 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited, synchronize] 6 | paths: ['src/**','test/**','.clj-kondo/config.edn','project.clj','.github/**'] 7 | 8 | jobs: 9 | clojure-linting: 10 | name: Clojure Linting 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: setup java 14 | uses: actions/setup-java@v3 15 | with: 16 | distribution: temurin 17 | java-version: 17 18 | - name: checkout repo 19 | uses: actions/checkout@v2 20 | - name: install clj-kondo (this is quite fast) 21 | run: | 22 | curl -sLO https://raw.githubusercontent.com/clj-kondo/clj-kondo/master/script/install-clj-kondo 23 | chmod +x install-clj-kondo 24 | ./install-clj-kondo --dir . 25 | - name: kondo lint 26 | run: ./clj-kondo --lint src test 27 | - name: eastwood lint 28 | run: | 29 | java -version 30 | lein eastwood -------------------------------------------------------------------------------- /.github/workflows/mend.yaml: -------------------------------------------------------------------------------- 1 | name: mend_scan 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: connect_twingate 12 | uses: twingate/github-action@v1 13 | with: 14 | service-key: ${{ secrets.TWINGATE_PUBLIC_REPO_KEY }} 15 | - name: checkout repo content 16 | uses: actions/checkout@v2 # checkout the repository content to github runner. 17 | with: 18 | fetch-depth: 1 19 | # install java which is required for mend and clojure 20 | - name: setup java 21 | uses: actions/setup-java@v3 22 | with: 23 | distribution: temurin 24 | java-version: 17 25 | # install clojure tools 26 | - name: Install Clojure tools 27 | uses: DeLaGuardo/setup-clojure@10.1 28 | with: 29 | # Install just one or all simultaneously 30 | # The value must indicate a particular version of the tool, or use 'latest' 31 | # to always provision the latest version 32 | cli: latest # Clojure CLI based on tools.deps 33 | lein: latest # Leiningen 34 | boot: latest # Boot.clj 35 | bb: latest # Babashka 36 | clj-kondo: latest # Clj-kondo 37 | cljstyle: latest # cljstyle 38 | zprint: latest # zprint 39 | # run lein gen 40 | - name: create pom.xml 41 | run: lein pom 42 | # download mend 43 | - name: download_mend 44 | run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar 45 | - name: run mend 46 | run: env WS_INCLUDES=pom.xml java -jar wss-unified-agent.jar 47 | env: 48 | WS_APIKEY: ${{ secrets.MEND_API_KEY }} 49 | WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent 50 | WS_USERKEY: ${{ secrets.MEND_TOKEN }} 51 | WS_PRODUCTNAME: Puppet Enterprise 52 | WS_PROJECTNAME: ${{ github.event.repository.name }} 53 | WS_FILESYSTEMSCAN: true 54 | WS_CHECKPOLICIES: true 55 | WS_FORCEUPDATE: true 56 | -------------------------------------------------------------------------------- /.github/workflows/pr-testing.yaml: -------------------------------------------------------------------------------- 1 | name: PR Testing 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, reopened, edited, synchronize] 7 | paths: ['src/**','test/**','project.clj'] 8 | 9 | jobs: 10 | pr-testing: 11 | name: PR Testing 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: ['11', '17'] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout repo 19 | uses: actions/checkout@v3 20 | with: 21 | submodules: recursive 22 | - name: setup java 23 | uses: actions/setup-java@v3 24 | with: 25 | distribution: 'temurin' 26 | java-version: ${{ matrix.version }} 27 | - name: clojure tests 28 | run: lein test 29 | timeout-minutes: 30 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | /target/ 6 | .lein-deps-sum 7 | .lein-failures 8 | .clj-kondo/.cache/ 9 | .eastwood 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | jdk: 3 | - openjdk8 4 | - openjdk11 5 | notifications: 6 | email: false 7 | hipchat: 8 | rooms: 9 | secure: ASvt1XwEYbgkKuYZjZHytwg/6Y53Tg4T7QhohiDB4Xb1dmJueqPFpV2ko/VjHCa18JjLiUq0nWcDpRjsqaGGvJ5FSxTyyWDKtZsg1sUf4F+7aZ5vq0Dzg8Uzvdu7m9X1Uszvs9zf6wJ+Jobq4xck1xpPYxFT/+ei2Q2STrJ9xwQ= 10 | template: 11 | - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}" 12 | - "Change view: %{compare_url}" 13 | - "Build details: %{build_url}" 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## unreleased 2 | 3 | ## 3.4.0 4 | * add new JSON encoders for java.time Instant, LocalDate, and LocalDateTime 5 | 6 | ## 3.3.1 7 | * add new functions for conversion of milliseconds to seconds, minutes, hours and days, along with a set of logical corresponding matrix of those units 8 | 9 | ## 3.3.0 10 | * add new functions `duration-str->seconds` and `duration-str->Duration` to calculate duration values in the style of Puppet duration settings resulting in an integer and a java.time.Duration respectively 11 | * add new function `safe-parse-int` to convert a number or string representing a to a number 12 | * update clj-parent to 5.6.14 13 | 14 | ## 3.2.5 15 | * in `atomic-write` ensure the writer is closed if the `write-function` throws an exception. 16 | 17 | ## 3.2.4 18 | * change use of java.security.cert.X509Certificate/getSubjectDN, which is now deprecated, to java.security.cert.X509Certificate/getSubjectX500Principal 19 | * add function `get-lein-project-version` to retrieve a given project version from the standard `lein` system properties. 20 | * mark deprecated interfaces with metadata deprecation 21 | * add 'unzip-file' routine to the files namespace to decompress zipped files 22 | * add 'untar-file' routine to the files namespace to decompress tar files 23 | * update clj-parent to 5.6.6 24 | * remove reflection use in atomic-write 25 | * add function `delete-recursively` to the files namespace to recursively delete a directory 26 | 27 | ## 3.2.3 28 | * add clj-kondo linting, eastwood linting, and PR testing 29 | * address issues identified by clj-kondo, and eastwood 30 | * add function equivalent of puppet's versioncmp function, and helper functions for to determine if strings have all numeric characters and if strings start with a leading zero. 31 | 32 | ## 3.2.2 33 | 34 | * add conversion functions from string -> ZonedDateTime -> string to facilitate the removal of clj-time 35 | * update clj-parent to 5.3.7 36 | 37 | ## 3.2.1 38 | 39 | * Update ini4j to 0.5.4 to address a Denial of Service 40 | vulnerability (CVE-2022-41404). 41 | 42 | ## 3.2.0 43 | 44 | This is a minor feature release. 45 | 46 | * Add key->str function to convert keywords to strings 47 | 48 | ## 3.1.3 49 | 50 | * Add base-type function that returns the base type from an HTTP Content-Type header. 51 | 52 | ## 3.1.2 53 | 54 | * Update clj-parent to resolve some outdated dependencies with security issues. 55 | 56 | ## 3.1.1 57 | 58 | * Don't select an open port from the ephemeral range in open-port-num. 59 | 60 | ## 3.1.0 61 | 62 | This is a minor feature release. 63 | 64 | * Add a function for atomic file writes 65 | 66 | ## 3.0.0 67 | 68 | Maintenance: 69 | * Update dependencies to take up clj-parent 4 70 | 71 | ## 2.5.2 72 | 73 | This is a minor maintenance release. 74 | 75 | Maintenance: 76 | * Fix adding URLs to classpath under Java 9. 77 | 78 | ## 2.5.1 79 | 80 | This is a minor maintenance release. 81 | 82 | Maintenance: 83 | * Fix symbol redef warnings under Clojure 1.9 84 | 85 | ## 2.5.0 86 | 87 | This is a minor feature release. 88 | 89 | Features: 90 | * add a `stream->sha256` function for hashing the contents of an InputStream 91 | 92 | ## 2.4.0 93 | 94 | This is a minor feature and improvement release. 95 | 96 | Features: 97 | * add a `utf8-string->sha256` function, directly analogous to `utf8-string->sha1` 98 | * add a `file->sha256` function, equivalent to reading a file's contents as a 99 | UTF-8 string and hashing the result. Uses an InputStream internally to avoid 100 | reading the entire file into memory at once. 101 | 102 | Improvement: 103 | * the `open-port-num` function should now return a random port number from the 104 | entire traditional ephemeral port range of 49152 through 65535. 105 | 106 | ## 2.3.0 107 | 108 | This is a minor feature release. 109 | 110 | Features: 111 | * add a parser for period strings (7d, 12h, etc) into Joda Periods 112 | * fix file connection leaks in the functions 'lines' and 'ini-to-map' 113 | 114 | ## 2.2.0 115 | 116 | This is a minor feature release. 117 | 118 | Features: 119 | 120 | * Add an `assoc-if-new` macro, which associates a map key to a value only if 121 | the key does not already exist in the map. 122 | * Add a `deref-swap!` function, which behaves like deref but returns the old 123 | value instead of the new one. 124 | * Add a `rand-str` function, for generating random strings from various 125 | character sets. 126 | 127 | Maintenance: 128 | 129 | * Update to dynapath 0.2.5, to address some compatibility issues with Java 9. 130 | 131 | ## 2.1.1 132 | 133 | The 2.1.1 release was burned and folded into 2.2.0. 134 | 135 | ## 2.1.0 136 | 137 | This is a minor feature release. 138 | 139 | Features: 140 | 141 | * Add an `open-port-num` function, which returns a currently open port. Tests 142 | that bind services to ports can use this to guard against chance port 143 | collisions. 144 | 145 | ## 2.0.0 146 | 147 | This is a bugfix release which contains one backward incompatible change. 148 | 149 | Bug fix: 150 | 151 | * Changes all of the maps in various slingshot errors thrown to use `:kind` and `:msg` 152 | in place of `:type` and `:message`, respectively. 153 | 154 | ## 1.4.0 155 | 156 | This is a minor feature release, which also includes some bugfixes and maintenance work. 157 | 158 | Features: 159 | 160 | * Add `uuid?` predicate function for determining whether a string is a valid UUID. 161 | 162 | Bug fixes: 163 | 164 | * Fix an issue in `with-additional-classpath-entries` wherein it's pre/post-conditions were 165 | not handling arguments properly. 166 | 167 | Maintenance: 168 | * Reduce use of reflection 169 | * Remove unused plugins and jenkins scripts 170 | * Switch to `lein-parent` for managing dependency versions 171 | 172 | ## 1.3.1 173 | 174 | This is a maintenance release. 175 | 176 | * Remove retired :flag option from cli tooling, to eliminate warnings on CLI 177 | invocations. 178 | * Bump to org.clojure/tools.cli 0.3.3. 179 | 180 | ## 1.3.0 181 | 182 | This is a maintenance / minor feature release. 183 | 184 | * [TK-315](https://tickets.puppetlabs.com/browse/TK-315) - update to latest version 185 | of `raynes.fs` to reduce downstream dependency conflicts. 186 | * Add an `absolute-path` fn to replace the one that was removed from raynes.fs 187 | * Add a `normalized-path` fn to replace the one that was removed from raynes.fs 188 | 189 | ## 1.2.0 190 | 191 | * Add `temp-file-name` function, which returns a unique name to a temporary file, 192 | but does not actually create the file. 193 | * Add `with-timeout` macro, which returns a default value if executing an 194 | arbitrary block of code takes longer than a specified timeout. 195 | 196 | ## 1.1.0 197 | 198 | * Add new `walk-leaves` function for applying a function to all of the leaf 199 | nodes of a map 200 | * Add new `zipper?` predicate which can be used to assert that an object 201 | is a clojure zipper. 202 | * Add new `while-let` macro 203 | * Add new `rand-weighted-selection` function 204 | * Add new `to-sentence` variant of string join 205 | 206 | ## 1.0.0 207 | * Promoting previous release to 1.0.0 so that we can be more deliberate about 208 | adhering to semver from now on. 209 | 210 | ## 0.7.3 211 | * Add 'filter-map' function that can be used to filter maps 212 | 213 | ## 0.7.2 214 | * Change `mkdirs!` to allow string as path arg (7097bb3) 215 | * Add a new `dissoc-in` function, for removing data from nested maps. 216 | 217 | ## 0.7.1 218 | * Add a new `to-bool` function, which provides a more tolerant way to coerce 219 | data to booleans 220 | 221 | ## 0.7.0 222 | * Upgrade fs dependency to 1.4.5 (to standardize across projects) 223 | * Add mkdirs! function to create parent directories with better failure reporting 224 | * Move temp file functions from testutils to core 225 | 226 | ## 0.6.0 227 | * Remove SSL utility code, which is now available in [puppetlabs/certificate-authority](https://github.com/puppetlabs/jvm-certificate-authority). 228 | 229 | ## 0.5.4 230 | * Upgrade cheshire dependency to version 5.3.1. 231 | 232 | ## 0.5.3 233 | * .ini parsing utilities now throw an Exception if a key appears in the file(s) more than once. 234 | * Added a `with-no-jvm-shutdown-hooks` macro for running a block of code without any JVM shutdown hooks. 235 | 236 | ## 0.5.2 237 | * Minor change to the cli! function so that, in addition to the data that it already returned, it now also returns a string representation of a banner/usage summary. Callers can use this to display a help message if additional validation of the cli args fails. 238 | * Utility functions added to ssl namespace that allow creation of an SSLContext or a KeyStore/TrustStore directly from the pem files. 239 | * Added some JSON utility functions 240 | * Added a deep-merge utility function 241 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @puppetlabs/dumpling 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Puppet Labs: Clojure 'kitchensink' Library 2 | 3 | This library contains general-purpose utility code that can be used 4 | across all other apps and projects. 5 | 6 | When considering submitting or reviewing contributions to this library, 7 | please try to make sure that the additions are truly general purpose 8 | and likely to be reasonable candidates for re-use in just about any 9 | other project. Things that are domain-specific for any subset of 10 | other Puppet Labs projects should probably not be included in this 11 | library. 12 | 13 | Another consideration: the `core` namespace is prety large an unwieldy 14 | in its current form, and should probably be broken into smaller, more 15 | specific namespaces. If you are contributing new code and see a 16 | reasonable way to break it off into a different namespace (perhaps moving 17 | some of the existing code from core along to the new namespace with it), 18 | please consider doing so. 19 | 20 | # General PL Contribution Guidelines 21 | 22 | Third-party patches are essential for keeping puppet open-source projects 23 | great. We want to keep it as easy as possible to contribute changes that 24 | allow you to get the most out of our projects. There are a few guidelines 25 | that we need contributors to follow so that we can have a chance of keeping on 26 | top of things. For more info, see our canonical guide to contributing: 27 | 28 | [https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md) 29 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppetlabs/kitchensink 2 | 3 | A library of utility functions that are common to several Puppet Labs 4 | clojure projects. 5 | 6 | ## Installation 7 | 8 | Add the following dependency to your `project.clj` file: 9 | 10 | [![Clojars Project](http://clojars.org/puppetlabs/kitchensink/latest-version.svg)](http://clojars.org/puppetlabs/kitchensink) 11 | 12 | ## Using Our Test Utils 13 | 14 | Kitchensink provides [utility code](./test/puppetlabs/kitchensink/) for use in tests. 15 | The code is available in a separate "test" jar that you may depend on by using a classifier in your project dependencies. 16 | 17 | ```clojure 18 | (defproject yourproject "1.0.0" 19 | ... 20 | :profiles {:test {:dependencies [[puppetlabs/kitchensink "x.y.z" :classifier "test"]]}}) 21 | ``` 22 | 23 | ## License 24 | 25 | Copyright © 2013 Puppet Labs 26 | 27 | Distributed under the [Apache License, version 2](http://www.apache.org/licenses/). 28 | 29 | ## Support 30 | 31 | Please log tickets and issues at our [Trapperkeeper JIRA tracker](https://tickets.puppetlabs.com/browse/TK). 32 | -------------------------------------------------------------------------------- /classpath-test/does-not-exist-anywhere-else: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/clj-kitchensink/9e55d93030f709251fdda9979563ad1336658478/classpath-test/does-not-exist-anywhere-else -------------------------------------------------------------------------------- /dev-resources/fixtures/plain-text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor sed viverra ipsum nunc aliquet bibendum enim. Auctor elit sed vulputate mi sit amet mauris commodo quis. Senectus et netus et malesuada fames ac. Ultricies leo integer malesuada nunc vel risus commodo. Nibh tortor id aliquet lectus proin nibh. Varius quam quisque id diam vel quam elementum pulvinar etiam. Laoreet non curabitur gravida arcu ac tortor dignissim convallis aenean. Commodo elit at imperdiet dui accumsan. Molestie nunc non blandit massa enim. Mauris pellentesque pulvinar pellentesque habitant. Non enim praesent elementum facilisis leo vel fringilla est ullamcorper. Arcu non sodales neque sodales ut etiam. Faucibus a pellentesque sit amet porttitor eget dolor. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada proin. Arcu bibendum at varius vel pharetra. Cras fermentum odio eu feugiat pretium. Massa placerat duis ultricies lacus. In mollis nunc sed id semper risus. Ut lectus arcu bibendum at. 2 | 3 | Dictum non consectetur a erat nam at lectus urna. Sed augue lacus viverra vitae congue eu consequat. Vel orci porta non pulvinar neque laoreet. Tellus mauris a diam maecenas sed enim ut. Lobortis mattis aliquam faucibus purus in massa tempor nec. Velit laoreet id donec ultrices tincidunt. Sollicitudin nibh sit amet commodo nulla facilisi. Hendrerit gravida rutrum quisque non tellus. Diam maecenas ultricies mi eget mauris pharetra et ultrices neque. Sem viverra aliquet eget sit amet tellus cras adipiscing. Aenean pharetra magna ac placerat vestibulum lectus. Mattis pellentesque id nibh tortor id aliquet lectus proin nibh. Neque laoreet suspendisse interdum consectetur libero id faucibus nisl. Nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Nec ultrices dui sapien eget. Enim nec dui nunc mattis enim ut tellus. Ultrices eros in cursus turpis massa tincidunt dui ut ornare. Interdum varius sit amet mattis vulputate enim nulla aliquet. 4 | 5 | Tortor consequat id porta nibh venenatis cras. Ac ut consequat semper viverra. Sollicitudin aliquam ultrices sagittis orci a scelerisque. Fringilla phasellus faucibus scelerisque eleifend donec pretium vulputate sapien. Aliquet eget sit amet tellus cras adipiscing enim. Ut ornare lectus sit amet est placerat. Enim facilisis gravida neque convallis a cras. Sagittis vitae et leo duis ut. Ornare lectus sit amet est placerat. Adipiscing elit ut aliquam purus sit. Massa ultricies mi quis hendrerit. Adipiscing at in tellus integer feugiat. A cras semper auctor neque vitae tempus quam. Sed adipiscing diam donec adipiscing tristique risus. Tortor aliquam nulla facilisi cras fermentum odio. Quisque non tellus orci ac auctor. Cras tincidunt lobortis feugiat vivamus at. Suspendisse in est ante in nibh mauris cursus. 6 | 7 | Ut sem nulla pharetra diam. Enim facilisis gravida neque convallis. Euismod quis viverra nibh cras. Viverra suspendisse potenti nullam ac tortor vitae. Id diam maecenas ultricies mi eget mauris. Egestas egestas fringilla phasellus faucibus scelerisque eleifend donec pretium. Ultricies tristique nulla aliquet enim tortor at. Est sit amet facilisis magna. Hendrerit gravida rutrum quisque non tellus orci ac auctor. Mi tempus imperdiet nulla malesuada pellentesque elit eget. Habitant morbi tristique senectus et netus. Purus semper eget duis at tellus at urna condimentum mattis. Magna ac placerat vestibulum lectus mauris. Rutrum tellus pellentesque eu tincidunt tortor aliquam nulla facilisi. Lorem donec massa sapien faucibus et molestie. Mattis aliquam faucibus purus in massa tempor nec feugiat nisl. Ipsum nunc aliquet bibendum enim. 8 | 9 | Auctor eu augue ut lectus arcu bibendum at. Tempor nec feugiat nisl pretium fusce id velit. Magna fermentum iaculis eu non diam phasellus vestibulum lorem sed. Sit amet porttitor eget dolor morbi non arcu risus quis. Fringilla ut morbi tincidunt augue interdum velit. Mattis rhoncus urna neque viverra justo nec ultrices. Sem nulla pharetra diam sit amet nisl suscipit adipiscing. Sit amet cursus sit amet dictum sit amet. Libero nunc consequat interdum varius sit amet mattis vulputate. Lectus magna fringilla urna porttitor. Amet volutpat consequat mauris nunc congue nisi vitae suscipit. In massa tempor nec feugiat nisl pretium fusce id velit. Eu facilisis sed odio morbi quis commodo odio aenean sed. 10 | 11 | Accumsan tortor posuere ac ut consequat. Morbi tincidunt augue interdum velit. Massa tincidunt dui ut ornare lectus sit. Cursus eget nunc scelerisque viverra mauris. Et egestas quis ipsum suspendisse ultrices gravida dictum fusce ut. Urna molestie at elementum eu facilisis sed. Nisi est sit amet facilisis magna etiam tempor. Etiam sit amet nisl purus in mollis nunc sed. In vitae turpis massa sed elementum tempus egestas. Nibh ipsum consequat nisl vel pretium lectus. At ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget. Ac auctor augue mauris augue neque. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Iaculis urna id volutpat lacus laoreet non curabitur gravida arcu. Malesuada nunc vel risus commodo viverra. 12 | 13 | Porttitor lacus luctus accumsan tortor posuere ac ut. Tempus imperdiet nulla malesuada pellentesque elit eget. Ornare massa eget egestas purus viverra accumsan in nisl nisi. Ligula ullamcorper malesuada proin libero. Tempor nec feugiat nisl pretium fusce id velit. Fermentum iaculis eu non diam phasellus vestibulum. Tellus mauris a diam maecenas sed. Nisi scelerisque eu ultrices vitae auctor eu augue. Morbi tristique senectus et netus et malesuada. Tortor aliquam nulla facilisi cras fermentum odio eu feugiat pretium. Pellentesque pulvinar pellentesque habitant morbi tristique senectus. Donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum. Molestie at elementum eu facilisis sed. Tortor id aliquet lectus proin nibh nisl condimentum id venenatis. Sapien et ligula ullamcorper malesuada. Massa sapien faucibus et molestie ac feugiat. Scelerisque purus semper eget duis. Sit amet volutpat consequat mauris nunc congue nisi vitae. Donec massa sapien faucibus et molestie ac feugiat. 14 | 15 | Arcu felis bibendum ut tristique et. Facilisis magna etiam tempor orci eu. Nisi vitae suscipit tellus mauris a diam maecenas sed. Et netus et malesuada fames ac turpis egestas sed tempus. Justo donec enim diam vulputate. Cursus turpis massa tincidunt dui ut ornare lectus. Nullam non nisi est sit amet facilisis magna etiam. Tortor consequat id porta nibh venenatis cras sed felis. Eu turpis egestas pretium aenean pharetra magna ac. Id neque aliquam vestibulum morbi. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Leo a diam sollicitudin tempor id. Sem fringilla ut morbi tincidunt. Non consectetur a erat nam at lectus urna duis. Neque viverra justo nec ultrices dui sapien eget mi. Fermentum leo vel orci porta non pulvinar neque. Sem fringilla ut morbi tincidunt augue interdum velit. Aliquam ut porttitor leo a. Scelerisque viverra mauris in aliquam. Elementum facilisis leo vel fringilla est ullamcorper eget nulla. 16 | 17 | Tortor vitae purus faucibus ornare suspendisse sed nisi. Aenean vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Leo vel orci porta non. Sed augue lacus viverra vitae congue eu. Quam nulla porttitor massa id neque aliquam vestibulum morbi. Justo donec enim diam vulputate. Tristique senectus et netus et malesuada fames ac turpis egestas. Aenean et tortor at risus viverra adipiscing at. Sed elementum tempus egestas sed. Eget egestas purus viverra accumsan in nisl. 18 | 19 | Scelerisque purus semper eget duis at tellus at. Amet tellus cras adipiscing enim eu turpis egestas pretium. Ullamcorper eget nulla facilisi etiam dignissim diam quis enim lobortis. Vel pharetra vel turpis nunc eget. Volutpat sed cras ornare arcu dui. Convallis aenean et tortor at. A arcu cursus vitae congue mauris rhoncus aenean vel. Elit at imperdiet dui accumsan sit. Egestas maecenas pharetra convallis posuere. Tortor at auctor urna nunc id cursus metus aliquam eleifend. Eget felis eget nunc lobortis mattis aliquam faucibus. 20 | 21 | Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Aliquet eget sit amet tellus cras adipiscing enim. Dignissim convallis aenean et tortor. Dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Sem viverra aliquet eget sit amet tellus. Phasellus egestas tellus rutrum tellus pellentesque eu. Mattis aliquam faucibus purus in massa tempor nec. Ac turpis egestas maecenas pharetra convallis posuere. Pellentesque habitant morbi tristique senectus. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Purus sit amet luctus venenatis. Sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque eu. Blandit libero volutpat sed cras ornare. Et netus et malesuada fames ac turpis egestas sed tempus. 22 | 23 | Quis ipsum suspendisse ultrices gravida dictum fusce. Eros donec ac odio tempor orci. Ipsum suspendisse ultrices gravida dictum fusce ut placerat orci nulla. Cras sed felis eget velit aliquet sagittis. Risus quis varius quam quisque id diam vel quam. Leo duis ut diam quam nulla porttitor massa id neque. Nec nam aliquam sem et tortor. Nascetur ridiculus mus mauris vitae ultricies leo integer. Viverra vitae congue eu consequat ac felis donec et odio. Ac tortor vitae purus faucibus ornare suspendisse sed. Adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus urna. Aliquet lectus proin nibh nisl. Risus in hendrerit gravida rutrum quisque non. Nec feugiat in fermentum posuere urna. 24 | 25 | Sit amet est placerat in egestas. Nulla posuere sollicitudin aliquam ultrices sagittis. Morbi blandit cursus risus at. Aenean et tortor at risus viverra adipiscing at in. Sagittis orci a scelerisque purus semper eget. Dui id ornare arcu odio. Ultrices mi tempus imperdiet nulla malesuada pellentesque. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus non. Condimentum vitae sapien pellentesque habitant morbi tristique. Sed viverra ipsum nunc aliquet bibendum enim facilisis. Neque gravida in fermentum et sollicitudin ac. Justo eget magna fermentum iaculis eu non diam phasellus vestibulum. Consectetur libero id faucibus nisl tincidunt eget nullam. Pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus. Gravida in fermentum et sollicitudin ac orci phasellus. Dolor sed viverra ipsum nunc aliquet bibendum enim facilisis gravida. Ipsum dolor sit amet consectetur adipiscing elit duis. 26 | 27 | Bibendum enim facilisis gravida neque convallis a cras semper. Habitant morbi tristique senectus et netus et malesuada fames ac. Eget lorem dolor sed viverra ipsum nunc aliquet. Facilisis gravida neque convallis a cras. Dolor sit amet consectetur adipiscing elit pellentesque. Nunc sed blandit libero volutpat sed cras ornare arcu dui. Aliquet nec ullamcorper sit amet risus nullam eget felis. Blandit massa enim nec dui nunc. Ut enim blandit volutpat maecenas. Varius duis at consectetur lorem donec massa sapien. 28 | 29 | Nunc non blandit massa enim nec dui nunc mattis. Nibh tellus molestie nunc non blandit. Purus in mollis nunc sed. Viverra orci sagittis eu volutpat odio facilisis mauris. Ultrices gravida dictum fusce ut placerat. Vitae et leo duis ut diam quam nulla porttitor massa. Et egestas quis ipsum suspendisse ultrices gravida dictum fusce. In eu mi bibendum neque egestas congue quisque egestas diam. Et odio pellentesque diam volutpat commodo sed egestas egestas fringilla. Blandit turpis cursus in hac habitasse platea dictumst. Proin libero nunc consequat interdum. Non blandit massa enim nec dui nunc mattis enim ut. Vel orci porta non pulvinar neque laoreet suspendisse interdum consectetur. 30 | 31 | Viverra maecenas accumsan lacus vel facilisis. Mauris cursus mattis molestie a iaculis. Vestibulum sed arcu non odio euismod lacinia at quis. Proin libero nunc consequat interdum. Cras fermentum odio eu feugiat pretium. Dolor morbi non arcu risus. Sit amet mauris commodo quis imperdiet massa tincidunt. Dolor sit amet consectetur adipiscing. Eget mauris pharetra et ultrices neque ornare aenean. Nulla aliquet porttitor lacus luctus. Nulla at volutpat diam ut venenatis tellus in metus vulputate. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis. 32 | 33 | Nulla pharetra diam sit amet. Et sollicitudin ac orci phasellus egestas tellus rutrum. Vehicula ipsum a arcu cursus vitae. Magna sit amet purus gravida quis blandit turpis cursus. Nunc congue nisi vitae suscipit tellus. Felis eget nunc lobortis mattis aliquam faucibus purus in massa. Id diam vel quam elementum pulvinar. Lacus viverra vitae congue eu consequat ac felis donec et. Facilisi cras fermentum odio eu feugiat pretium nibh ipsum. Duis at tellus at urna condimentum mattis. Ut faucibus pulvinar elementum integer enim neque volutpat ac tincidunt. Egestas diam in arcu cursus. Sollicitudin nibh sit amet commodo. Pharetra diam sit amet nisl suscipit. Et tortor consequat id porta nibh venenatis cras sed. 34 | 35 | At augue eget arcu dictum varius duis. Magna fermentum iaculis eu non diam phasellus. Facilisi nullam vehicula ipsum a arcu cursus vitae congue mauris. Varius quam quisque id diam vel quam elementum. Amet nisl suscipit adipiscing bibendum est ultricies integer. Id diam maecenas ultricies mi eget mauris pharetra et. Nulla porttitor massa id neque. Aliquet risus feugiat in ante metus. Etiam erat velit scelerisque in. Consequat id porta nibh venenatis cras sed felis eget. Ut tortor pretium viverra suspendisse potenti. Nunc sed blandit libero volutpat sed. Orci sagittis eu volutpat odio facilisis mauris sit amet. Ullamcorper malesuada proin libero nunc consequat interdum varius sit amet. Accumsan tortor posuere ac ut consequat semper viverra nam. Tincidunt ornare massa eget egestas purus. Mauris cursus mattis molestie a iaculis at. 36 | 37 | Sodales ut eu sem integer vitae justo eget magna fermentum. Purus in mollis nunc sed id. Mattis molestie a iaculis at erat. Et sollicitudin ac orci phasellus egestas. Et pharetra pharetra massa massa ultricies mi. Nisl condimentum id venenatis a. Libero id faucibus nisl tincidunt eget nullam. Tincidunt nunc pulvinar sapien et ligula. Vulputate mi sit amet mauris commodo quis. Suscipit tellus mauris a diam maecenas sed enim ut. Tincidunt eget nullam non nisi est sit amet. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. In aliquam sem fringilla ut morbi tincidunt augue. Nec feugiat nisl pretium fusce id. Nam libero justo laoreet sit amet cursus sit. Mi proin sed libero enim sed faucibus turpis in eu. At elementum eu facilisis sed odio morbi quis commodo. Sed odio morbi quis commodo. Lacus vel facilisis volutpat est velit egestas dui id ornare. 38 | 39 | Orci porta non pulvinar neque laoreet suspendisse. Sollicitudin nibh sit amet commodo nulla. Sit amet volutpat consequat mauris nunc congue. Fermentum leo vel orci porta non pulvinar. Risus ultricies tristique nulla aliquet enim tortor at auctor. Dolor magna eget est lorem ipsum dolor. Dictum varius duis at consectetur lorem donec. Sed cras ornare arcu dui vivamus arcu felis bibendum. Lacus sed turpis tincidunt id aliquet. Donec et odio pellentesque diam volutpat commodo sed egestas egestas. Est velit egestas dui id ornare arcu. Mauris in aliquam sem fringilla ut. Nisl nunc mi ipsum faucibus vitae. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper. Massa tincidunt dui ut ornare lectus. Neque aliquam vestibulum morbi blandit cursus risus at ultrices. -------------------------------------------------------------------------------- /dev-resources/fixtures/plain-text.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/clj-kitchensink/9e55d93030f709251fdda9979563ad1336658478/dev-resources/fixtures/plain-text.txt.gz -------------------------------------------------------------------------------- /dev-resources/fixtures/tar-contents.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/clj-kitchensink/9e55d93030f709251fdda9979563ad1336658478/dev-resources/fixtures/tar-contents.tar -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to clj-utils 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject puppetlabs/kitchensink "3.4.1-SNAPSHOT" 2 | :description "Clojure utility functions" 3 | :license {:name "Apache License, Version 2.0" 4 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 5 | 6 | :min-lein-version "2.9.1" 7 | 8 | :parent-project {:coords [puppetlabs/clj-parent "5.6.14"] 9 | :inherit [:managed-dependencies]} 10 | 11 | ;; Abort when version ranges or version conflicts are detected in 12 | ;; dependencies. Also supports :warn to simply emit warnings. 13 | ;; requires lein 2.2.0+. 14 | :pedantic? :abort 15 | 16 | :dependencies [[org.clojure/clojure] 17 | [org.clojure/tools.logging] 18 | [org.clojure/tools.cli] 19 | 20 | [org.apache.commons/commons-compress] 21 | [clj-time] 22 | [clj-commons/fs] 23 | [slingshot] 24 | [cheshire] 25 | 26 | [org.ini4j/ini4j "0.5.4"] 27 | [org.tcrawley/dynapath] 28 | [digest "1.4.3"]] 29 | 30 | ;; By declaring a classifier here and a corresponding profile below we'll get an additional jar 31 | ;; during `lein jar` that has all the code in the test/ directory. Downstream projects can then 32 | ;; depend on this test jar using a :classifier in their :dependencies to reuse the test utility 33 | ;; code that we have. 34 | :classifiers [["test" :testutils]] 35 | 36 | :profiles {:testutils {:source-paths ^:replace ["test"]}} 37 | 38 | ;; this plugin is used by jenkins jobs to interrogate the project version 39 | :plugins [[lein-project-version "0.1.0"] 40 | [jonase/eastwood "1.2.2" :exclusions [org.clojure/clojure]] 41 | [lein-parent "0.3.7"]] 42 | 43 | :eastwood {:ignored-faults {:unused-ret-vals {puppetlabs.kitchensink.classpath [{:line 93}]} 44 | :deprecations {puppetlabs.kitchensink.classpath [{:line 66} 45 | {:line 91}] 46 | puppetlabs.kitchensink.core true 47 | puppetlabs.kitchensink.core-test true} 48 | :reflection {puppetlabs.kitchensink.file [{:line 62}] 49 | puppetlabs.kitchensink.core [{:line 929}]} 50 | :constant-test {puppetlabs.kitchensink.core-test [{:line 726} 51 | {:line 733}]}} 52 | :continue-on-exception true} 53 | 54 | :test-selectors {:default (complement :slow) 55 | :slow :slow} 56 | 57 | :deploy-repositories [["releases" {:url "https://clojars.org/repo" 58 | :username :env/clojars_jenkins_username 59 | :password :env/clojars_jenkins_password 60 | :sign-releases false}] 61 | ["snapshots" "http://nexus.delivery.puppetlabs.net/content/repositories/snapshots/"]]) 62 | -------------------------------------------------------------------------------- /src/puppetlabs/kitchensink/classpath.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.classpath 2 | (:import (java.net URLClassLoader URL)) 3 | (:require [clojure.java.io :refer [file Coercions]] 4 | [dynapath.util :as dp]) 5 | (:refer-clojure :exclude (add-classpath))) 6 | 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | ;;;; Here, we have copied `add-classpath` out of pomegranate 9 | ;;;; (https://github.com/cemerick/pomegranate). 10 | ;;;; 11 | ;;;; We did this because we needed a corollary to the (deprecated) 12 | ;;;; `add-classpath` function in clojure.core, 13 | ;;;; but we did not want to pull pomegranate's rather large dependency tree 14 | ;;;; (we tried excluding as many of its dependencies as possible, but only 15 | ;;;; a small part of its dependency tree was exclude-able). 16 | ;;;; 17 | ;;;; There are no tests for these functions because pomegranate does not 18 | ;;;; contain any tests for them. 19 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 20 | 21 | (defn- classloader-hierarchy 22 | "Returns a seq of classloaders, with the tip of the hierarchy first. 23 | Uses the current thread context ClassLoader as the tip ClassLoader 24 | if one is not provided." 25 | ([] (classloader-hierarchy (. (Thread/currentThread) getContextClassLoader))) 26 | ([tip] 27 | (->> tip 28 | (iterate #(.getParent ^ClassLoader %)) 29 | (take-while boolean)))) 30 | 31 | (defn- modifiable-classloader? 32 | "Returns true iff the given ClassLoader is of a type that satisfies 33 | the dynapath.dynamic-classpath/DynamicClasspath protocol, and it can 34 | be modified." 35 | [cl] 36 | (dp/addable-classpath? cl)) 37 | 38 | (defn- ensure-modifiable-classloader 39 | "Check if there is a modifiable classloader in the current hierarchy, and add 40 | one if not." 41 | [] 42 | (let [classloader (.. Thread currentThread getContextClassLoader)] 43 | (when (not-any? modifiable-classloader? (classloader-hierarchy classloader)) 44 | (let [new-cl (clojure.lang.DynamicClassLoader. classloader)] 45 | (.. Thread currentThread (setContextClassLoader new-cl)))))) 46 | 47 | (defn add-classpath 48 | "A corollary to the (deprecated) `add-classpath` in clojure.core. This implementation 49 | requires a java.io.File or String path to a jar file or directory, and will attempt 50 | to add that path to the right classloader (with the search rooted at the current 51 | thread's context classloader). 52 | 53 | Because this function is a replacement for `add-classpath` in clojure.core, 54 | if you simply `:require` this namespace and then `:refer` to this function, you 55 | will get the following warning: 56 | 57 | WARNING: add-classpath already refers to: #'clojure.core/add-classpath in 58 | namespace: [...], being replaced by: 59 | #'puppetlabs.kitchensink.classpath/add-classpath 60 | 61 | You can avoid this by referencing this function through its namespace. 62 | 63 | This function is copied out of the 'pomegranate' library 64 | (https://github.com/cemerick/pomegranate)." 65 | ([jar-or-dir classloader] 66 | (when-not (dp/add-classpath-url classloader (.toURL (file jar-or-dir))) 67 | (throw (IllegalStateException. (str classloader " is not a modifiable classloader"))))) 68 | ([jar-or-dir] 69 | (ensure-modifiable-classloader) 70 | (let [classloaders (classloader-hierarchy)] 71 | (if-let [cl (last (filter modifiable-classloader? classloaders))] 72 | (add-classpath jar-or-dir cl) 73 | (throw (IllegalStateException. (str "Could not find a suitable classloader to modify from " 74 | classloaders))))))) 75 | 76 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 77 | ;;;; end of functions copied from pomegranate (see note above) 78 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 79 | 80 | (defn jar-or-dir-to-url 81 | "Given the path to a jar file or a directory, return a `java.net.URL` 82 | object suitable for using with a `URLClassLoader`" 83 | [jar-or-dir] 84 | {:pre [(satisfies? Coercions jar-or-dir)] 85 | :post [(instance? URL %)]} 86 | ;; explicitly calling `getAbsoluteFile` causes relative file paths to 87 | ;; be evaluated relative to the system property `user.dir` (which is 88 | ;; usually set to the current working directory). This is useful for 89 | ;; tests and other code that wants to emulated changing the working 90 | ;; directory. 91 | (.. (file jar-or-dir) getAbsoluteFile toURL)) 92 | 93 | (defmacro with-additional-classpath-entries 94 | "This macro takes a list of paths as an argument. It then temporarily 95 | overrides the classpath to include the specified paths; the original 96 | classpath is restored prior to returning." 97 | [jars-and-dirs & body] 98 | `{:pre [(coll? ~jars-and-dirs) 99 | (every? (partial satisfies? Coercions) ~jars-and-dirs)]} 100 | `(let [orig-loader# (.. Thread currentThread getContextClassLoader) 101 | temp-loader# (URLClassLoader. 102 | (into-array 103 | URL 104 | (map jar-or-dir-to-url ~jars-and-dirs)) 105 | orig-loader#)] 106 | (try 107 | (.. Thread currentThread (setContextClassLoader temp-loader#)) 108 | ~@body 109 | (finally (.. Thread currentThread (setContextClassLoader orig-loader#)))))) 110 | -------------------------------------------------------------------------------- /src/puppetlabs/kitchensink/core.clj: -------------------------------------------------------------------------------- 1 | ;; ## "The Kitchen Sink" 2 | ;; 3 | ;; Pretty much everything in here should _probably_ be organized into 4 | ;; proper namespaces, or perhaps even separate libraries 5 | ;; altogether. But who has time for that? 6 | 7 | (ns puppetlabs.kitchensink.core 8 | (:refer-clojure :exclude [boolean? uuid?]) 9 | (:require [clj-time.coerce :as time-coerce] 10 | [clj-time.core :as time] 11 | [clj-time.format :as time-format] 12 | [clojure.java.io :as io] 13 | [clojure.pprint :as pprint] 14 | [clojure.set :as set] 15 | [clojure.string :as string] 16 | [clojure.tools.cli :as cli] 17 | [clojure.tools.logging :as log] 18 | [digest] 19 | [me.raynes.fs :as fs] 20 | [slingshot.slingshot :refer [throw+]]) 21 | (:import (java.io File Reader StringWriter) 22 | (java.time ZoneId ZoneOffset ZonedDateTime) 23 | (java.time.format DateTimeFormatter) 24 | javax.naming.ldap.LdapName 25 | (javax.naming.ldap Rdn) 26 | (org.ini4j BasicProfileSection Config Ini))) 27 | 28 | (defn error-map 29 | [kind message] 30 | {:kind kind 31 | :msg message}) 32 | 33 | ;; ## Type checking 34 | 35 | (defn array? 36 | "Returns true if `x` is an array" 37 | [x] 38 | (some-> x 39 | (class) 40 | (.isArray))) 41 | 42 | (defn datetime? 43 | "Predicate returning whether or not the supplied object is 44 | convertible to a Joda DateTime" 45 | [x] 46 | (and 47 | (satisfies? time-coerce/ICoerce x) 48 | (time-coerce/to-date-time x))) 49 | 50 | (defn boolean? 51 | "Returns true if the value is a boolean" 52 | [value] 53 | (instance? Boolean value)) 54 | 55 | (defn regexp? 56 | "Returns true if the type is a regexp pattern" 57 | [regexp] 58 | {:post [(boolean? %)]} 59 | (instance? java.util.regex.Pattern regexp)) 60 | 61 | (defn zipper? 62 | "Checks to see if the object has zip/make-node metadata on it (confirming it 63 | to be a zipper." 64 | [obj] 65 | (contains? (meta obj) :zip/make-node)) 66 | 67 | ;; ## String utilities 68 | 69 | (defn strict-parse-bool 70 | "Parse a string and return its boolean value; throws an exception if the String 71 | does not match `\"true\"` or `\"false\"` (case-insensitive)." 72 | [^String s] 73 | {:pre [(string? s)] 74 | :post [(boolean? %)]} 75 | (condp = (.toLowerCase s) 76 | "true" true 77 | "false" false 78 | (throw+ (error-map ::parse-error 79 | (format "Unable to parse '%s' to a boolean" s))))) 80 | 81 | (defn parse-bool 82 | "Parse a string and return its boolean value." 83 | [s] 84 | {:pre [(or (nil? s) (string? s))] 85 | :post [(boolean? %)]} 86 | (Boolean/parseBoolean s)) 87 | 88 | (defn to-bool 89 | "Converts the argument to a boolean. The behavior is as follows: 90 | 91 | * If the argument is a Boolean, it is simply returned. 92 | * If the argument is a String, returns the Boolean `true` if the String 93 | matches `\"true\"` (case insensitive), or `false` if the String matches 94 | `\"false\"` (case insensitive). Throws an exception otherwise. 95 | * If the argument is `nil`, returns false." 96 | [val] 97 | {:pre [((some-fn boolean? string? nil?) val)] 98 | :post [(boolean? %)]} 99 | (cond 100 | (boolean? val) val 101 | (string? val) (strict-parse-bool val) 102 | (nil? val) false)) 103 | 104 | (defn string-contains? 105 | "Returns true if `s` has the `substring` in it" 106 | [^String substring ^String s] 107 | {:pre [(string? s) 108 | (string? substring)]} 109 | (>= (.indexOf s substring) 0)) 110 | 111 | (defn true-str? 112 | "Return true if the string contains true" 113 | [^String s] 114 | (.equalsIgnoreCase "true" s)) 115 | 116 | (defn pprint-to-string [x] 117 | (let [w (StringWriter.)] 118 | (pprint/pprint x w) 119 | (.toString w))) 120 | 121 | (defn to-sentence 122 | "Join the given strings as they would be listed in a sentence (using an Oxford 123 | comma if there are three or more strings). 124 | Examples: 125 | [\"apple\"] => \"apple\" 126 | [\"apple\" \"orange\"] => \"apple and orange\" 127 | [\"apple\" \"orange\" \"banana\"] => \"apple, orange, and banana\"" 128 | [strings] 129 | (let [num-strings (count strings)] 130 | (cond 131 | (empty? strings) 132 | "" 133 | 134 | (= num-strings 1) 135 | (first strings) 136 | 137 | (= num-strings 2) 138 | (str (first strings) " and " (second strings)) 139 | 140 | (= num-strings 3) 141 | (let [[s0 s1 s2] strings] 142 | (str s0 ", " s1 ", and " s2)) 143 | 144 | (> num-strings 3) 145 | (str (first strings) ", " (to-sentence (rest strings)))))) 146 | 147 | (defn key->str 148 | "Convert a keyword to a string, stripping the leading : character. This is 149 | distinct from the `name` function as it will preserve the 'namespace' portion 150 | before a slash. If the key is already a string, it is returned unmodified." 151 | [kw] 152 | (if (keyword? kw) 153 | (subs (str kw) 1) 154 | kw)) 155 | 156 | ;; ## I/O 157 | 158 | (defn lines 159 | "Returns a sequence of lines from the given filename" 160 | [filename] 161 | (with-open [file-reader (io/reader (fs/file filename))] 162 | ;; line seq is lazy and file-reader gets closed 163 | (doall (line-seq file-reader)))) 164 | 165 | (defn mkdirs! 166 | "Given a path (may be a File or a string), creates a directory (including any 167 | missing parent directories). Throws a slingshot exception with a meaningful 168 | error message if the directory cannot be created. 169 | 170 | (The reason for the existence of this function is that the Java File.mkdirs 171 | method only returns a boolean indicating whether the directory was created; 172 | if you get back a `false`, you have no idea whether it failed due to permission 173 | errors, or the path being invalid in some way, or the directory already exists.) 174 | 175 | The slingshot exception will look like this: 176 | 177 | `{:kind :puppetlabs.kitchensink.core/io-error 178 | :msg \"Parent directory '/foo/bar' is not writable\"}`" 179 | [path] 180 | {:pre [((some-fn #(instance? File %) string?) path)] 181 | :post [(fs/directory? path)]} 182 | (let [path-as-file (fs/file path)] 183 | (if (fs/file? path-as-file) 184 | (throw+ (error-map ::io-error 185 | (format "Path '%s' is a file" path))) 186 | (doseq [^File dir (reverse (cons path-as-file (fs/parents path-as-file)))] 187 | (when-not (fs/exists? dir) 188 | (let [parent (.getParentFile dir)] 189 | (when (fs/file? parent) 190 | (throw+ (error-map ::io-error 191 | (format "Parent directory '%s' is a file" 192 | parent)))) 193 | 194 | (when-not (.canWrite parent) 195 | (throw+ (error-map ::io-error 196 | (format "Parent directory '%s' is not writable" 197 | parent)))) 198 | 199 | (let [success (.mkdir dir)] 200 | (when-not success 201 | (throw+ (error-map ::io-error 202 | (format "Unable to create directory '%s'" 203 | parent))))))))))) 204 | 205 | ;; ## Math 206 | 207 | (defn quotient 208 | "Performs division on the supplied arguments, substituting `default` 209 | when the divisor is 0" 210 | ([dividend divisor] 211 | (quotient dividend divisor 0)) 212 | ([dividend divisor default] 213 | (if (zero? divisor) 214 | default 215 | (/ dividend divisor)))) 216 | 217 | ;; ## Numerics 218 | 219 | (defn parse-int 220 | "Parse a string `s` as an integer, returning nil if the string doesn't 221 | contain an integer." 222 | [s] 223 | {:pre [(string? s)] 224 | :post [(or (integer? %) (nil? %))]} 225 | (try (Integer/parseInt s) 226 | (catch NumberFormatException _e 227 | nil))) 228 | 229 | (defn safe-parse-int 230 | "Accept strings or integers. If string, parse to an integer, otherwise return the integer" 231 | [s] 232 | (if (integer? s) 233 | s 234 | (parse-int s))) 235 | 236 | (defn parse-float 237 | "Parse a string `s` as a float, returning nil if the string doesn't 238 | contain a float" 239 | [s] 240 | {:pre [(string? s)] 241 | :post [(or (float? %) (nil? %))]} 242 | (try (Float/parseFloat s) 243 | (catch NumberFormatException _e 244 | nil))) 245 | 246 | (defn parse-number 247 | "Converts a string `s` to a number, by attempting to parse it as an integer 248 | and then as a float. Returns nil if the string isn't numeric." 249 | [s] 250 | {:pre [(string? s)] 251 | :post [(or (number? %) (nil? %))]} 252 | ((some-fn parse-int parse-float) s)) 253 | 254 | ;; ## Randomness 255 | 256 | (defn rand-weighted-selection 257 | "Given alternating numeric weights and values, produces a randomly-selected 258 | value according to the weights, which should sum to one. If the weights sum 259 | to less than one, then the last value will have its weight adjusted upwards 260 | accordingly. If the weights sum to more than one, then values after the 261 | cumulative sum of the weights exceeds one will never be selected." 262 | [& weights-and-values] 263 | {:pre [(even? (count weights-and-values)) 264 | (every? number? (take-nth 2 weights-and-values))]} 265 | (let [weights (take-nth 2 weights-and-values) 266 | values (-> (take-nth 2 (drop 1 weights-and-values)) butlast) 267 | cutoffs (-> (reductions + weights) butlast) 268 | selected? (let [selected-cutoff (rand)] 269 | #(>= % selected-cutoff)) 270 | selected (filter (comp selected? first) 271 | (map vector cutoffs values))] 272 | (if (empty? selected) 273 | (last weights-and-values) 274 | (-> (first selected) second)))) 275 | 276 | (def ascii-character-sets 277 | (let [concatv (comp vec concat) 278 | ALPHA (mapv char (range 65 91)) 279 | alpha (mapv char (range 97 123)) 280 | digits (mapv char (range 48 58)) 281 | symbols (concatv (map char (range 33 48)) 282 | (map char (range 91 97)) 283 | (map char (range 123 127)))] 284 | {:alpha (concatv alpha ALPHA) 285 | :alpha-lower alpha 286 | :alpha-upper ALPHA 287 | :alpha-digits (concatv alpha ALPHA digits) 288 | :alpha-digits-symbols (concatv alpha ALPHA digits symbols) 289 | :symbols symbols 290 | :digits digits})) 291 | 292 | (defn rand-str 293 | "Produces a random string of length n, drawn from the given collection of 294 | characters. The following keywords may be used in place of a character 295 | collection: 296 | :alpha - [a-zA-Z] 297 | :alpha-lower - [a-z] 298 | :alpha-upper - [A-Z] 299 | :alpha-digits - [a-zA-Z0-9] 300 | :alpha-digits-symbols - all printable ASCII characters besides space 301 | :symbols - all visible, non-alpha-numeric ASCII characters (no space) 302 | :digits - [0-9] 303 | If no character collection or keyword is provided, :alpha-digits-symbols is 304 | used by default." 305 | ([n] (rand-str :alpha-digits-symbols n)) 306 | ([characters n] 307 | (let [char-coll (cond 308 | (and (keyword? characters) (contains? ascii-character-sets characters)) 309 | (get ascii-character-sets characters) 310 | 311 | (keyword? characters) 312 | (throw (IllegalArgumentException. 313 | (str characters " is not a recognized character collection keyword"))) 314 | 315 | :else (vec characters))] 316 | (apply str (repeatedly n #(rand-nth char-coll)))))) 317 | 318 | ;; ## Collection operations 319 | 320 | (defn symmetric-difference 321 | "Computes the symmetric set/difference between 2 sets" 322 | [s1 s2] 323 | (set/union (set/difference s1 s2) (set/difference s2 s1))) 324 | 325 | (defn as-collection 326 | "Returns the item wrapped in a collection, if it's not one 327 | already. Returns a list by default, or you can use a constructor func 328 | as the second arg." 329 | ([item] 330 | (as-collection item list)) 331 | ([item constructor] 332 | {:post [(coll? %)]} 333 | (if (coll? item) 334 | item 335 | (constructor item)))) 336 | 337 | (defn seq-contains? 338 | "True if seq contains elm" 339 | [seq elm] 340 | (some #(= elm %) seq)) 341 | 342 | (defn enumerate 343 | "Returns a lazy sequence consisting of 0 and the first item of coll, 344 | followed by 1 and the second item in coll, etc, until coll is 345 | exhausted." 346 | [coll] 347 | (map-indexed vector coll)) 348 | 349 | (def excludes? 350 | "Inverse of `contains?`. Returns false if key is present in the given collectoin, 351 | otherwise returns true." 352 | (complement contains?)) 353 | 354 | (defn contains-some 355 | "If coll `contains?` any of the keys in ks, returns the first such 356 | key. Otherwise returns nil." 357 | [coll ks] 358 | (some #(when (contains? coll %) %) ks)) 359 | 360 | (defn excludes-some 361 | "If coll `excludes?` any of the keys in ks, returns the first such 362 | key. Otherwise returns nil." 363 | [coll ks] 364 | (some #(when (excludes? coll %) %) ks)) 365 | 366 | (defn mapvals 367 | "Return map `m`, with each value transformed by function `f`. 368 | 369 | You may also provide an optional list of keys `ks`; if provided, only the 370 | specified keys will be modified." 371 | ([f m] 372 | (into {} (for [[k v] m] [k (f v)]))) 373 | ([f ks m] 374 | ;; would prefer to share code between the two implementations here, but 375 | ;; the `into` is much faster for the base case and the reduce is much 376 | ;; faster for any case where we're operating on a subset of the keys. 377 | ;; It seems like `select-keys` is fairly expensive. 378 | (reduce (fn [m k] (update-in m [k] f)) m ks))) 379 | 380 | (defn mapkeys 381 | "Return map `m`, with each key transformed by function `f`" 382 | [f m] 383 | (into {} (concat (for [[k v] m] 384 | [(f k) v])))) 385 | 386 | (defn maptrans 387 | "Return map `m`, with values transformed according to the key-to-function 388 | mappings specified in `keys-fns`. `keys-fns` should be a map whose keys 389 | are lists of keys from `m`, and whose values are functions to apply to those 390 | keys. 391 | 392 | Example: `(maptrans {[:a, :b] inc [:c] dec} {:a 1 :b 1 :c 1})` yields `{:a 2, :c 0, :b 2}`" 393 | [keys-fns m] 394 | {:pre [(map? keys-fns) 395 | (every? (fn [[ks fn]] (and (coll? ks) (ifn? fn))) keys-fns) 396 | (map? m)]} 397 | (let [ks (keys keys-fns)] 398 | (reduce (fn [m k] (mapvals (keys-fns k) k m)) m ks))) 399 | 400 | (defn dissoc-if-nil 401 | "Given a map and a key, checks to see if the value for the key is `nil`; if so, 402 | returns a modified map with the specified key removed. If the value is not `nil`, 403 | simply returns the original map." 404 | ([m k] 405 | {:pre [(map? m)] 406 | :post [(map? %)]} 407 | (if (nil? (m k)) 408 | (dissoc m k) 409 | m)) 410 | ([m k & ks] 411 | (let [ret (dissoc-if-nil m k)] 412 | (if ks 413 | (recur ret (first ks) (next ks)) 414 | ret)))) 415 | 416 | (defn dissoc-in 417 | "Dissociates an entry from a nested map. ks is a sequence of keys. Any empty maps that result 418 | will not be present in the new map." 419 | [m [k & ks]] 420 | (when m 421 | (if-let [res (and ks (dissoc-in (m k) ks))] 422 | (assoc m k res) 423 | (let [res (dissoc m k)] 424 | (when-not (empty? res) 425 | res))))) 426 | 427 | (defn walk-leaves 428 | "Walk a map applying a function to all leaf nodes" 429 | [m f] 430 | (mapvals #(if (map? %) (walk-leaves % f) (f %)) m)) 431 | 432 | (defn merge-with-key 433 | "Returns a map that consists of the rest of the maps conj-ed onto 434 | the first. If a key `k` occurs in more than one map, the mapping(s) 435 | from the latter (left-to-right) will be combined with the mapping in 436 | the result by calling (f k val-in-result val-in-latter)." 437 | {:added "1.0" 438 | :static true} 439 | [f & maps] 440 | (when (some identity maps) 441 | (let [merge-entry (fn [m e] 442 | (let [k (key e) v (val e)] 443 | (if (contains? m k) 444 | (assoc m k (f k (get m k) v)) 445 | (assoc m k v)))) 446 | merge2 (fn [m1 m2] 447 | (reduce merge-entry (or m1 {}) (seq m2)))] 448 | (reduce merge2 maps)))) 449 | 450 | (defn deep-merge 451 | "Deeply merges maps so that nested maps are combined rather than replaced. 452 | 453 | For example: 454 | (deep-merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}}) 455 | ;;=> {:foo {:bar :baz, :fuzz :buzz}} 456 | 457 | ;; contrast with clojure.core/merge 458 | (merge {:foo {:bar :baz}} {:foo {:fuzz :buzz}}) 459 | ;;=> {:foo {:fuzz :quzz}} ; note how last value for :foo wins" 460 | [& vs] 461 | (if (every? map? vs) 462 | (apply merge-with deep-merge vs) 463 | (last vs))) 464 | 465 | (defn deep-merge-with 466 | "Deeply merges like `deep-merge`, but uses `f` to produce a value from the 467 | conflicting values for a key in multiple maps." 468 | [f & vs] 469 | (if (every? map? vs) 470 | (apply merge-with (partial deep-merge-with f) vs) 471 | (apply f vs))) 472 | 473 | (defn deep-merge-with-keys* 474 | "Helper function for deep-merge-with-keys" 475 | [f ks & vs] 476 | (if (every? map? vs) 477 | (apply merge-with-key 478 | (fn [k & vs] (apply deep-merge-with-keys* f (conj ks k) vs)) 479 | vs) 480 | (apply f ks vs))) 481 | 482 | (defn deep-merge-with-keys 483 | "Deeply merges like `deep-merge`, but uses `f` to produce a value from the 484 | conflicting values for a key path `ks` that appears in multiple maps, by calling 485 | `(f ks val-in-result val-in-latter)`." 486 | [f & vs] 487 | (apply deep-merge-with-keys* f [] vs)) 488 | 489 | (defn keyset 490 | "Returns the set of keys from the supplied map" 491 | [m] 492 | {:pre [(map? m)] 493 | :post [(set? %)]} 494 | (set (keys m))) 495 | 496 | (defn valset 497 | "Returns the set of values from the supplied map" 498 | [m] 499 | {:pre [(map? m)] 500 | :post [(set? %)]} 501 | (set (vals m))) 502 | 503 | (def select-values 504 | "Returns the sequence of values from the map for the entries with the specified keys" 505 | (comp vals select-keys)) 506 | 507 | (defn filter-map 508 | "Like 'filter', but works on maps. Returns a map containing the 509 | key-value pairs in 'm' for which 'pred' returns a truth-y value. 510 | 'pred' must be a function which takes two arguments." 511 | [pred m] 512 | (reduce 513 | (fn [result [key value]] 514 | (if (pred key value) 515 | (assoc result key value) 516 | result)) 517 | {} 518 | m)) 519 | 520 | (defn missing? 521 | "Inverse of contains? that supports multiple keys. Will return true if all items are 522 | missing from the collection, false otherwise. 523 | 524 | Example: 525 | 526 | ;; Returns true, as :z :f :h are all missing 527 | (missing? {:a 'a' :b 'b' :c 'c'} :z :f :h) 528 | 529 | ;; Returns false, as :a is in the collection 530 | (missing? {:a 'a' :b 'b' :c 'c'} :z :b)" 531 | [coll & keys] 532 | {:pre [(coll? coll)] 533 | :post [(boolean? %)]} 534 | (reduce (fn [_ key] 535 | (if (contains? coll key) 536 | (reduced false) 537 | true)) 538 | nil 539 | keys)) 540 | 541 | (defn ordered-comparator 542 | "Given a function and an order (:ascending or :descending), 543 | return a comparator function that takes two objects and compares them in 544 | ascending or descending order based on the value of applying the function 545 | to each." 546 | [f order] 547 | {:pre [(ifn? f) 548 | (contains? #{:ascending :descending} order)] 549 | :post [(fn? %)]} 550 | (fn [x y] 551 | (if (= order :ascending) 552 | (compare (f x) (f y)) 553 | (compare (f y) (f x))))) 554 | 555 | (defn compose-comparators 556 | "Composes two comparator functions into a single comparator function 557 | which will call the first comparator and return the result if it is 558 | non-zero; otherwise it will call the second comparator and return 559 | its result." 560 | [comp-fn1 comp-fn2] 561 | {:pre [(fn? comp-fn1) 562 | (fn? comp-fn2)] 563 | :post [(fn? %)]} 564 | (fn [x y] 565 | (let [val1 (comp-fn1 x y)] 566 | (if (= val1 0) 567 | (comp-fn2 x y) 568 | val1)))) 569 | 570 | (defn order-by-expr? 571 | "Predicate that returns true if the argument is a valid expression for use 572 | with the `order-by` function; in other words, returns true if the argument 573 | is a 2-item vector whose first element is an `ifn` and whose second element 574 | is either `:ascending` or `:descending`." 575 | [x] 576 | (and 577 | (vector? x) 578 | (ifn? (first x)) 579 | (contains? #{:ascending :descending} (second x)))) 580 | 581 | (defn order-by 582 | "Sorts a collection based on a sequence of 'order by' expressions. Each expression 583 | is a tuple containing a fn followed by either `:ascending` or `:descending`; 584 | returns a collection that is sorted based on the values of the 'order by' fns 585 | being applied to the elements in the original collection. If multiple 'order by' 586 | expressions are passed in, their precedence is determined by their order in 587 | the argument list." 588 | [order-bys coll] 589 | {:pre [(sequential? order-bys) 590 | (every? order-by-expr? order-bys) 591 | (coll? coll)]} 592 | (let [comp-fns (map (fn [[f order]] (ordered-comparator f order)) order-bys) 593 | final-comp (reduce compose-comparators comp-fns)] 594 | (sort final-comp coll))) 595 | 596 | (defn sort-nested-maps 597 | "For a data structure, recursively sort any nested maps and sets descending 598 | into map values, lists, vectors and set members as well. The result should be 599 | that all maps in the data structure become explicitly sorted with natural 600 | ordering. This can be used before serialization to ensure predictable 601 | serialization. 602 | 603 | The returned data structure is not a transient so it is still able to be 604 | modified, therefore caution should be taken to avoid modification else the 605 | data will lose its sorted status." 606 | [data] 607 | (cond 608 | (map? data) 609 | (into (sorted-map) (for [[k v] data] 610 | [k (sort-nested-maps v)])) 611 | (sequential? data) 612 | (map sort-nested-maps data) 613 | :else data)) 614 | 615 | ;; ## Date and Time 616 | 617 | (defn timestamp 618 | "Returns a timestamp string for the given `time`, or the current time if none 619 | is provided. The format of the timestamp is eg. 2012-02-23T22:01:39.539Z." 620 | ([] 621 | (timestamp (time/now))) 622 | ([time] 623 | (time-format/unparse (time-format/formatters :date-time) time))) 624 | 625 | ;; ## Exception handling 626 | 627 | (defn without-ns 628 | "Given a clojure keyword that is optionally namespaced, returns 629 | a keyword with the same name but with no namespace." 630 | [kw] 631 | {:pre [(keyword? kw)] 632 | :post [(keyword? %) 633 | (nil? (namespace %))]} 634 | (keyword (name kw))) 635 | 636 | (defn keep-going* 637 | "Executes the supplied fn repeatedly. Execution may be stopped with an 638 | InterruptedException." 639 | [f on-error] 640 | (when 641 | (try 642 | (f) 643 | true 644 | (catch InterruptedException _e 645 | false) 646 | (catch Throwable e 647 | (on-error e) 648 | true)) 649 | (recur f on-error))) 650 | 651 | (defmacro keep-going 652 | "Executes body, repeating the execution of body even if an exception 653 | is thrown" 654 | [on-error & body] 655 | `(keep-going* (fn [] ~@body) ~on-error)) 656 | 657 | (defmacro with-error-delivery 658 | "Executes body, and delivers an exception to the provided promise if one is 659 | thrown." 660 | [error & body] 661 | `(try 662 | ~@body 663 | (catch Throwable e# 664 | (deliver ~error e#)))) 665 | 666 | (defmacro with-timeout [timeout-s default & body] 667 | `(let [f# (future (do ~@body)) 668 | result# (deref f# (* 1000 ~timeout-s) ~default)] 669 | (future-cancel f#) 670 | result#)) 671 | 672 | ;; ## File paths 673 | (defn absolute-path 674 | "Replacement for raynes.fs/absolute-path, which was removed in raynes.fs 1.4.6. 675 | Returns string representation of absolute path, as opposed to fs/absolute, which 676 | returns a File object." 677 | [path] 678 | (.getPath ^File (fs/absolute path))) 679 | 680 | (defn normalized-path 681 | "Replacement for raynes.fs/normalized-path, which was removed in raynes.fs 1.4.6. 682 | Returns string representation of absolute path, as opposed to fs/normalized, which 683 | returns a File object." 684 | [path] 685 | (.getPath ^File (fs/normalized path))) 686 | 687 | ;; ## Temp files 688 | 689 | (defn temp-file-name 690 | "Returns a unique name to a temporary file, but does not actually create the file." 691 | [file-name-prefix] 692 | (io/file (fs/tmpdir) (fs/temp-name file-name-prefix))) 693 | 694 | (defn delete-on-exit 695 | "Will delete `f` on shutdown of the JVM" 696 | [f] 697 | (.deleteOnExit (fs/file f)) 698 | f) 699 | 700 | (defn temp-file 701 | "Creates a temporary file that will be deleted on JVM shutdown. 702 | 703 | Supported arguments are the same as for me.raynes.fs/temp-file: 704 | [prefix] 705 | [prefix suffix] 706 | [prefix suffix tries] 707 | 708 | You may also call with no arguments, in which case the prefix string will be 709 | empty." 710 | [& args] 711 | (if (empty? args) 712 | (delete-on-exit (fs/temp-file nil)) 713 | (delete-on-exit (apply fs/temp-file args)))) 714 | 715 | (defn temp-dir 716 | "Creates a temporary directory that will be deleted on JVM shutdown. 717 | 718 | Supported arguments are the same as for me.raynes.fs/temp-dir: 719 | [prefix] 720 | [prefix suffix] 721 | [prefix suffix tries] 722 | 723 | You may also call with no arguments, in which case the prefix string will be 724 | empty." 725 | [& args] 726 | (if (empty? args) 727 | (delete-on-exit (fs/temp-dir nil)) 728 | (delete-on-exit (apply fs/temp-dir args)))) 729 | 730 | ;; ## Configuration files 731 | 732 | (def keywordize 733 | "Normalize INI keys by ensuring they're lower-case and keywords" 734 | (comp keyword string/lower-case)) 735 | 736 | (defn fetch-int 737 | "Fetch a key from the INI section and convert it 738 | to an integer if it parses, otherwise return the string" 739 | [^BasicProfileSection section key] 740 | (let [val (.fetch section key)] 741 | (or (parse-int val) 742 | val))) 743 | 744 | (defn create-section-map 745 | "Given an INI section, create a clojure map of it's key/values" 746 | [^BasicProfileSection section] 747 | (reduce (fn [acc [key _]] 748 | (if (> (.length section key) 1) 749 | (throw (IllegalArgumentException. 750 | (str "Duplicate configuration entry: " 751 | (mapv keyword [(.getName section) key])))) 752 | (assoc acc 753 | (keywordize key) 754 | (fetch-int section key)))) 755 | {} section)) 756 | 757 | (defn parse-ini 758 | "Takes a io/reader that contains an ini file, and returns an Ini object 759 | containing the parsed results" 760 | [^Reader ini-reader] 761 | {:pre [(instance? Reader ini-reader)] 762 | :post [(instance? Ini %)]} 763 | (let [config (Config.) 764 | ini (Ini.)] 765 | (.setMultiOption config true) 766 | (.setConfig ini config) 767 | (.load ini ini-reader) 768 | ini)) 769 | 770 | (defn ini-to-map 771 | "Takes a .ini filename and returns a nested map of 772 | fully-interpolated values. Strings that look like integers are 773 | returned as integers, and all section names and keys are returned as 774 | symbols." 775 | [filename] 776 | {:pre [(or (string? filename) 777 | (instance? java.io.File filename))] 778 | :post [(map? %) 779 | (every? keyword? (keys %)) 780 | (every? map? (vals %))]} 781 | 782 | (with-open [ini (io/reader filename)] 783 | (reduce (fn [acc [name section]] 784 | (assoc acc 785 | (keywordize name) 786 | (create-section-map section))) 787 | {} 788 | (parse-ini ini)))) 789 | 790 | (defn inis-to-map 791 | "Takes a path and converts the pointed-at .ini files into a nested 792 | map (see `ini-to-map` for details). If `path` is a file, the 793 | behavior is exactly the same as `ini-to-map`. If `path` is a 794 | directory, we return a merged version of parsing all the .ini files 795 | in the directory (we do not do a recursive find of .ini files)." 796 | ([path] 797 | (inis-to-map path "*.ini")) 798 | ([path glob-pattern] 799 | {:pre [(or (string? path) 800 | (instance? java.io.File path))] 801 | :post [(map? %)]} 802 | (let [files (if-not (fs/directory? path) 803 | [path] 804 | (fs/glob (fs/file path glob-pattern)))] 805 | (->> files 806 | (map fs/absolute) 807 | (map ini-to-map) 808 | (apply deep-merge-with-keys 809 | (fn [ks & _] 810 | (throw (IllegalArgumentException. 811 | (str "Duplicate configuration entry: " ks))))) 812 | (merge {}))))) 813 | 814 | (defn spit-ini 815 | "Writes the `ini-map` to the Ini file at `file`. `ini-map` should 816 | a map similar to the ones created by ini-to-map. The keys are keywords 817 | for the sections and their values are maps of config keypairs." 818 | [^File file ini-map] 819 | (let [ini (org.ini4j.Ini. file)] 820 | (doseq [[section-key section] ini-map 821 | [k v] section] 822 | (.put ini (name section-key) (name k) v)) 823 | (.store ini))) 824 | 825 | (defn add-shutdown-hook! 826 | "Adds a shutdown hook to the JVM runtime. 827 | 828 | `f` is a function that takes 0 arguments; the return value is ignored. This 829 | function will be called if the JVM receiveds an interrupt signal (e.g. from 830 | `kill` or CTRL-C); you can use it to log shutdown messages, handle state 831 | cleanup, etc." 832 | [^Runnable f] 833 | {:pre [(fn? f)]} 834 | (.addShutdownHook (Runtime/getRuntime) (Thread. f))) 835 | 836 | (defmacro demarcate 837 | "Executes `body`, but logs `msg` to info before and after `body` is 838 | executed. `body` is executed in an implicit do, and the last 839 | expression's return value is returned by `demarcate`. 840 | 841 | user> (demarcate \"reticulating splines\" (+ 1 2 3)) 842 | \"Starting reticulating splines\" 843 | \"Finished reticulating splines\" 844 | 6 845 | " 846 | [msg & body] 847 | `(do (log/info (str "Starting " ~msg)) 848 | (let [result# (do ~@body)] 849 | (log/info (str "Finished " ~msg)) 850 | result#))) 851 | 852 | ;; ## Command-line parsing 853 | 854 | (defn cli! 855 | "Validates that required command-line arguments are present. If they are not, 856 | throws a map** with an error message that is intended to be displayed to the user. 857 | Also checks to see whether the user has passed the `--help` flag. 858 | 859 | Input: 860 | 861 | - args : the command line arguments passed in by the user 862 | - specs : an array of supported argument specifications, as accepted by 863 | `clojure.tools.cli` 864 | - required : an array of keywords (using the long form of the argument spec) 865 | specifying which of the `specs` are required. If any of the 866 | `required` options are not present, the function will cause 867 | the program to exit and display the help message. 868 | 869 | ** The map is thrown using 'slingshot' (https://github.com/scgilardi/slingshot). 870 | It contains a `:kind` and `:msg`, where type is either `:error` or `:help`, 871 | and the message is either the error message or a help banner. 872 | 873 | Returns a three-item vector, containing: 874 | * a map of the parsed options 875 | * a vector containing the remaining cli arguments that were not parsed 876 | * a string containing a summary of all of the options that are available; for 877 | use in printing help messages if the user detects that the arguments are 878 | still invalid in some way." 879 | ([args specs] (cli! args specs nil)) 880 | ([args specs required-args] 881 | (let [specs (conj specs ["-h" "--help" "Show help" :default false]) 882 | {:keys [options arguments summary errors]} (cli/parse-opts args specs)] 883 | (when errors 884 | (let [msg (str 885 | "\n\n" 886 | "Error(s) occurred while parsing command-line arguments: " 887 | (apply str errors) 888 | "\n\n" 889 | summary)] 890 | (throw+ (error-map ::cli-error msg)))) 891 | (when (:help options) 892 | (throw+ (error-map ::cli-help summary))) 893 | (when-let [missing-field (some #(when-not (contains? options %) %) required-args)] 894 | (let [msg (str 895 | "\n\n" 896 | (format "Missing required argument '--%s'!" (name missing-field)) 897 | "\n\n" 898 | summary)] 899 | (throw+ (error-map ::cli-error msg)))) 900 | [options arguments summary]))) 901 | 902 | 903 | ;; ## SSL Certificate handling 904 | ;; 905 | ;; NOTE: Prefer functions provided by the jvm-certificate-authority library over these. 906 | ;; 907 | ;; These functions are only used by PuppetDB and they should likely move back into that 908 | ;; project until they can be refactored away over functions from the jvm-ca library. 909 | 910 | (defn- safe-value-request 911 | "Used to make eastwood happy about the type" 912 | [^Rdn v] 913 | (.getValue v)) 914 | 915 | (defn ^:deprecated cn-for-dn 916 | "Deprecated. Use functions from https://github.com/puppetlabs/jvm-ssl-utils instead. 917 | 918 | Extracts the CN (common name) from an LDAP DN (distinguished name). 919 | 920 | If more than one CN entry exists in the given DN, we return the most-specific 921 | one (the one that comes last, textually). If no CN is present in the DN, we 922 | return nil. 923 | 924 | Example: 925 | 926 | (cn-for-dn \"CN=foo.bar.com,OU=meh,C=us\") 927 | \"foo.bar.com\" 928 | 929 | (cn-for-dn \"CN=foo.bar.com,CN=baz.goo.com,OU=meh,C=us\") 930 | \"baz.goo.com\" 931 | 932 | (cn-for-dn \"OU=meh,C=us\") 933 | nil" 934 | [^String dn] 935 | {:pre [(string? dn)]} 936 | (some->> dn 937 | (LdapName.) 938 | (.getRdns) 939 | (filter #(= "CN" (.getType ^Rdn %))) 940 | (first) 941 | (safe-value-request) 942 | (str))) 943 | 944 | (defn ^:deprecated cn-for-cert 945 | "Deprecated. Use functions from https://github.com/puppetlabs/jvm-ssl-utils instead. 946 | 947 | Extract the CN from the DN of an x509 certificate. See `cn-for-dn` for details 948 | on how extraction is performed. 949 | 950 | If no CN exists in the certificate DN, nil is returned." 951 | [^java.security.cert.X509Certificate cert] 952 | (-> cert 953 | (.getSubjectX500Principal) 954 | (.getName) 955 | (cn-for-dn))) 956 | 957 | ;; ## Ring helpers 958 | 959 | (defn cn-whitelist->authorizer 960 | "Given a 'whitelist' file containing allowed CNs (one per line), 961 | build a function that takes a Ring request and returns true if the 962 | CN contained in the client certificate appears in the whitelist. 963 | 964 | `whitelist` can be either a local filename or a File object. 965 | 966 | This makes use of the `:ssl-client-cn` request parameter. See 967 | `com.puppetlabs.middleware/wrap-with-certificate-cn`." 968 | [whitelist] 969 | {:pre [(or (string? whitelist) 970 | (instance? java.io.File whitelist))] 971 | :post [(fn? %)]} 972 | (let [allowed? (set (lines whitelist))] 973 | (fn [{:keys [ssl-client-cn scheme]}] 974 | (or (= scheme :http) 975 | (allowed? ssl-client-cn))))) 976 | 977 | ;; ## Hashing 978 | 979 | (defn utf8-string->sha1 980 | "Compute a SHA-1 hash for the UTF-8 encoded version of the supplied 981 | string" 982 | [^String s] 983 | {:pre [(string? s)] 984 | :post [(string? %)]} 985 | (let [bytes (.getBytes s "UTF-8")] 986 | (digest/sha-1 [bytes]))) 987 | 988 | (defn utf8-string->sha256 989 | "Compute a SHA-256 hash for the UTF-8 encoded version of the supplied 990 | string" 991 | [^String s] 992 | {:pre [(string? s)] 993 | :post [(string? %)]} 994 | (let [bytes (.getBytes s "UTF-8")] 995 | (digest/sha-256 [bytes]))) 996 | 997 | (defn stream->sha256 998 | "Compute a SHA-256 hash for the given java.io.InputStream object." 999 | [^java.io.InputStream stream] 1000 | {:pre [(instance? java.io.InputStream stream)] 1001 | :post [(string? %)]} 1002 | (digest/sha-256 stream)) 1003 | 1004 | (defn file->sha256 1005 | "Compute a SHA-256 hash for the given java.io.File object. 1006 | Uses an InputStream to read the file, so it doesn't load all the contents into 1007 | memory at once." 1008 | [^java.io.File file] 1009 | {:pre [(instance? java.io.File file)] 1010 | :post [(string? %)]} 1011 | (digest/sha-256 file)) 1012 | 1013 | (defn bounded-memoize 1014 | "Similar to memoize, but the cache will be reset if the number of entries 1015 | exceeds the specified `bound`." 1016 | [f bound] 1017 | {:pre [(integer? bound) 1018 | (pos? bound)]} 1019 | (let [cache (atom {})] 1020 | (fn [& args] 1021 | (if-let [e (find @cache args)] 1022 | (val e) 1023 | (let [v (apply f args)] 1024 | (when (> (count @cache) bound) 1025 | (reset! cache {})) 1026 | (swap! cache assoc args v) 1027 | v))))) 1028 | 1029 | ;; ## UUID handling 1030 | 1031 | (defn uuid 1032 | "Generate a random UUID and return its string representation" 1033 | [] 1034 | (str (java.util.UUID/randomUUID))) 1035 | 1036 | (defn uuid? 1037 | "Verifies whether a string is a valid UUID" 1038 | [uuid] 1039 | (try 1040 | (java.util.UUID/fromString uuid) 1041 | true 1042 | (catch IllegalArgumentException _e 1043 | false))) 1044 | 1045 | ;; ## System interface 1046 | 1047 | (defn num-cpus 1048 | "Grabs the number of available CPUs for the local host" 1049 | [] 1050 | {:post [(pos? %)]} 1051 | (.availableProcessors (Runtime/getRuntime))) 1052 | 1053 | ;; Comparison of JVM versions 1054 | 1055 | (defn compare-jvm-versions 1056 | "Same behavior as `compare`, but specifically for JVM version 1057 | strings. Because Java versions don't follow semver or anything, we 1058 | need to do some massaging of the input first: 1059 | 1060 | http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html" 1061 | [a b] 1062 | {:pre [(string? a) 1063 | (string? b)] 1064 | :post [(number? %)]} 1065 | (let [parse #(mapv parse-int (-> % 1066 | (string/split #"-") 1067 | (first) 1068 | (string/split #"[\\._]")))] 1069 | (compare (parse a) (parse b)))) 1070 | 1071 | (def java-version 1072 | "Returns a string of the currently running java version" 1073 | (System/getProperty "java.version")) 1074 | 1075 | (defn get-lein-project-version 1076 | "Get the version of the specified leinigen project from the runtime." 1077 | [project] 1078 | (System/getProperty (format "%s.version" project))) 1079 | 1080 | ;; control flow 1081 | 1082 | (defmacro cond-let 1083 | "Takes a binding-form and a set of test/expr pairs. Evaluates each test 1084 | one at a time. If a test returns logical true, cond-let evaluates and 1085 | returns expr with binding-form bound to the value of test and doesn't 1086 | evaluate any of the other tests or exprs. To provide a default value 1087 | either provide a literal that evaluates to logical true and is 1088 | binding-compatible with binding-form, or use :else as the test and don't 1089 | refer to any parts of binding-form in the expr. (cond-let binding-form) 1090 | returns nil." 1091 | [bindings & clauses] 1092 | (let [binding (first bindings)] 1093 | (when-let [[test expr & more] clauses] 1094 | (if (= test :else) 1095 | expr 1096 | `(if-let [~binding ~test] 1097 | ~expr 1098 | (cond-let ~bindings ~@more)))))) 1099 | 1100 | (defmacro while-let 1101 | "Repeatedly executes body while test expression is true, evaluating the body 1102 | with binding-form bound to the value of test." 1103 | [bindings & body] 1104 | (let [form (first bindings) test (second bindings)] 1105 | `(loop [~form ~test] 1106 | (when ~form 1107 | ~@body 1108 | (recur ~test))))) 1109 | 1110 | (defmacro some-pred->> 1111 | "When expr does not satisfy pred, threads it into the first form (via ->>), 1112 | and when that result does not satisfy pred, through the next etc" 1113 | [pred expr & forms] 1114 | (let [g (gensym) 1115 | pstep (fn [step] `(if (~pred ~g) ~g (->> ~g ~step)))] 1116 | `(let [~g ~expr 1117 | ~@(interleave (repeat g) (map pstep forms))] 1118 | ~g))) 1119 | 1120 | (defn- port-open? 1121 | [port] 1122 | (try 1123 | (with-open [_ (java.net.ServerSocket. port)] 1124 | port) 1125 | (catch java.net.BindException e 1126 | (when-not (re-find #"already in use" (.getMessage e)) 1127 | (throw e))))) 1128 | 1129 | (defn open-port-num 1130 | "Returns a currently open port number in the port range 16384 1131 | through 32767. Note that on Linux, anything above 32767 lies in 1132 | the ephemeral port range, which is the range that the system uses 1133 | to allocate ports upon user request. Thus, we choose from a 1134 | different range to prevent possible port conflicts caused by 1135 | various services (e.g. database connection pools)." 1136 | [] 1137 | (let [lo 16384 1138 | hi 32768] ; one higher because the upper limit is exclusive 1139 | (if-let [open-port (some port-open? (shuffle (range lo hi)))] 1140 | open-port 1141 | (throw (java.net.BindException. 1142 | (format "All ports in the range %d through %d are already in use (Bind failed)" lo (dec hi))))))) 1143 | 1144 | (defmacro assoc-if-new 1145 | "Assocs the provided values with the corresponding keys if and only 1146 | if the key is not already present in map." 1147 | [map key val & kvs] 1148 | {:pre [(even? (count kvs))]} 1149 | (let [deferred-kvs (vec (for [[k v] (cons [key val] (partition 2 kvs))] 1150 | [k `(fn [] ~v)]))] 1151 | `(let [updates# (for [[k# v#] ~deferred-kvs 1152 | :when (= ::not-found (get ~map k# ::not-found))] 1153 | [k# (v#)])] 1154 | (merge ~map (into {} updates#))))) 1155 | 1156 | (defn deref-swap! 1157 | "Like swap! but returns the old value. 1158 | Adapted from http://stackoverflow.com/a/15442107." 1159 | [atom f & args] 1160 | (loop [] 1161 | (let [old @atom 1162 | new (apply f old args)] 1163 | (if (compare-and-set! atom old new) 1164 | old 1165 | (recur))))) 1166 | 1167 | (defn parse-interval 1168 | "Given a time string of the form \"\", or \"\", this 1169 | function parses this time amount and returns a joda time Period instance. 1170 | If the unit is left off, the units are assumed to be time/seconds 1171 | 1172 | Example: \"12h\" -> (clj-time.core/hours 12) 1173 | Example: \"12\" -> (clj-time.core/seconds 12) 1174 | 1175 | Possible units: s(econds), m(inutes), h(ours), d(ays), y(ears) 1176 | 1177 | Returns nil if the time string cannot be parsed." 1178 | [time-str] 1179 | (when-not (nil? time-str) 1180 | (when-let [[_ num unit] (re-matches #"^(\d+)([smhdy]?)$" time-str)] 1181 | (let [num (parse-int num) 1182 | time-fn (case unit 1183 | "s" time/seconds 1184 | "m" time/minutes 1185 | "h" time/hours 1186 | "d" time/days 1187 | "y" time/years 1188 | "" time/seconds)] 1189 | (time-fn num))))) 1190 | 1191 | ;; ## HTTP Headers 1192 | 1193 | (defn base-type 1194 | "Parse a base type out of a Content-Type, returns nil if there's no match. 1195 | Does not validate any parameters." 1196 | ;; Content-Type header grammar https://tools.ietf.org/html/rfc7231#section-3.1.1.1 1197 | ;; token definition https://tools.ietf.org/html/rfc7230#section-3.2.6 1198 | ;; white space definition https://tools.ietf.org/html/rfc7230#section-3.2.3 1199 | [content-type] 1200 | (let [token #"[a-zA-Z0-9!#$%&'*+.^_`|~-]+" 1201 | matcher (format "^(%s/%s)(?:[ \t;]|$)" token token)] 1202 | (second (re-find (re-pattern matcher) content-type)))) 1203 | 1204 | (defn ZonedDateTime->timestamp-string 1205 | ^String [^ZonedDateTime zdt] 1206 | (.format DateTimeFormatter/ISO_OFFSET_DATE_TIME zdt)) 1207 | 1208 | (defn now->timestamp-string 1209 | "Using the java native libraries, using the system clock, create a UTC based iso8601 timestamp" 1210 | ^String [] 1211 | (ZonedDateTime->timestamp-string (ZonedDateTime/now ZoneOffset/UTC))) 1212 | 1213 | (defn timestamp-string->ZonedDateTime 1214 | "Given an iso8601 timestamp with offset, convert it into a java.time.ZonedDateTime. 1215 | Throws DateTimeParseException if string can't be parsed" 1216 | ^ZonedDateTime [^String timestamp] 1217 | (ZonedDateTime/from (.parse DateTimeFormatter/ISO_OFFSET_DATE_TIME timestamp))) 1218 | 1219 | (defn ZonedDateTime->utc-ZonedDateTime 1220 | "given a zoned date time, convert it to the utc timezone" 1221 | ^ZonedDateTime [^ZonedDateTime zdt] 1222 | (.withZoneSameInstant zdt (ZoneId/of "UTC"))) 1223 | 1224 | (defn is-only-number? 1225 | "Given a string, does the string only consist of numeric characters (0 through 9)" 1226 | [a] 1227 | (some? (re-find #"^\d+$" a))) 1228 | 1229 | (defn starts-with-zero? 1230 | "Given a string, does it start with zero?" 1231 | [a] 1232 | (some? (re-find #"^0" a))) 1233 | 1234 | (defn compare-versions 1235 | "Similar in concept to clojure's `compare` but for version strings. Based on puppet's versioncmp function 1236 | https://github.com/puppetlabs/puppet/blob/9ab8526d0bc5f0fe4e29e7dac9832307d8af2e48/lib/puppet/util/package.rb#L3 but does 1237 | not ignore trailing zeros. 1238 | 1239 | Given two version strings, do a logical comparison of the two. If the first is an earlier version that the second, 1240 | return a positive number. If they are equivalent versions, return 0, if the first is newer than the second, return a 1241 | negative number." 1242 | [a b] 1243 | (let [version-regular-expression #"[\-.]|\d+|[^\-.\d]+" 1244 | a-matches (re-seq version-regular-expression a) 1245 | b-matches (re-seq version-regular-expression b)] 1246 | (if (and (pos? (count a-matches)) 1247 | (pos? (count b-matches))) 1248 | (loop [current-a-matches a-matches 1249 | current-b-matches b-matches] 1250 | (let [current-a (first current-a-matches) 1251 | current-b (first current-b-matches)] 1252 | (cond 1253 | (and (nil? current-a) (nil? current-b)) 1254 | 0 1255 | 1256 | (nil? current-a) 1257 | (compare a b) 1258 | 1259 | (nil? current-b) 1260 | (compare a b) 1261 | 1262 | (= current-a current-b) 1263 | (recur (rest current-a-matches) (rest current-b-matches)) 1264 | 1265 | (= "-" current-a) 1266 | -1 1267 | 1268 | (= "-" current-b) 1269 | 1 1270 | 1271 | (= "." current-a) 1272 | -1 1273 | 1274 | (= "." current-b) 1275 | 1 1276 | 1277 | (and (is-only-number? current-a) (is-only-number? current-b)) 1278 | (if (or (starts-with-zero? current-a) 1279 | (starts-with-zero? current-b)) 1280 | (compare (.toUpperCase ^String current-a) (.toUpperCase ^String current-b)) 1281 | (compare (parse-int current-a) (parse-int current-b))) 1282 | 1283 | :else 1284 | (compare (.toUpperCase ^String current-a) (.toUpperCase ^String current-b))))) 1285 | (compare a b)))) -------------------------------------------------------------------------------- /src/puppetlabs/kitchensink/file.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.file 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as str] 4 | [clojure.tools.logging :as log]) 5 | (:import (java.io BufferedWriter File FileOutputStream OutputStreamWriter) 6 | (java.nio ByteBuffer) 7 | (java.nio.channels Channels) 8 | (java.nio.file CopyOption Files LinkOption Path Paths StandardCopyOption) 9 | (java.nio.file.attribute FileAttribute PosixFilePermissions) 10 | (java.util.zip GZIPInputStream) 11 | (org.apache.commons.compress.archivers.tar TarArchiveInputStream))) 12 | 13 | (defn str->path 14 | ^Path [path] 15 | (Paths/get path (into-array String []))) 16 | 17 | (defn dir+file->path 18 | ^Path [directory file] 19 | (Paths/get directory (into-array String [file]))) 20 | 21 | (defn get-perms 22 | "Returns the currently set permissions of the given file path." 23 | [path] 24 | (-> (str->path path) 25 | (Files/getPosixFilePermissions (into-array LinkOption [])) 26 | PosixFilePermissions/toString)) 27 | 28 | (defn set-perms 29 | "Set the provided permissions on the given path. The permissions string is in 30 | the form of the standard 9 character posix format, e.g. \"rwxr-xr-x\"." 31 | [path permissions] 32 | (-> (str->path path) 33 | (Files/setPosixFilePermissions 34 | (PosixFilePermissions/fromString permissions)) 35 | (.toFile))) 36 | 37 | (defn perms->attribute 38 | [permissions] 39 | (PosixFilePermissions/asFileAttribute (PosixFilePermissions/fromString permissions))) 40 | 41 | (def nofollow-links 42 | (into-array LinkOption [LinkOption/NOFOLLOW_LINKS])) 43 | 44 | (def ^:private default-permissions 45 | (PosixFilePermissions/fromString "rw-r-----")) 46 | 47 | (defn atomic-write 48 | "Write to a file atomically. This takes a function that should operate on a 49 | Writer as its first argument. If permissions are specified (as a string of the 50 | form \"rwxrwxrwx\") then the file will be created with those permissions. If 51 | not specified and the file already exists then existing permissions will be 52 | preserved. If no file exist a default is set." 53 | ([path write-function] 54 | (atomic-write path write-function nil)) 55 | ([path write-function permissions] 56 | (let [target (str->path path) 57 | dir (.getParent target) 58 | file-exists? (Files/exists target nofollow-links) 59 | owner (when file-exists? 60 | (Files/getOwner target nofollow-links)) 61 | group (when file-exists? 62 | (Files/getAttribute target "posix:group" nofollow-links)) 63 | permissions (if permissions 64 | (PosixFilePermissions/fromString permissions) 65 | (if file-exists? 66 | (Files/getPosixFilePermissions target nofollow-links) 67 | default-permissions)) 68 | temp-attributes (into-array FileAttribute [(perms->attribute "rw-------")]) 69 | temp-file (Files/createTempFile dir (.toString (.getFileName target)) "tmp" temp-attributes) 70 | stream (proxy [FileOutputStream] [(.toString temp-file)] 71 | (close [] 72 | (.sync (.getFD ^FileOutputStream this)) 73 | ;; this looks weird, but makes the proxy-super avoid reflection by masking `this` with a version that has the meta tag 74 | (let [^FileOutputStream this this] 75 | (proxy-super close))))] 76 | 77 | (with-open [writer (BufferedWriter. (OutputStreamWriter. stream))] 78 | (write-function writer)) 79 | 80 | (when owner 81 | (Files/setOwner temp-file owner)) 82 | (when group 83 | (Files/setAttribute temp-file "posix:group" group nofollow-links)) 84 | 85 | ;; We set these here instead of at file creation to avoid problems with 86 | ;; read-only files and umask 87 | (Files/setPosixFilePermissions temp-file permissions) 88 | (Files/move temp-file target (into-array CopyOption [StandardCopyOption/ATOMIC_MOVE]))))) 89 | 90 | (defn atomic-write-string 91 | "Write a string to a file atomically. See atomic-write for more details." 92 | ([path string] 93 | (atomic-write-string path string nil)) 94 | ([path string permissions] 95 | (atomic-write path #(.write ^BufferedWriter % ^String string) permissions))) 96 | 97 | (defn unzip-file 98 | "Given a path to an input file, and a path to an output location, attempt to unzip the file. 99 | Will not overwrite the same path with the output" 100 | [input-file output-file] 101 | (when (not= input-file output-file) 102 | (io/make-parents output-file) 103 | (with-open [file-input-stream (io/input-stream input-file) 104 | gzip-input-stream (GZIPInputStream. file-input-stream) 105 | file-output-stream (io/output-stream output-file)] 106 | (io/copy gzip-input-stream file-output-stream)))) 107 | 108 | (def empty-file-attributes 109 | (into-array FileAttribute [])) 110 | 111 | (defn write-tar-stream-to-file 112 | [^TarArchiveInputStream input-stream ^Path output-path ^ByteBuffer buffer] 113 | (with-open [out-channel (.getChannel (FileOutputStream. (.toFile output-path)))] 114 | ;; move the byte buffer out for efficiency 115 | (let [;; specifically don't close this as it will close the whole tarfile stream 116 | in-channel (Channels/newChannel input-stream)] 117 | (loop [in-buffer-size (.read in-channel buffer)] 118 | (when (or (pos? in-buffer-size) (pos? (.position buffer))) 119 | (.flip buffer) 120 | (.write out-channel buffer) 121 | (.compact buffer) 122 | (recur (.read in-channel buffer))))))) 123 | 124 | (defn untar-file 125 | "Given a path to a tar file, and a path to the parent directory to untar the repo in, recreate the contents of the tar file 126 | Note: does not recreate original permissions, or support symlinks." 127 | [path-to-tar-file output-directory] 128 | (let [trimmed-output (if (str/ends-with? output-directory "/") (subs output-directory 0 (dec (count output-directory))) output-directory) 129 | ;; allocate the copy buffer once and reuse for efficiency. 130 | buffer (ByteBuffer/allocateDirect (* 64 1024))] 131 | (with-open [tar-input-stream (TarArchiveInputStream. (io/input-stream path-to-tar-file))] 132 | (loop [entry (.getNextEntry tar-input-stream)] 133 | (when (some? entry) 134 | (if (.isDirectory entry) 135 | (Files/createDirectories (dir+file->path trimmed-output (.getName entry)) empty-file-attributes) 136 | (let [output-file (dir+file->path trimmed-output (.getName entry))] 137 | (io/make-parents output-file) 138 | (write-tar-stream-to-file tar-input-stream output-file buffer))) 139 | (recur (.getNextEntry tar-input-stream))))))) 140 | 141 | (defn delete-recursively 142 | "Given a path to a directory, delete everything in the directory recursively. 143 | Given a path to a file, it will delete that file. 144 | Returns true if successful, false if not. Failures are logged at the info level. 145 | Uses a `stack` based tail recursion method to avoid using a recursive stack" 146 | [^String path-to-delete] 147 | (let [path (str->path path-to-delete) 148 | first (.toFile path)] 149 | (loop [stack [first]] 150 | (if (seq stack) 151 | (let [^File last-element (last stack)] 152 | (if (.isDirectory last-element) 153 | (let [files (.listFiles ^File last-element)] 154 | ;; if it is empty, we can delete it 155 | (if (empty? files) 156 | (if (.delete last-element) 157 | (recur (butlast stack)) 158 | (do (log/infof "Failed to delete (%s), aborting delete of (%s)" (.getCanonicalPath last-element) (.getCanonicalPath first)) 159 | false)) 160 | ;; it isn't empty, so push all the contents to the stack and recurse 161 | (recur (concat stack files)))) 162 | (if (.delete last-element) 163 | (recur (butlast stack)) 164 | (do (log/infof "Failed to delete (%s), aborting delete of (%s)" (.getCanonicalPath last-element) (.getCanonicalPath first)) 165 | false)))) 166 | true)))) -------------------------------------------------------------------------------- /src/puppetlabs/kitchensink/json.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.json 2 | "Cheshire related functions 3 | 4 | This front-ends the common set of core cheshire functions: 5 | 6 | * generate-string 7 | * generate-stream 8 | * parse-string 9 | * parse-stream 10 | 11 | This namespace when 'required' will also setup some common JSON encoders 12 | globally, so you can avoid doing this for each call." 13 | (:require 14 | [cheshire.core :as core] 15 | [cheshire.generate :as generate] 16 | [clj-time.coerce :as coerce] 17 | [clj-time.core :as clj-time] 18 | [clojure.java.io :as io] 19 | [clojure.tools.logging :as log]) 20 | (:import 21 | com.fasterxml.jackson.core.JsonGenerator 22 | (java.time Instant LocalDate LocalDateTime))) 23 | 24 | (defn- clj-time-encoder 25 | [data jsonGenerator] 26 | (.writeString ^JsonGenerator jsonGenerator ^String (coerce/to-string data))) 27 | 28 | (def ^:dynamic *datetime-encoder* clj-time-encoder) 29 | 30 | (defn- java-instant-encoder 31 | [^Instant data jsonGenerator] 32 | (.writeString ^JsonGenerator jsonGenerator ^String (.toString data))) 33 | 34 | (def ^:dynamic *instant-encoder* java-instant-encoder) 35 | 36 | (defn- java-localdate-encoder 37 | [^LocalDate data jsonGenerator] 38 | (.writeString ^JsonGenerator jsonGenerator ^String (.toString data))) 39 | 40 | (def ^:dynamic *localdate-encoder* java-localdate-encoder) 41 | 42 | (defn- java-localdatetime-encoder 43 | [^LocalDateTime data jsonGenerator] 44 | (.writeString ^JsonGenerator jsonGenerator ^String (.toString data))) 45 | 46 | (def ^:dynamic *localdatetime-encoder* java-localdatetime-encoder) 47 | 48 | (defn add-common-json-encoders!* 49 | "Non-memoize version of add-common-json-encoders!" 50 | [] 51 | (when (satisfies? generate/JSONable (clj-time/date-time 1999)) 52 | (log/warn "Overriding existing JSONable protocol implementation for org.joda.time.DateTime")) 53 | (when (satisfies? generate/JSONable (Instant/now)) 54 | (log/warn "Overriding existing JSONable protocol implementation for java.time.Instant")) 55 | (when (satisfies? generate/JSONable (LocalDateTime/now)) 56 | (log/warn "Overriding existing JSONable protocol implementation for java.time.LocalDateTime")) 57 | (when (satisfies? generate/JSONable (LocalDate/now)) 58 | (log/warn "Overriding existing JSONable protocol implementation for java.time.LocalDate")) 59 | (generate/add-encoder 60 | org.joda.time.DateTime 61 | (fn [data jsonGenerator] 62 | (*datetime-encoder* data jsonGenerator))) 63 | (generate/add-encoder 64 | java.time.Instant 65 | (fn [data jsonGenerator] 66 | (*instant-encoder* data jsonGenerator))) 67 | (generate/add-encoder 68 | java.time.LocalDate 69 | (fn [data jsonGenerator] 70 | (*localdate-encoder* data jsonGenerator))) 71 | (generate/add-encoder 72 | java.time.LocalDateTime 73 | (fn [data jsonGenerator] 74 | (*localdatetime-encoder* data jsonGenerator)))) 75 | 76 | (def 77 | ^{:doc "Registers some common encoders for cheshire JSON encoding. 78 | 79 | This is a memoize function, to avoid unnecessary calls to add-encoder. 80 | 81 | Ideally this function should be called once in your apply, for example your 82 | main class. 83 | 84 | Encoders currently include: 85 | 86 | * org.joda.time.DateTime - handled with to-string"} 87 | add-common-json-encoders! (memoize add-common-json-encoders!*)) 88 | 89 | (defmacro with-datetime-encoder 90 | "Evaluates the body using the given encoder to serialize DateTime objects to 91 | JSON. Requires that `add-common-json-encoders!` from this namespace has 92 | already been called, and that nobody else has re-extended 93 | org.joda.date.DateTime to cheshire's JSONable protocol in the meantime." 94 | [encoder & body] 95 | `(binding [*datetime-encoder* ~encoder] 96 | ~@body)) 97 | 98 | (def default-pretty-opts {:date-format "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" :pretty true}) 99 | 100 | (def ^String generate-string core/generate-string) 101 | 102 | (def ^String generate-stream core/generate-stream) 103 | 104 | (defn generate-pretty-string 105 | "Thinly wraps cheshire.core/generate-string, adding the clj-time default date 106 | format and pretty printing from `default-pretty-opts`" 107 | ([obj] 108 | (generate-pretty-string obj default-pretty-opts)) 109 | ([obj opts] 110 | (generate-string obj (merge default-pretty-opts opts)))) 111 | 112 | (defn generate-pretty-stream 113 | "Thinly wraps cheshire.core/generate-stream, adding the clj-time default date 114 | format and pretty printing from `default-pretty-opts`" 115 | ([obj writer] 116 | (generate-pretty-stream obj writer default-pretty-opts)) 117 | ([obj writer opts] 118 | (generate-stream obj writer (merge default-pretty-opts opts)))) 119 | 120 | (def parse-string core/parse-string) 121 | 122 | (def parse-stream core/parse-stream) 123 | 124 | (defn spit-json 125 | "Similar to clojure.core/spit, but writes the Clojure 126 | datastructure as JSON to `f`" 127 | [f obj & options] 128 | (with-open [writer ^java.io.BufferedWriter (apply io/writer f options)] 129 | (generate-pretty-stream obj writer)) 130 | nil) 131 | -------------------------------------------------------------------------------- /src/puppetlabs/kitchensink/time.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.time 2 | (:require [puppetlabs.kitchensink.core :as core]) 3 | (:import (java.time Duration))) 4 | 5 | (def matching-pattern #"^(\d+)(y|d|h|m|s)?$") 6 | (def unit-map {"y" (* 365 24 60 60), ; 365 days isn't technically a year, but is sufficient for most purposes 7 | "d" (* 24 60 60), 8 | "h" (* 60 60), 9 | "m" 60, 10 | "s" 1}) 11 | 12 | (defn duration-str->seconds 13 | "Given a puppet duration string, see https://github.com/puppetlabs/puppet/blob/fb44fd90c64ee11f0a29fb8924adbab5b0695555/lib/puppet/settings/duration_setting.rb 14 | for a reference implementation, return the duration in seconds" 15 | [duration] 16 | ;; handle both integer input and strings that parse as integers 17 | (if-let [parsed-seconds (core/safe-parse-int duration)] 18 | parsed-seconds 19 | ;; must be a format string 20 | (let [matches (re-seq matching-pattern duration)] 21 | (if-let [last-matches (last matches)] ;; in the form [ ] 22 | (* (core/parse-int (second last-matches)) (get unit-map (last last-matches))) 23 | (throw (IllegalArgumentException. (format "Invalid duration format %s" duration))))))) 24 | 25 | (defn duration-str->Duration 26 | "Given a puppet duration string, return a java.time.Duration equivalent" 27 | ^Duration [duration] 28 | (Duration/ofSeconds (duration-str->seconds duration))) 29 | 30 | (defn days->hours 31 | "Convert a number of days into equivalent hours" 32 | [days] 33 | (* 24 days)) 34 | 35 | (defn hours->days 36 | "Convert a number of hours into days and fractional days" 37 | [hours] 38 | (/ hours 24)) 39 | 40 | (defn hours->min 41 | "Convert a number of hours into minutes" 42 | [hours] 43 | (* 60 hours)) 44 | 45 | (defn min->hours 46 | "Convert a number of minutes into hours and fractional hours" 47 | [min] 48 | (/ min 60)) 49 | 50 | (defn min->sec 51 | "Convert a number of minutes into seconds" 52 | [min] 53 | (* 60 min)) 54 | 55 | (defn sec->min 56 | "Convert a number of seconds into minutes and fractional minutes" 57 | [sec] 58 | (/ sec 60)) 59 | 60 | (defn sec->ms 61 | "Convert a number of seconds into milliseconds" 62 | [sec] 63 | (* 1000 sec)) 64 | 65 | (defn ms->sec 66 | "Convert a number of milliseconds into seconds and fractional seconds" 67 | [ms] 68 | (/ ms 1000)) 69 | 70 | (defn min->ms 71 | "Convert a number of minutes into milliseconds" 72 | [min] 73 | (-> min 74 | min->sec 75 | sec->ms)) 76 | 77 | (defn ms->min 78 | "Convert a number of milliseconds into minutes and fractional minutes" 79 | [ms] 80 | (-> ms 81 | ms->sec 82 | sec->min)) 83 | 84 | (defn hours->ms 85 | "Convert a number of hours into milliseconds" 86 | [hours] 87 | (-> hours 88 | hours->min 89 | min->ms)) 90 | 91 | (defn ms->hours 92 | "Convert a number of milliseconds into hours and fractional hours" 93 | [ms] 94 | (-> ms 95 | ms->min 96 | min->hours)) 97 | 98 | (defn days->ms 99 | "Convert a number of days into milliseconds" 100 | [days] 101 | (-> days 102 | days->hours 103 | hours->ms)) 104 | 105 | (defn ms->days 106 | "Convert a number of milliseconds into days and fractional days" 107 | [ms] 108 | (-> ms 109 | ms->hours 110 | hours->days)) -------------------------------------------------------------------------------- /test/puppetlabs/kitchensink/classpath_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.classpath-test 2 | (:require [clojure.test :refer :all] 3 | [puppetlabs.kitchensink.classpath :refer [with-additional-classpath-entries]])) 4 | 5 | (deftest with-additional-classpath-entries-test 6 | (let [paths ["classpath-test"] 7 | get-resource #(-> (Thread/currentThread) 8 | .getContextClassLoader 9 | (.getResource "does-not-exist-anywhere-else"))] 10 | (with-additional-classpath-entries 11 | paths 12 | (testing "classloader now includes the new path" 13 | (is (get-resource)))) 14 | 15 | (testing "classloader no longer includes the new path" 16 | (is (not (get-resource)))))) 17 | -------------------------------------------------------------------------------- /test/puppetlabs/kitchensink/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.core-test 2 | (:require [clj-time.core :as t] 3 | [clojure.string :as string] 4 | [clojure.test :refer :all] 5 | [clojure.zip :as zip] 6 | [me.raynes.fs :as fs] 7 | [puppetlabs.kitchensink.core :as core] 8 | [puppetlabs.kitchensink.testutils :as testutils] 9 | [slingshot.slingshot :refer [try+]]) 10 | (:import (java.io ByteArrayInputStream File) 11 | (java.time Month ZonedDateTime) 12 | (java.time.format DateTimeParseException) 13 | (java.util ArrayList))) 14 | 15 | (deftest array?-test 16 | (testing "array?" 17 | 18 | (testing "should work for nil input" 19 | (is (nil? (core/array? nil)))) 20 | 21 | (testing "should detect primitive arrays" 22 | (doseq [f #{object-array boolean-array byte-array short-array char-array int-array long-array float-array double-array}] 23 | (is (true? (core/array? (f 1)))))) 24 | 25 | (testing "should return nil for non-array objects" 26 | (doseq [x ['() [] {} "foo" 123 456.789 1/3]] 27 | (is (false? (core/array? x))))))) 28 | 29 | (deftest boolean?-test 30 | (testing "should return true if true" 31 | (is (boolean? true))) 32 | (testing "should return true if false" 33 | (is (boolean? false))) 34 | (testing "should return false if string" 35 | (is (not (boolean? "test")))) 36 | (testing "should return false if nil" 37 | (is (not (boolean? nil))))) 38 | 39 | (deftest regexp?-test 40 | (testing "should return true if pattern" 41 | (is (core/regexp? (re-pattern "test")))) 42 | (is (core/regexp? #"test")) 43 | (testing "should return false if string" 44 | (is (not (core/regexp? "test"))))) 45 | 46 | (deftest datetime?-test 47 | (testing "should return false for non-coercible types" 48 | (is (not (core/datetime? 2.0)))) 49 | (testing "should return false for nil" 50 | (is (not (core/datetime? nil)))) 51 | (testing "should return true for a valid string" 52 | (is (core/datetime? "2011-01-01T12:00:00-03:00"))) 53 | (testing "should return false for an invalid string" 54 | (is (not (core/datetime? "foobar")))) 55 | (testing "should return true for a valid integer" 56 | (is (core/datetime? 20))) 57 | (testing "should return false for an invalid integer") 58 | (is (not (core/datetime? -9999999999999999999999999999999)))) 59 | 60 | (deftest zipper?-test 61 | (testing "should return true for zippers" 62 | (is (true? (core/zipper? (zip/vector-zip [:foo :bar]))))) 63 | (testing "should return false for non-zippers" 64 | (is (false? (core/zipper? "hi"))) 65 | (is (false? (core/zipper? 42))) 66 | (is (false? (core/zipper? :foo))) 67 | (is (false? (core/zipper? [:foo :bar]))) 68 | (is (false? (core/zipper? {:foo :bar}))))) 69 | 70 | (deftest to-bool-test 71 | (testing "should return the same value when passed a Boolean" 72 | (is (true? (core/to-bool true))) 73 | (is (false? (core/to-bool false)))) 74 | (testing "should return true or false when passed a string representation of same" 75 | (is (true? (core/to-bool "true"))) 76 | (is (true? (core/to-bool "TRUE"))) 77 | (is (true? (core/to-bool "tRuE"))) 78 | (is (false? (core/to-bool "false"))) 79 | (is (false? (core/to-bool "FALSE"))) 80 | (is (false? (core/to-bool "fAlSe")))) 81 | (testing "should return false when passed nil" 82 | (is (false? (core/to-bool nil)))) 83 | (testing "should throw an exception when passed a string other than true or false" 84 | (try+ 85 | (core/to-bool "hi") 86 | (is (not true) "Expected exception to be thrown by core/to-bool when an invalid string is passed") 87 | #_{:clj-kondo/ignore [:unresolved-symbol]} 88 | (catch map? m 89 | (is (contains? m :kind)) 90 | (is (= :puppetlabs.kitchensink.core/parse-error (:kind m))) 91 | (is (= :parse-error (core/without-ns (:kind m)))) 92 | (is (contains? m :msg)) 93 | (is (re-find #"Unable to parse 'hi' to a boolean" (:msg m))))))) 94 | 95 | (deftest true-str?-test 96 | (are [t-or-f? str-val] (t-or-f? (core/true-str? str-val)) 97 | 98 | true? "true" 99 | true? "TRUE" 100 | true? "TrUe" 101 | 102 | false? "false" 103 | false? nil 104 | false? "FALSE")) 105 | 106 | (deftest to-sentence-test 107 | (are [coll string] (= string (core/to-sentence coll)) 108 | [] "" 109 | ["foo"] "foo" 110 | ["foo" "bar"] "foo and bar" 111 | ["foo" "bar" "baz"] "foo, bar, and baz" 112 | ["foo" "bar" "baz" "qux"] "foo, bar, baz, and qux")) 113 | 114 | (deftest key->str-test 115 | (testing "returns strings unmodified" 116 | (is (= "foo" (core/key->str "foo"))) 117 | (is (= ":foo" (core/key->str ":foo"))) 118 | (is (= "foo/bar" (core/key->str "foo/bar")))) 119 | 120 | (testing "returns the entire stringified keyword" 121 | (is (= "foo" (core/key->str :foo))) 122 | (is (= ":foo" (core/key->str (keyword ":foo")))) 123 | (is (= "foo/bar" (core/key->str :foo/bar))))) 124 | 125 | (deftest mkdirs-test 126 | (testing "creates all specified directories that don't exist for File arg" 127 | (let [tmpdir (core/temp-dir)] 128 | (fs/mkdirs (fs/file tmpdir "foo")) 129 | (core/mkdirs! (fs/file tmpdir "foo" "bar" "baz")) 130 | (is (fs/directory? (fs/file tmpdir "foo" "bar" "baz"))))) 131 | (testing "creates all specified directories that don't exist for String arg" 132 | (let [tmpdir (core/temp-dir)] 133 | (fs/mkdirs (fs/file tmpdir "foo")) 134 | (core/mkdirs! (.getPath (fs/file tmpdir "foo" "bar" "baz"))) 135 | (is (fs/directory? (fs/file tmpdir "foo" "bar" "baz"))))) 136 | (testing "throws exception if one of the elements of the path exists and is a file" 137 | (let [tmpdir (core/temp-dir)] 138 | (fs/mkdirs (fs/file tmpdir "foo")) 139 | (fs/touch (fs/file tmpdir "foo" "bar")) 140 | (try+ 141 | (core/mkdirs! (fs/file tmpdir "foo" "bar" "baz")) 142 | (is (not true) "Expected exception to be thrown by core/mkdirs! when one of the elements of the path already exists and is a file") 143 | (catch map? m 144 | (is (contains? m :kind)) 145 | (is (= :puppetlabs.kitchensink.core/io-error (:kind m))) 146 | (is (= :io-error (core/without-ns (:kind m)))) 147 | (is (contains? m :msg)) 148 | (is (re-find #"foo/bar' is a file" (:msg m))))))) 149 | (testing "throws exception if the path exists and is a file" 150 | (let [tmpdir (core/temp-dir)] 151 | (fs/mkdirs (fs/file tmpdir "foo")) 152 | (fs/touch (fs/file tmpdir "foo" "bar")) 153 | (try+ 154 | (core/mkdirs! (fs/file tmpdir "foo" "bar")) 155 | (is (not true) (str "Expected exception to be thrown by core/mkdirs! when " 156 | "the path already exists and is a file")) 157 | (catch map? m 158 | (is (contains? m :kind)) 159 | (is (= :puppetlabs.kitchensink.core/io-error (:kind m))) 160 | (is (= :io-error (core/without-ns (:kind m)))) 161 | (is (contains? m :msg)) 162 | (is (re-find #"foo/bar' is a file" (:msg m))))))) 163 | (testing "Permission denied on some directory in the hierarchy" 164 | (let [tmpdir (core/temp-dir)] 165 | (fs/mkdirs (fs/file tmpdir "foo")) 166 | (fs/chmod "-w" (fs/file tmpdir "foo")) 167 | (try+ 168 | (core/mkdirs! (fs/file tmpdir "foo" "bar" "baz")) 169 | (is (not true) "Expected exception to be thrown by core/mkdirs! when a permissions error occurs") 170 | (catch map? m 171 | (is (contains? m :kind)) 172 | (is (= :puppetlabs.kitchensink.core/io-error (:kind m))) 173 | (is (= :io-error (core/without-ns (:kind m)))) 174 | (is (contains? m :msg)) 175 | (is (re-find #"foo' is not writable" (:msg m)))))))) 176 | 177 | (deftest quotient-test 178 | (testing "core/quotient" 179 | 180 | (testing "should behave like '/' when divisor is non-zero" 181 | (is (= 22/7 (core/quotient 22 7)))) 182 | 183 | (testing "should return default when divisor is zero" 184 | (is (= 0 (core/quotient 1 0))) 185 | (is (= 10 (core/quotient 1 0 10)))))) 186 | 187 | (deftest rand-weighted-selection-test 188 | (testing "core/rand-weighted-selection" 189 | 190 | (testing "should make selections within 1% of expected values when n=100k" 191 | (let [make-selection #(core/rand-weighted-selection 192 | 0.1 :foo 193 | 0.3 :bar 194 | 0.4 :baz 195 | 0.19 :quux 196 | 0.01 :waffle) 197 | n (int 1e5) 198 | freqs (frequencies (repeatedly n make-selection)) 199 | expected-freqs {:foo 10000 200 | :bar 30000 201 | :baz 40000 202 | :quux 19000 203 | :waffle 1000}] 204 | (doseq [[value actual] freqs 205 | :let [expected (get expected-freqs value)]] 206 | (is (< (Math/abs ^int (- expected actual)) (/ n 100)))))) 207 | 208 | (testing "adjusts the weight of the last value when the weights sum to less than 1" 209 | (is (= :foo (core/rand-weighted-selection 0.0 :foo)))) 210 | 211 | (testing "doesn't select values whose weights come after prior weights have sum to 1" 212 | (dotimes [_ 1e3] 213 | (is (not= :bar (core/rand-weighted-selection 1.0 :foo 10.0 :bar))))) 214 | 215 | (testing "throws an error when" 216 | 217 | (testing "a weight does not have a value" 218 | (is (thrown? AssertionError (core/rand-weighted-selection 0.0)))) 219 | 220 | (testing "a weight is not numeric" 221 | (is (thrown? AssertionError (core/rand-weighted-selection :foo :bar))))))) 222 | 223 | (deftest rand-str-test 224 | (testing "core/rand-str" 225 | (testing "throws an IllegalArgumentException when given an unknown characters keyword" 226 | (is (thrown-with-msg? IllegalArgumentException #":CJK" (core/rand-str :CJK 42)))) 227 | 228 | (doseq [[kw cs] core/ascii-character-sets 229 | :let [cs (set cs)]] 230 | (testing (str "recognizes the " kw " character set keyword") 231 | (dotimes [_ 10] 232 | (is (every? cs (core/rand-str kw 1000)))))) 233 | 234 | (testing "uses collections of strings & characters as character sets" 235 | (let [as ["a" \a]] 236 | (dotimes [_ 100] 237 | (is (every? #(= % \a) (core/rand-str as 100)))))))) 238 | 239 | (deftest excludes?-test 240 | (testing "should return true if coll does not contain key" 241 | (is (core/excludes? {:foo 1} :bar))) 242 | (testing "should return false if coll does contain key" 243 | (is (not (core/excludes? {:foo 1} :foo))))) 244 | 245 | (deftest contains-some-test 246 | (testing "should return nil if coll doesn't contain any of the keys" 247 | (is (= nil (core/contains-some {:foo 1} [:bar :baz :bam])))) 248 | (testing "should return the first key that coll does contain" 249 | (is (= :baz (core/contains-some {:foo 1 :baz 2 :bam 3} [:bar :baz :bam]))))) 250 | 251 | (deftest excludes-some-test 252 | (testing "should return nil if coll does `contain?` all of the keys" 253 | (is (= nil (core/excludes-some {:bar 1 :baz 2} [:bar :baz])))) 254 | (testing "should return the first key that coll does *not* `contain?`" 255 | (is (= :baz (core/excludes-some {:bar 1 :foo 2} [:foo :baz :bam]))))) 256 | 257 | (deftest mapvals-test 258 | (testing "should default to applying a function to all of the keys" 259 | (is (= {:a 2 :b 3} (core/mapvals inc {:a 1 :b 2})))) 260 | (testing "should support applying a function to a subset of the keys" 261 | (is (= {:a 2 :b 2} (core/mapvals inc [:a] {:a 1 :b 2})))) 262 | (testing "should support keywords as the function to apply to all of the keys" 263 | (is (= {:a 1 :b 2} (core/mapvals :foo {:a {:foo 1} :b {:foo 2}})))) 264 | (testing "should support keywords as the function to apply to a subset of the keys" 265 | (is (= {:a 1 :b {:foo 2}} (core/mapvals :foo [:a] {:a {:foo 1} :b {:foo 2}}))))) 266 | 267 | (deftest maptrans-test 268 | (testing "should fail if the keys-fns param isn't valid" 269 | (is (thrown? AssertionError (core/maptrans "blah" {:a 1 :b 1})))) 270 | (testing "should transform a map based on the given functions" 271 | (is (= {:a 3 :b 3 :c 3 :d 3} 272 | (core/maptrans {[:a :b] inc [:d] dec} {:a 2 :b 2 :c 3 :d 4})))) 273 | (testing "should accept keywords as functions in the keys-fns param" 274 | (is (= {:a 3 :b 3} 275 | (core/maptrans {[:a :b] :foo} {:a {:foo 3} :b {:foo 3}}))))) 276 | 277 | (deftest dissoc-if-nil-test 278 | (let [testmap {:a 1 :b nil}] 279 | (testing "should remove the key if the value is nil" 280 | (is (= (dissoc testmap :b) (core/dissoc-if-nil testmap :b)))) 281 | (testing "should not remove the key if the value is not nil" 282 | (is (= testmap (core/dissoc-if-nil testmap :a)))))) 283 | 284 | (deftest dissoc-in-test 285 | (let [testmap {:a {:b 1 :c {:d 2}}}] 286 | (testing "should remove the key" 287 | (is (= {:a {:c {:d 2}}} (core/dissoc-in testmap [:a :b])))) 288 | (testing "should remove the empty map" 289 | (is (= {:a {:b 1}} (core/dissoc-in testmap [:a :c :d])))))) 290 | 291 | (deftest walk-leaves-test 292 | (testing "should apply a function to all of the leaves" 293 | (is (= {:a 2 :b {:c 5}} (core/walk-leaves {:a 1 :b {:c 4}} inc))))) 294 | 295 | (deftest merge-with-key-test 296 | (let [m1 {:a 1 :b 2} 297 | m2 {:a 3 :b 4} 298 | merge-fn (fn [k v1 v2] 299 | (if (= k :a) 300 | (+ v1 v2) 301 | v2))] 302 | (is (= {:a 4 :b 4} (core/merge-with-key merge-fn m1 m2))))) 303 | 304 | (deftest deep-merge-test 305 | (testing "should deeply nest duplicate keys that both have map values" 306 | (let [testmap-1 {:foo {:bar :baz}, :pancake :flapjack} 307 | testmap-2 {:foo {:fuzz {:buzz :quux}}}] 308 | (is (= {:foo {:bar :baz, :fuzz {:buzz :quux}}, :pancake :flapjack} 309 | (core/deep-merge testmap-1 testmap-2))))) 310 | (testing "should combine duplicate keys' values that aren't all maps by 311 | calling the provided function" 312 | (let [testmap-1 {:foo {:bars 2}} 313 | testmap-2 {:foo {:bars 3, :bazzes 4}}] 314 | (is (= {:foo {:bars 5, :bazzes 4}} (core/deep-merge-with + testmap-1 testmap-2))))) 315 | (testing "core/deep-merge-with-keys should pass keys to specified fn" 316 | (let [m1 {:a {:b 1 :c 2}} 317 | m2 {:a {:b 3 :c 4}} 318 | merge-fn (fn [ks v1 v2] 319 | (if (= ks [:a :b]) 320 | (+ v1 v2) 321 | v2))] 322 | (is (= {:a {:b 4 :c 4}} (core/deep-merge-with-keys merge-fn m1 m2)))))) 323 | 324 | (deftest filter-map-test 325 | (testing "should filter based on a given predicate" 326 | (let [test-map {:dog 5 :cat 4 :mouse 7 :cow 6}] 327 | (is (= (core/filter-map (fn [_k v] (even? v)) test-map) 328 | {:cat 4, :cow 6})) 329 | (is (= (core/filter-map (fn [k _v] (= 3 (count (name k)))) test-map) 330 | {:dog 5, :cat 4, :cow 6})) 331 | (is (= (core/filter-map (fn [k v] (and (= 3 (count (name k))) (> v 5))) test-map) 332 | {:cow 6})) 333 | (is (= (core/filter-map (fn [_k _v] true) test-map) 334 | test-map)) 335 | (is (= (core/filter-map (fn [_k _v] false) test-map) 336 | {})))) 337 | (testing "should return empty map if given nil" 338 | (is (= {} (core/filter-map nil nil))))) 339 | 340 | (deftest missing?-test 341 | (let [sample {:a "asdf" :b "asdf" :c "asdf"}] 342 | (testing "should return true for single key items if they don't exist in the coll" 343 | (is (true? (core/missing? sample :n)))) 344 | (testing "should return false for single key items if they exist in the coll" 345 | (is (false? (core/missing? sample :c)))) 346 | (testing "should return true for multiple key items if they all don't exist in the coll" 347 | (is (true? (core/missing? sample :n :f :g :z :h)))) 348 | (testing "should return false for multiple key items if one item exists in the coll" 349 | (is (false? (core/missing? sample :n :b :f))) 350 | (is (false? (core/missing? sample :a :h :f)))) 351 | (testing "should return false for multiple key items if all items exist in the coll" 352 | (is (false? (core/missing? sample :a :b :c)))))) 353 | 354 | (deftest order-by-test 355 | (let [test-data [{:id 1 356 | :k1 "ALPHA" 357 | :k2 "BETA" 358 | :k3 "GAMMA"} 359 | {:id 2 360 | :k1 "ALPHA" 361 | :k2 "BETA" 362 | :k3 "EPSILON"} 363 | {:id 3 364 | :k1 "ALPHA" 365 | :k2 "CHARLIE" 366 | :k3 "DELTA"} 367 | {:id 4 368 | :k1 "ALPHA" 369 | :k2 "CHARLIE" 370 | :k3 "FOXTROT"} 371 | {:id 5 372 | :k1 "alpha" 373 | :k2 "beta" 374 | :k3 "alpha"}]] 375 | (testing "single field, ascending core/order-by" 376 | (is (= [3 2 4 1 5] (map :id (core/order-by [[:k3 :ascending]] test-data))))) 377 | (testing "single field, descending core/order-by" 378 | (is (= [5 1 4 2 3] (map :id (core/order-by [[:k3 :descending]] test-data))))) 379 | (testing "single function, descending core/order-by" 380 | (is (= [1 4 2 3 5] (map :id (core/order-by [[#(string/lower-case (:k3 %)) :descending]] test-data))))) 381 | (testing "multiple core/order-bys" 382 | (is (= [5 3 4 2 1] (map :id 383 | (core/order-by 384 | [[#(string/upper-case (:k1 %)) :ascending] 385 | [:k2 :descending] 386 | [:k3 :ascending]] 387 | test-data))))))) 388 | 389 | (deftest sort-nested-maps-test 390 | (testing "with nested structure" 391 | (let [input {:b "asdf" 392 | :a {:z "asdf" 393 | :k [:z {:z 26 :a 1} :c] 394 | :a {:m 12 :a 1} 395 | :b "asdf"}} 396 | output (core/sort-nested-maps input)] 397 | (testing "after sorting, maps should still match" 398 | (is (= input output))) 399 | (testing "all maps levels of output should be sorted" 400 | (is (sorted? output)) 401 | (is (sorted? (:a output))) 402 | (is (sorted? (get (vec (get-in output [:a :k])) 1))) 403 | (is (sorted? (get-in output [:a :a])))))) 404 | (testing "with a string" 405 | (let [input "string here" 406 | output (core/sort-nested-maps input)] 407 | (testing "should match" 408 | (is (= input output))))) 409 | (testing "with a list" 410 | (let [input '(:a :b :c) 411 | output (core/sort-nested-maps input)] 412 | (testing "should still match" 413 | (is (= input output)))))) 414 | 415 | (deftest without-ns-test 416 | (testing "removes namespace from a namespaced keyword" 417 | (is (= :foo (core/without-ns :foo/foo))) 418 | (is (= :foo (core/without-ns ::foo)))) 419 | (testing "doesn't alter non-namespaced keyword" 420 | (let [kw :foo] 421 | (is (= kw (core/without-ns kw)))))) 422 | 423 | (deftest string-hashing-test 424 | (testing "Computing a SHA-1 for a UTF-8 string" 425 | (testing "should fail if not passed a string" 426 | #_{:clj-kondo/ignore [:type-mismatch]} 427 | (is (thrown? AssertionError (core/utf8-string->sha1 1234)))) 428 | 429 | (testing "should produce a stable hash" 430 | (is (= (core/utf8-string->sha1 "foobar") 431 | (core/utf8-string->sha1 "foobar")))) 432 | 433 | (testing "should produce the correct hash" 434 | (is (= "8843d7f92416211de9ebb963ff4ce28125932878" 435 | (core/utf8-string->sha1 "foobar"))))) 436 | 437 | (testing "Computing a SHA-256 for a UTF-8 string" 438 | (testing "should fail if not passed a string" 439 | #_{:clj-kondo/ignore [:type-mismatch]} 440 | (is (thrown? AssertionError (core/utf8-string->sha256 1234)))) 441 | 442 | (testing "should produce a stable hash" 443 | (is (= (core/utf8-string->sha256 "foobar") 444 | (core/utf8-string->sha256 "foobar")))) 445 | 446 | (testing "should produce the correct hash" 447 | (is (= "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" 448 | (core/utf8-string->sha256 "foobar")))))) 449 | 450 | (deftest stream-hashing 451 | (testing "Computing a SHA-256 hash for an input stream" 452 | (testing "should fail if not passed an input stream" 453 | #_ {:clj-kondo/ignore [:type-mismatch]} 454 | (is (thrown? AssertionError (core/stream->sha256 "what")))) 455 | 456 | (let [stream-fn #(ByteArrayInputStream. (.getBytes "foobar" "UTF-8"))] 457 | (testing "should produce a stable hash" 458 | (is (= (core/stream->sha256 (stream-fn)) 459 | (core/stream->sha256 (stream-fn))))) 460 | 461 | (testing "should produce the correct hash" 462 | (is (= "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" 463 | (core/stream->sha256 (stream-fn)))))))) 464 | 465 | (deftest file-hashing 466 | (testing "Computing a SHA-256 hash for a file" 467 | (testing "should fail if not passed a file" 468 | (is (thrown? AssertionError (core/file->sha256 "what")))) 469 | 470 | (let [f (core/temp-file "sha256" ".txt")] 471 | (spit f "foobar") 472 | 473 | (testing "should produce a stable hash" 474 | (is (= (core/file->sha256 f) 475 | (core/file->sha256 f)))) 476 | 477 | (testing "should produce the correct hash" 478 | (is (= "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" 479 | (core/file->sha256 f))))))) 480 | 481 | (deftest temp-file-name-test 482 | (testing "The file should not exist." 483 | (is (not (fs/exists? (core/temp-file-name "foo"))))) 484 | (testing "It should be possible to create a file at the given path." 485 | (is (fs/create (core/temp-file-name "foo"))))) 486 | 487 | (deftest temp-file-test 488 | (testing "should create a temp file when not given a prefix" 489 | (let [f (core/temp-file)] 490 | (is (fs/file? f)))) 491 | (testing "should create a temp file when given a prefix and suffix" 492 | (let [^File f (core/temp-file "foo" ".bar")] 493 | (is (fs/file? f)) 494 | (is (.startsWith (.getName f) "foo")) 495 | (is (.endsWith (.getName f) ".bar")))) 496 | (testing "should create a temp dir when not given a prefix" 497 | (let [d (core/temp-dir)] 498 | (is (fs/directory? d)))) 499 | (testing "should create a temp dir when given a prefix and suffix" 500 | (let [^File d (core/temp-dir "foo" ".bar")] 501 | (is (fs/directory? d)) 502 | (is (.startsWith (.getName d) "foo")) 503 | (is (.endsWith (.getName d) ".bar"))))) 504 | 505 | 506 | (deftest ini-parsing 507 | (testing "Parsing ini files" 508 | (testing "should work for a single file" 509 | (let [tf (core/temp-file)] 510 | (spit tf "[foo]\nbar=baz") 511 | 512 | (testing "when specified as a file object" 513 | (is (= (core/inis-to-map tf) 514 | {:foo {:bar "baz"}}))) 515 | 516 | (testing "when specified as a string" 517 | (is (= (core/inis-to-map (core/absolute-path tf)) 518 | {:foo {:bar "baz"}}))))) 519 | 520 | (testing "should work for a directory" 521 | (let [td (core/temp-dir)] 522 | (testing "when no matching files exist" 523 | (is (= (core/inis-to-map td) {}))) 524 | 525 | (let [tf (fs/file td "a-test.ini")] 526 | (spit tf "[foo]\nbar=baz")) 527 | 528 | (testing "when only a single matching file exists" 529 | (is (= (core/inis-to-map td) 530 | {:foo {:bar "baz"}}))) 531 | 532 | (let [tf (fs/file td "b-test.ini")] 533 | ;; Now add a second file 534 | (spit tf "[bar]\nbar=baz")) 535 | 536 | (testing "when multiple matching files exist" 537 | (is (= (core/inis-to-map td) 538 | {:foo {:bar "baz"} 539 | :bar {:bar "baz"}}))))))) 540 | 541 | (deftest cli-parsing 542 | (testing "Should throw an error if a required option is missing" 543 | (let [got-expected-error (atom false)] 544 | (try+ 545 | (core/cli! [] [["-r" "--required" "A required field"]] [:required]) 546 | (catch map? m 547 | (is (contains? m :kind)) 548 | (is (= :puppetlabs.kitchensink.core/cli-error (:kind m))) 549 | (is (= :cli-error (core/without-ns (:kind m)))) 550 | (is (contains? m :msg)) 551 | (reset! got-expected-error true))) 552 | (is (true? @got-expected-error)))) 553 | 554 | (testing "Should throw a help message if --help is provided" 555 | (let [got-expected-help (atom false)] 556 | (try+ 557 | (core/cli! ["--help"] [] []) 558 | (catch map? m 559 | (is (contains? m :kind)) 560 | (is (= :puppetlabs.kitchensink.core/cli-help (:kind m))) 561 | (is (= :cli-help (core/without-ns (:kind m)))) 562 | (is (contains? m :msg)) 563 | (reset! got-expected-help true))) 564 | (is (true? @got-expected-help)))) 565 | 566 | (testing "Should return options map, remaining args, and summary after parsing CLI args" 567 | (let [[cli-data remaining-args summary] (core/cli! ["-a" "1234 Sunny ave." "--greeting" "Hey, what's up?" "--toggle" "extra-arg"] 568 | [["-g" "--greeting GREETING" "A string to greet somebody"] 569 | ["-a" "--address ADDRESS" "Somebody's address"] 570 | ["-t" "--toggle" "A flag/boolean option"]] [])] 571 | (is (map? cli-data)) 572 | (is (= "1234 Sunny ave." (cli-data :address))) 573 | (is (= "Hey, what's up?" (cli-data :greeting))) 574 | (is (= true (cli-data :toggle))) 575 | (is (vector? remaining-args)) 576 | (is (= ["extra-arg"] remaining-args)) 577 | (is (= (str " -g, --greeting GREETING A string to greet somebody\n" 578 | " -a, --address ADDRESS Somebody's address\n" 579 | " -t, --toggle A flag/boolean option\n" 580 | " -h, --help Show help") 581 | summary)))) 582 | 583 | (testing "Errors reported by tools.cli should be thrown out of cli! as slingshot exceptions" 584 | (let [got-expected-exception (atom false)] 585 | (try+ 586 | (let [specs [["-f" "--foo FOO" "Something that is foo"]] 587 | args ["--bar"]] 588 | (core/cli! args specs)) 589 | (catch map? m 590 | (is (= :puppetlabs.kitchensink.core/cli-error (:kind m))) 591 | (is (contains? m :msg)) 592 | (is (re-find 593 | #"Unknown option.*--bar" 594 | (m :msg))) 595 | (reset! got-expected-exception true))) 596 | (is (true? @got-expected-exception))))) 597 | 598 | (deftest cert-utils 599 | (testing "extracting cn from a dn" 600 | #_{:clj-kondo/ignore [:type-mismatch]} 601 | (is (thrown? AssertionError (core/cn-for-dn 123)) 602 | "should throw error when arg is a number") 603 | (is (thrown? AssertionError (core/cn-for-dn nil)) 604 | "should throw error when arg is nil") 605 | 606 | (is (= (core/cn-for-dn "") nil) 607 | "should return nil when passed an empty string") 608 | (is (= (core/cn-for-dn "MEH=bar") nil) 609 | "should return nil when no CN is present") 610 | (is (= (core/cn-for-dn "cn=foo.bar.com") nil) 611 | "should return nil when CN present but lower case") 612 | (is (= (core/cn-for-dn "cN=foo.bar.com") nil) 613 | "should return nil when CN present but with mixed case") 614 | 615 | (is (= (core/cn-for-dn "CN=foo.bar.com") "foo.bar.com") 616 | "should work when only CN is present") 617 | (is (= (core/cn-for-dn "CN=foo.bar.com,OU=something") "foo.bar.com") 618 | "should work when more than just the CN is present") 619 | (is (= (core/cn-for-dn "CN=foo.bar.com,OU=something") "foo.bar.com") 620 | "should work when more than just the CN is present") 621 | (is (= (core/cn-for-dn "OU=something,CN=foo.bar.com") "foo.bar.com") 622 | "should work when more than just the CN is present and CN is last") 623 | (is (= (core/cn-for-dn "OU=something,CN=foo.bar.com,D=foobar") "foo.bar.com") 624 | "should work when more than just the CN is present and CN is in the middle") 625 | (is (= (core/cn-for-dn "CN=foo.bar.com,CN=goo.bar.com,OU=something") "goo.bar.com") 626 | "should use the most specific CN if multiple CN's are present"))) 627 | 628 | (deftest cert-whitelist-auth 629 | (testing "cert whitelist authorizer" 630 | (testing "should fail when whitelist is not given" 631 | (is (thrown? AssertionError (core/cn-whitelist->authorizer nil)))) 632 | 633 | (testing "should fail when whitelist is given, but not readable" 634 | (is (thrown? java.io.FileNotFoundException 635 | (core/cn-whitelist->authorizer "/this/does/not/exist")))) 636 | 637 | (testing "when whitelist is present" 638 | (let [whitelist (core/temp-file)] 639 | (spit whitelist "foo\nbar\n") 640 | 641 | (let [authorized? (core/cn-whitelist->authorizer whitelist)] 642 | (testing "should allow plain-text, HTTP requests" 643 | (is (authorized? {:scheme :http :ssl-client-cn "foobar"}))) 644 | 645 | (testing "should fail HTTPS requests without a client cert" 646 | (is (not (authorized? {:scheme :https})))) 647 | 648 | (testing "should reject certs that don't appear in the whitelist" 649 | (is (not (authorized? {:scheme :https :ssl-client-cn "goo"})))) 650 | 651 | (testing "should accept certs that appear in the whitelist" 652 | (is (authorized? {:scheme :https :ssl-client-cn "foo"})))))))) 653 | 654 | (deftest memoization 655 | (testing "with an illegal bound" 656 | (is (thrown? AssertionError (core/bounded-memoize identity -1))) 657 | (is (thrown? AssertionError (core/bounded-memoize identity 0))) 658 | (is (thrown? AssertionError (core/bounded-memoize identity 1.5))) 659 | (is (thrown? AssertionError (core/bounded-memoize identity "five")))) 660 | 661 | (testing "with a legal bound" 662 | (let [f (testutils/call-counter) 663 | memoized (core/bounded-memoize f 2)] 664 | (testing "should only call the function once per argument" 665 | (is (= (testutils/times-called f) 0)) 666 | 667 | (memoized 0) 668 | (is (= (testutils/times-called f) 1)) 669 | 670 | (memoized 0) 671 | (is (= (testutils/times-called f) 1)) 672 | 673 | (memoized 1) 674 | (is (= (testutils/times-called f) 2))) 675 | 676 | ;; We call it here for a hit, which we expect not to clear the cache, 677 | ;; then call it again to verify the cache wasn't cleared and therefore f 678 | ;; wasn't called 679 | (testing "should not clear the cache at max size on a hit" 680 | (memoized 1) 681 | (is (= (testutils/times-called f) 2)) 682 | 683 | (memoized 1) 684 | (is (= (testutils/times-called f) 2))) 685 | 686 | ;; Now call it with a new argument to clear the cache, then with an old 687 | ;; one to show f is called 688 | (testing "should clear the cache at max size on a miss" 689 | (memoized 2) 690 | (is (= (testutils/times-called f) 3)) 691 | 692 | (memoized 3) 693 | (is (= (testutils/times-called f) 4)))))) 694 | 695 | (deftest uuid-handling 696 | (testing "a generated core/uuid is a valid core/uuid" 697 | (is (core/uuid? (core/uuid)))) 698 | (testing "a phrase is not a core/uuid" 699 | (is (not (core/uuid? "Hello World"))))) 700 | 701 | (deftest jvm-versions 702 | (testing "comparing same versions should return 0" 703 | (is (= 0 (core/compare-jvm-versions "1.7.0_3" "1.7.0_3")))) 704 | 705 | (testing "comparing same versions should return 0, even with trailing fields" 706 | (is (= 0 (core/compare-jvm-versions "1.7.0_3" "1.7.0_3-beta3")))) 707 | 708 | (testing "should detect older versions" 709 | (is (neg? (core/compare-jvm-versions "1.7.0_0" "1.7.0_3"))) 710 | (is (neg? (core/compare-jvm-versions "1.7.0_0" "1.7.0_3-beta3"))) 711 | (is (neg? (core/compare-jvm-versions "1.7.0_2" "1.7.0_03"))) 712 | (is (neg? (core/compare-jvm-versions "1.6.0_3" "1.7.0_3"))) 713 | (is (neg? (core/compare-jvm-versions "1.6.0_2" "1.7.0_3"))) 714 | (is (neg? (core/compare-jvm-versions "0.6.0_2" "1.7.0_3")))) 715 | 716 | (testing "should detect newer versions" 717 | (is (pos? (core/compare-jvm-versions "1.7.0_13" "1.7.0_3"))) 718 | (is (pos? (core/compare-jvm-versions "1.8.0_3" "1.7.0_3"))) 719 | (is (pos? (core/compare-jvm-versions "1.8.0_3" "1.7.0_3-beta3"))) 720 | (is (pos? (core/compare-jvm-versions "2.7.0_3" "1.7.0_3"))) 721 | (is (pos? (core/compare-jvm-versions "1.7.0_10" "1.7.0_3"))))) 722 | 723 | (deftest some-pred->>-macro 724 | (testing "should thread all the way through if the pred never matches" 725 | (is (= 10 726 | (core/some-pred->> nil? 1 727 | (* 2) 728 | (+ 9) 729 | #_ {:clj-kondo/ignore [:invalid-arity]} 730 | (dec))))) 731 | (testing "should break and return the value if the pred matches" 732 | (is (= {:a 1} 733 | (core/some-pred->> map? 5 734 | (/ 5) 735 | #_ {:clj-kondo/ignore [:invalid-arity]} 736 | (assoc {} :a) 737 | #_ {:clj-kondo/ignore [:invalid-arity]} 738 | (keys)))))) 739 | 740 | (deftest while-let-macro 741 | (let [counter (atom 0) 742 | list (ArrayList.)] 743 | (dotimes [_ 5] (.add list "foo")) 744 | (let [iter (.iterator list)] 745 | #_ {:clj-kondo/ignore [:unresolved-symbol]} 746 | (core/while-let [item (and (.hasNext iter) 747 | (.next iter))] 748 | (swap! counter inc))) 749 | (is (= 5 @counter)))) 750 | 751 | (deftest spit-ini-test 752 | (let [tf (core/temp-file)] 753 | (spit tf "[foo]\nbar=baz\n[bar]\nfoo=baz") 754 | (let [ini-map (core/ini-to-map tf)] 755 | (is (= ini-map 756 | {:foo {:bar "baz"} 757 | :bar {:foo "baz"}})) 758 | (testing "changing existing keys" 759 | (let [result-file (core/temp-file)] 760 | (core/spit-ini result-file (-> ini-map 761 | (assoc-in [:foo :bar] "baz changed") 762 | (assoc-in [:bar :foo] "baz also changed"))) 763 | (is (= {:foo {:bar "baz changed"} 764 | :bar {:foo "baz also changed"}} 765 | (core/ini-to-map result-file))))) 766 | (testing "adding a new section to an existing ini" 767 | (let [result-file (core/temp-file)] 768 | (core/spit-ini result-file (assoc-in ini-map [:baz :foo] "bar")) 769 | (is (= {:foo {:bar "baz"} 770 | :bar {:foo "baz"} 771 | :baz {:foo "bar"}} 772 | (core/ini-to-map result-file)))))))) 773 | 774 | (deftest duplicate-ini-entries 775 | (testing "duplicate settings" 776 | (let [tempfile (core/temp-file)] 777 | (spit tempfile "[foo]\nbar=baz\nbar=bizzle\n") 778 | (is (thrown-with-msg? 779 | IllegalArgumentException 780 | #"Duplicate configuration entry: \[:foo :bar\]" 781 | (core/ini-to-map tempfile)))) 782 | 783 | (let [tempdir (core/temp-dir) 784 | tempfile1 (fs/file tempdir "initest1.ini") 785 | tempfile2 (fs/file tempdir "initest2.ini")] 786 | (spit tempfile1 "[foo]\nsetting1=hi\nbar=baz\n") 787 | (spit tempfile2 "[foo]\nsetting2=hi\nbar=bizzle\n") 788 | (is (thrown-with-msg? 789 | IllegalArgumentException 790 | #"Duplicate configuration entry: \[:foo :bar\]" 791 | (core/inis-to-map tempdir))))) 792 | 793 | (testing "duplicate sections but no duplicate settings" 794 | (let [tempdir (core/temp-dir) 795 | tempfile1 (fs/file tempdir "initest1.ini") 796 | tempfile2 (fs/file tempdir "initest.ini")] 797 | (spit tempfile1 "[foo]\nsetting1=hi\nbar=baz\n") 798 | (spit tempfile2 "[foo]\nsetting2=hi\nbunk=bizzle\n") 799 | (is (= {:foo {:setting1 "hi" 800 | :bar "baz" 801 | :setting2 "hi" 802 | :bunk "bizzle"}} 803 | (core/inis-to-map tempdir)))))) 804 | 805 | (deftest timeout-test 806 | (let [wait-return (fn [time val] (Thread/sleep time) val)] 807 | (testing "core/with-timeout" 808 | (testing "does nothing if the body returns within the limit" 809 | (is (= true 810 | (core/with-timeout 1 false 811 | (wait-return 500 true))))) 812 | (testing "returns the default value if the body times out" 813 | (is (= false 814 | (core/with-timeout 1 false 815 | (wait-return 1005 true)))))))) 816 | 817 | (deftest ^:slow open-port-num-test 818 | (let [port-in-use (core/open-port-num)] 819 | (with-open [_s (java.net.ServerSocket. port-in-use)] 820 | (let [open-ports (set (take 60000 (repeatedly core/open-port-num)))] 821 | (is (every? pos? open-ports)) 822 | (is (not (contains? open-ports port-in-use))))))) 823 | 824 | (deftest assoc-if-new-test 825 | (testing "assoc-if-new assocs appropriately" 826 | (is (= {:a "foo"} 827 | (core/assoc-if-new {:a "foo"} :a "bar"))) 828 | (is (= {:a "bar" :b "foo"} 829 | (core/assoc-if-new {:b "foo"} :a "bar"))) 830 | (is (= {:a "foo" :b "bar"} 831 | (core/assoc-if-new {} :a "foo" :b "bar"))) 832 | (is (= {:a "foo" :b nil} 833 | (core/assoc-if-new {:b nil} :a "foo" :b "bar"))) 834 | (is (= {:a "foo" :b "baz"} 835 | (core/assoc-if-new {:b "baz"} :a "foo" :b "bar"))))) 836 | 837 | (deftest deref-swap-test 838 | (testing "deref-swap behaves as advertised" 839 | (let [a (atom 10) 840 | b (core/deref-swap! a inc)] 841 | (is (= 11 @a)) 842 | (is (= 10 b))))) 843 | 844 | (deftest parse-interval-test 845 | (are [x y] (= x (core/parse-interval y)) 846 | (t/seconds 11) "11s" 847 | (t/minutes 12) "12m" 848 | (t/hours 13) "13h" 849 | (t/days 14) "14d" 850 | (t/years 15) "15y" 851 | (t/seconds 0) "0" 852 | (t/seconds 10) "10" 853 | nil "15a" 854 | nil "h" 855 | nil "12hhh" 856 | nil "12H" 857 | nil "1,300y" 858 | nil "" 859 | nil nil)) 860 | 861 | (deftest base-type-test 862 | (is (= "application/json" (core/base-type "application/json"))) 863 | (is (= "application/json" (core/base-type "application/json;charset=UTF-8"))) 864 | (is (= "application/json" (core/base-type "application/json ; charset=UTF-8"))) 865 | 866 | (is (= "foo/bar" (core/base-type "foo/bar;someparam=baz"))) 867 | 868 | (is (nil? (core/base-type "application/json:charset=UTF-8"))) 869 | (is (nil? (core/base-type "appl=ication/json ; charset=UTF-8")))) 870 | 871 | (deftest now->timestamp-string-test 872 | (let [result (core/now->timestamp-string)] 873 | (is (re-matcher #"202\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d" result)))) 874 | 875 | (deftest timestamp-string->ZonedDateTime-test 876 | (testing "throws exceptions for invalid timestamps" 877 | (is (thrown? DateTimeParseException (core/timestamp-string->ZonedDateTime "not a timestamp")))) 878 | (testing "correctly converts timestamps to local date times" 879 | (let [result (core/timestamp-string->ZonedDateTime "2023-06-14T17:27:51.732102Z")] 880 | (is (instance? ZonedDateTime result)) 881 | (is (= 2023 (.getYear result))) 882 | (is (= Month/JUNE (.getMonth result))) 883 | (is (= 14 (.getDayOfMonth result))) 884 | (is (= 17 (.getHour result))) 885 | (is (= 27 (.getMinute result))) 886 | (is (= 51 (.getSecond result))) 887 | (is (= "Z" (.toString (.getZone result)))))) 888 | (testing "respects time zone" 889 | (let [result (core/timestamp-string->ZonedDateTime "2023-06-14T17:27:51.732102+01:00")] 890 | (is (instance? ZonedDateTime result)) 891 | (is (= 2023 (.getYear result))) 892 | (is (= Month/JUNE (.getMonth result))) 893 | (is (= 14 (.getDayOfMonth result))) 894 | (is (= 17 (.getHour result))) 895 | (is (= 27 (.getMinute result))) 896 | (is (= 51 (.getSecond result))) 897 | (is (= "+01:00" (.toString (.getZone result))))))) 898 | 899 | 900 | (deftest ZonedDateTime->utc-ZonedDateTime-test 901 | (testing "does not impact timezone in utc" 902 | (let [result (core/ZonedDateTime->utc-ZonedDateTime (core/timestamp-string->ZonedDateTime "2023-06-14T17:27:51.732102Z"))] 903 | (is (instance? ZonedDateTime result)) 904 | (is (= 2023 (.getYear result))) 905 | (is (= Month/JUNE (.getMonth result))) 906 | (is (= 14 (.getDayOfMonth result))) 907 | (is (= 17 (.getHour result))) 908 | (is (= 27 (.getMinute result))) 909 | (is (= 51 (.getSecond result))) 910 | (is (= "UTC" (.toString (.getZone result)))))) 911 | (testing "converts non UTC to UtC" 912 | (let [result (core/ZonedDateTime->utc-ZonedDateTime (core/timestamp-string->ZonedDateTime "2023-06-14T17:27:51.732102+01:00"))] 913 | (is (instance? ZonedDateTime result)) 914 | (is (= 2023 (.getYear result))) 915 | (is (= Month/JUNE (.getMonth result))) 916 | (is (= 14 (.getDayOfMonth result))) 917 | (is (= 16 (.getHour result))) 918 | (is (= 27 (.getMinute result))) 919 | (is (= 51 (.getSecond result))) 920 | (is (= "UTC" (.toString (.getZone result))))))) 921 | 922 | (deftest is-only-number?-test 923 | (testing "returns false for things that aren't just numbers" 924 | (is (false? (core/is-only-number? "one"))) 925 | (is (false? (core/is-only-number? "1one"))) 926 | (is (false? (core/is-only-number? "one1"))) 927 | (is (false? (core/is-only-number? "1 1"))) 928 | (is (false? (core/is-only-number? "-1"))) 929 | (is (false? (core/is-only-number? "1.1")))) 930 | (testing "returns true for things that are just numbers" 931 | (is (true? (core/is-only-number? "1"))) 932 | (is (true? (core/is-only-number? "100000"))) 933 | (is (true? (core/is-only-number? "0"))))) 934 | 935 | 936 | (deftest starts-with-zero?-test 937 | (testing "returns false for strings that don't start with zero" 938 | (is (false? (core/starts-with-zero? "one"))) 939 | (is (false? (core/starts-with-zero? "1one"))) 940 | (is (false? (core/starts-with-zero? "1one0"))) 941 | (is (false? (core/starts-with-zero? "1 0")))) 942 | (testing "returns true for strings that start with zero" 943 | (is (true? (core/starts-with-zero? "0one"))) 944 | (is (true? (core/starts-with-zero? "01one"))) 945 | (is (true? (core/starts-with-zero? "01one0"))) 946 | (is (true? (core/starts-with-zero? "0 1 0"))))) 947 | 948 | (deftest compare-versions-test 949 | (testing "expected input produces expected output" 950 | (is (zero? (core/compare-versions "1.2" "1.2"))) 951 | (is (neg? (core/compare-versions "1.2" "1.3"))) 952 | (is (pos? (core/compare-versions "1.3" "1.2"))) 953 | (is (zero? (core/compare-versions "0002" "0002"))) 954 | (is (neg? (core/compare-versions "0002" "1"))) 955 | (is (neg? (core/compare-versions "0002" "1.06"))) 956 | (is (neg? (core/compare-versions "0002" "1.1-3"))) 957 | (is (neg? (core/compare-versions "0002" "1.1-6"))) 958 | (is (neg? (core/compare-versions "0002" "1.1.a"))) 959 | (is (pos? (core/compare-versions "1" "0002"))) 960 | (is (pos? (core/compare-versions "1.06" "0002"))) 961 | (is (pos? (core/compare-versions "1.1-3" "0002"))) 962 | (is (pos? (core/compare-versions "1.1-6" "0002"))) 963 | (is (pos? (core/compare-versions "1.1.a" "0002"))) 964 | (is (zero? (core/compare-versions "1" "1"))) 965 | (is (neg? (core/compare-versions "1" "1.06"))) 966 | (is (neg? (core/compare-versions "1" "1.1-3"))) 967 | (is (neg? (core/compare-versions "1" "1.1-6"))) 968 | (is (neg? (core/compare-versions "1" "1.1.a"))) 969 | (is (neg? (core/compare-versions "1" "2"))) 970 | (is (neg? (core/compare-versions "1" "2.0"))) 971 | (is (pos? (core/compare-versions "1.06" "1"))) 972 | (is (pos? (core/compare-versions "1.1-3" "1"))) 973 | (is (pos? (core/compare-versions "1.1-6" "1"))) 974 | (is (pos? (core/compare-versions "1.1.a" "1"))) 975 | (is (pos? (core/compare-versions "2" "1"))) 976 | (is (pos? (core/compare-versions "2.0" "1"))) 977 | (is (zero? (core/compare-versions "1.1-3" "1.1-3"))) 978 | (is (neg? (core/compare-versions "1.1-3" "1.1-6"))) 979 | (is (neg? (core/compare-versions "1.1-3" "1.1.a"))) 980 | (is (neg? (core/compare-versions "1.1-3" "2"))) 981 | (is (neg? (core/compare-versions "1.1-3" "2.0"))) 982 | (is (pos? (core/compare-versions "1.1-6" "1.1-3"))) 983 | (is (pos? (core/compare-versions "1.1.a" "1.1-3"))) 984 | (is (pos? (core/compare-versions "2" "1.1-3"))) 985 | (is (pos? (core/compare-versions "2.0" "1.1-3"))) 986 | (is (zero? (core/compare-versions "1.1.6" "1.1.6"))) 987 | (is (neg? (core/compare-versions "1.1.6" "1.1.a"))) 988 | (is (neg? (core/compare-versions "1.1.6" "2"))) 989 | (is (neg? (core/compare-versions "1.1.6" "2.0"))) 990 | (is (pos? (core/compare-versions "1.1.a" "1.1.6"))) 991 | (is (pos? (core/compare-versions "2" "1.1.6"))) 992 | (is (pos? (core/compare-versions "2.0" "1.1.6"))) 993 | (is (zero? (core/compare-versions "1.1.a" "1.1.a"))) 994 | (is (neg? (core/compare-versions "1.1.a" "2"))) 995 | (is (neg? (core/compare-versions "1.1.a" "2.0"))) 996 | (is (pos? (core/compare-versions "2" "1.1.a"))) 997 | (is (zero? (core/compare-versions "2" "2"))) 998 | (is (zero? (core/compare-versions "2.0" "2.0"))) 999 | ;; this might not be expected, but it is the behavior. 1000 | (is (pos? (core/compare-versions "2.0" "2"))) 1001 | (is (pos? (core/compare-versions "2.0" "1.1.a"))) 1002 | (is (neg? (core/compare-versions "2.4" "2.4b"))) 1003 | (is (pos? (core/compare-versions "2.4b" "2.4a"))))) 1004 | 1005 | (deftest get-lein-project-version-test 1006 | (testing "unknown project returns nil" 1007 | (is (nil? (core/get-lein-project-version "unknown")))) 1008 | (testing "known project returns something" 1009 | (is (string? (core/get-lein-project-version "kitchensink"))) 1010 | (is (not (string/blank? (core/get-lein-project-version "kitchensink")))))) 1011 | 1012 | (deftest safe-parse-int-test 1013 | (testing "Converts appropriately" 1014 | (are [x y] (= x (core/safe-parse-int y)) 1015 | 0 0 1016 | 1 1 1017 | 0 "0" 1018 | 1 "1" 1019 | nil "notanumber"))) -------------------------------------------------------------------------------- /test/puppetlabs/kitchensink/file_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.file-test 2 | (:require [clojure.java.io :as io] 3 | [clojure.pprint] 4 | [clojure.test :refer :all] 5 | [me.raynes.fs :as fs] 6 | [puppetlabs.kitchensink.file :refer [atomic-write get-perms set-perms unzip-file untar-file delete-recursively]]) 7 | (:import (java.io BufferedWriter File FileNotFoundException) 8 | (java.nio.file Files Path) 9 | (java.nio.file.attribute FileAttribute))) 10 | 11 | (deftest atomic-write-test 12 | (testing "when the file doesn't exist" 13 | (let [tmp-file (.toString ^File (fs/temp-file "atomic-writes-test")) 14 | content "Something said, not good"] 15 | (fs/delete tmp-file) 16 | (atomic-write tmp-file #(.write ^BufferedWriter % content)) 17 | 18 | (testing "it writes the data" 19 | (is (= content (slurp tmp-file)))) 20 | 21 | (testing "it applies a default mode of 0640" 22 | (is (= "rw-r-----" (get-perms tmp-file)))) 23 | 24 | (testing "it applies the specified mode" 25 | (fs/delete tmp-file) 26 | (atomic-write tmp-file #(.write ^BufferedWriter % content) "rwxrwxrwx") 27 | (is (= "rwxrwxrwx" (get-perms tmp-file)))) 28 | 29 | (testing "it can create a read-only file" 30 | (fs/delete tmp-file) 31 | (atomic-write tmp-file #(.write ^BufferedWriter % content) "r--------") 32 | (is (= "r--------" (get-perms tmp-file)))) 33 | 34 | (fs/delete tmp-file))) 35 | 36 | (testing "when overwriting" 37 | (let [tmp-file (.toString ^File (fs/temp-file "atomic-writes-test")) 38 | content "Something said, not good"] 39 | (set-perms tmp-file "rwxr-x--x") 40 | 41 | (testing "it preserves existing permissions" 42 | (atomic-write tmp-file #(.write ^BufferedWriter % content)) 43 | (is (= "rwxr-x--x" (get-perms tmp-file)))) 44 | 45 | (testing "it overwrites existing permissions if specified" 46 | (atomic-write tmp-file #(.write ^BufferedWriter % content) "rw-rw----") 47 | (is (= "rw-rw----" (get-perms tmp-file)))) 48 | 49 | (testing "it updates a read-only file" 50 | (set-perms tmp-file "r--------") 51 | (atomic-write tmp-file #(.write ^BufferedWriter % content)) 52 | (is (= content (slurp tmp-file)))) 53 | 54 | (fs/delete tmp-file))) 55 | 56 | (testing "the supplied function is allowed to close the writer" 57 | (let [tmp-file (.toString ^File (fs/temp-file "atomic-writes-test")) 58 | write-fn (fn [^BufferedWriter writer] 59 | (.write writer "They called me slow") 60 | (.close writer))] 61 | 62 | (atomic-write tmp-file write-fn) 63 | 64 | (fs/delete tmp-file)))) 65 | 66 | (deftest unzip-file-test 67 | (testing "can unzip known .gz file" 68 | (let [tmp-file (.toString ^File (fs/temp-file "unzipped-file"))] 69 | (unzip-file "dev-resources/fixtures/plain-text.txt.gz" tmp-file) 70 | (is (= (slurp tmp-file) (slurp "dev-resources/fixtures/plain-text.txt"))))) 71 | (testing "creates missing directories" 72 | (let [tmp-file (str (.toString ^File (fs/temp-dir "test-directory")) "/foo/bar/baz/thing.txt")] 73 | (unzip-file "dev-resources/fixtures/plain-text.txt.gz" tmp-file) 74 | (is (= (slurp tmp-file) (slurp "dev-resources/fixtures/plain-text.txt"))))) 75 | (testing "throws if file does not exist" 76 | (let [tmp-file (.toString ^File (fs/temp-file "not-unzipped-file"))] 77 | (is (thrown? FileNotFoundException (unzip-file "something" tmp-file))))) 78 | (testing "refuses to overwrite input file" 79 | (let [tmp-file (.toString ^File (fs/temp-file "copy-of-file"))] 80 | (io/copy (io/file "dev-resources/fixtures/plain-text.txt.gz") (io/file tmp-file)) 81 | (unzip-file tmp-file tmp-file) 82 | (is (= (slurp "dev-resources/fixtures/plain-text.txt.gz") (slurp tmp-file)))))) 83 | 84 | (def large-file-line-content 85 | "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789") 86 | (deftest untar-file-test 87 | (testing "can untar known tar file" 88 | (let [temp-dir (.toString ^File (fs/temp-dir "test-tar-directory")) 89 | temp-dir-length (count temp-dir)] 90 | (untar-file "dev-resources/fixtures/tar-contents.tar" temp-dir) 91 | ;; extract the paths, remove the temp-dir front, and sort the result so the results are predictable 92 | (let [names (sort (map #(subs (str %) temp-dir-length) (file-seq (io/file temp-dir))))] 93 | (is (= ["" 94 | "/tar-contents" 95 | "/tar-contents/bar" 96 | "/tar-contents/bar/1" 97 | "/tar-contents/bar/1/largetextfile.txt" 98 | "/tar-contents/bar/2" 99 | "/tar-contents/bar/3" 100 | "/tar-contents/bar/4" 101 | "/tar-contents/bar/4/hello.txt" 102 | "/tar-contents/foo" 103 | "/tar-contents/foo/foo" 104 | "/tar-contents/foo/hi.txt"] 105 | names)) 106 | (is (= "this is a greeting.\n" (slurp (str temp-dir "/tar-contents/bar/4/hello.txt")))) 107 | (is (= "This is a warm greeting.\n" (slurp (str temp-dir "/tar-contents/foo/hi.txt")))) 108 | (testing "large files are correctly processed" 109 | (let [counter (atom 0)] 110 | (with-open [rdr (io/reader (str temp-dir "/tar-contents/bar/1/largetextfile.txt"))] 111 | (doseq [line (line-seq rdr)] 112 | (swap! counter inc) 113 | ;; each line should be identical 114 | (is (= large-file-line-content line)))) 115 | ;; there should be 90 lines in the file. 116 | (is (= 90 @counter)))))))) 117 | 118 | 119 | (defn recursively-fill-directory 120 | [^Path directory depth max-depth] 121 | (when (< depth max-depth) 122 | (let [num-files (rand-int 5) 123 | num-dirs (rand-int 5)] 124 | (dotimes [n num-files] 125 | (Files/createTempFile directory (str "file-" depth "-" n "-") ".tmp" (into-array FileAttribute []))) 126 | (dotimes [n num-dirs] 127 | (let [new-dir (Files/createTempDirectory directory (str "directory-" depth "-" n "-") (into-array FileAttribute []))] 128 | (recursively-fill-directory new-dir (inc depth) max-depth)))))) 129 | (deftest delete-recursively-test 130 | (testing "can delete a single file" 131 | (let [tmp-file (Files/createTempFile "prefix-" ".tmp" (into-array FileAttribute []))] 132 | (is (.exists (.toFile tmp-file))) 133 | (is (delete-recursively (.getAbsolutePath (.toFile tmp-file)))) 134 | (is (not (.exists (.toFile tmp-file)))))) 135 | (testing "can delete a tree with files and directories" 136 | (let [tmp-dir (Files/createTempDirectory "testing" (into-array FileAttribute []))] 137 | (recursively-fill-directory tmp-dir 0 10) 138 | (is (.exists (.toFile tmp-dir))) 139 | (is (delete-recursively (.getAbsolutePath (.toFile tmp-dir)))) 140 | (is (not (.exists (.toFile tmp-dir))))))) 141 | -------------------------------------------------------------------------------- /test/puppetlabs/kitchensink/json_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.json-test 2 | (:require 3 | [clj-time.core :as clj-time] 4 | [puppetlabs.kitchensink.core :refer [temp-file]] 5 | [puppetlabs.kitchensink.json :as ks-json]) 6 | (:use [clojure.test]) 7 | (:import 8 | (com.fasterxml.jackson.core JsonGenerator) 9 | (java.io StringReader StringWriter) 10 | (java.time Instant LocalDate LocalDateTime))) 11 | 12 | (defn add-common-encoders-fixture 13 | [f] 14 | (ks-json/add-common-json-encoders!*) 15 | (f)) 16 | 17 | (use-fixtures :once add-common-encoders-fixture) 18 | 19 | (deftest test-with-custom-datetime-encoder 20 | (testing "should allow use of custom encoder" 21 | (is (= (ks-json/with-datetime-encoder (fn [_dt gn8r] (.writeString ^JsonGenerator gn8r "Beer-o-clock")) 22 | (ks-json/generate-string (clj-time/date-time 1989 11 17 5 6 24 654))) 23 | "\"Beer-o-clock\"")))) 24 | 25 | (deftest test-generate-string 26 | (testing "should generate a json string" 27 | (is (= (ks-json/generate-string {:a 1 :b 2}) 28 | "{\"a\":1,\"b\":2}"))) 29 | (testing "should generate a json string that has a Joda DataTime object in it and not explode" 30 | (is (= (ks-json/generate-string {:a 1 :b (clj-time/date-time 1986 10 14 4 3 27 456)}) 31 | "{\"a\":1,\"b\":\"1986-10-14T04:03:27.456Z\"}"))) 32 | (testing "should generate a json string that has a java.time.Instant in it and not explode" 33 | (is (= (ks-json/generate-string {:a 1 :b (Instant/parse "1994-09-19T21:00:30Z")}) 34 | "{\"a\":1,\"b\":\"1994-09-19T21:00:30Z\"}"))) 35 | (testing "should generate a json string that has a java.time.LocalDate in it and not explode" 36 | (is (= (ks-json/generate-string {:a 1 :b (LocalDate/parse "1953-05-29")}) 37 | "{\"a\":1,\"b\":\"1953-05-29\"}"))) 38 | (testing "should generate a json string that has a java.time.Instant in it and not explode" 39 | (is (= (ks-json/generate-string {:a 1 :b (LocalDateTime/parse "1954-07-31T21:00:30")}) 40 | "{\"a\":1,\"b\":\"1954-07-31T21:00:30\"}")))) 41 | 42 | (deftest test-generate-pretty-string 43 | (testing "should generate a json string" 44 | (is (= (ks-json/generate-pretty-string {:a 1 :b 2}) 45 | "{\n \"a\" : 1,\n \"b\" : 2\n}"))) 46 | (testing "should generate a json string that has a Joda DataTime object in it and not explode" 47 | (is (= (ks-json/generate-pretty-string {:a 1 :b (clj-time/date-time 1986 10 14 4 3 27 456)}) 48 | "{\n \"a\" : 1,\n \"b\" : \"1986-10-14T04:03:27.456Z\"\n}"))) 49 | (testing "should generate a json string that has a java.time.Instant in it and not explode" 50 | (is (= (ks-json/generate-pretty-string {:a 1 :b (Instant/parse "1994-09-19T21:00:30Z")}) 51 | "{\n \"a\" : 1,\n \"b\" : \"1994-09-19T21:00:30Z\"\n}"))) 52 | (testing "should generate a json string that has a java.time.LocalDate in it and not explode" 53 | (is (= (ks-json/generate-pretty-string {:a 1 :b (LocalDate/parse "1953-05-29")}) 54 | "{\n \"a\" : 1,\n \"b\" : \"1953-05-29\"\n}"))) 55 | (testing "should generate a json string that has a java.time.Instant in it and not explode" 56 | (is (= (ks-json/generate-pretty-string {:a 1 :b (LocalDateTime/parse "1954-07-31T21:00:30")}) 57 | "{\n \"a\" : 1,\n \"b\" : \"1954-07-31T21:00:30\"\n}")))) 58 | 59 | (deftest test-generate-stream 60 | (testing "should generate a json string from a stream" 61 | (let [sw (StringWriter.)] 62 | (ks-json/generate-stream {:a 1 :b 2} sw) 63 | (is (= (.toString sw) 64 | "{\"a\":1,\"b\":2}"))))) 65 | 66 | (deftest test-generate-pretty-stream 67 | (testing "should generate a pretty printed json string from a stream" 68 | (let [sw (StringWriter.)] 69 | (ks-json/generate-pretty-stream {:a 1 :b 2} sw) 70 | (is (= (.toString sw) 71 | "{\n \"a\" : 1,\n \"b\" : 2\n}"))))) 72 | 73 | (deftest test-parse-string 74 | (testing "should return a map from parsing a json string" 75 | (is (= (ks-json/parse-string "{\"a\":1,\"b\":2}") 76 | {"a" 1 "b" 2})))) 77 | 78 | (deftest test-parse-stream 79 | (testing "should return map from parsing a json stream" 80 | (is (= (ks-json/parse-stream (StringReader. "{\"a\":1,\"b\":2}")) 81 | {"a" 1 "b" 2})))) 82 | 83 | (deftest test-spit-json 84 | (let [json-out (temp-file "spit-json")] 85 | (testing "json output with keywords" 86 | (ks-json/spit-json json-out {:a 1 :b 2}) 87 | (is (= "{\n \"a\" : 1,\n \"b\" : 2\n}" 88 | (slurp json-out)))) 89 | (testing "json output with strings" 90 | (ks-json/spit-json json-out {"a" 1 "b" 2}) 91 | (is (= "{\n \"a\" : 1,\n \"b\" : 2\n}" 92 | (slurp json-out)))))) 93 | -------------------------------------------------------------------------------- /test/puppetlabs/kitchensink/testutils.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.testutils 2 | (:require [puppetlabs.kitchensink.core :as ks])) 3 | 4 | (defn call-counter 5 | "Returns a method that just tracks how many times it's called, and 6 | with what arguments. That information is stored in metadata for the 7 | method." 8 | [] 9 | (let [ncalls (ref 0) 10 | arguments (ref [])] 11 | (with-meta 12 | (fn [& args] 13 | (dosync 14 | (alter ncalls inc) 15 | (alter arguments conj args))) 16 | {:ncalls ncalls 17 | :args arguments}))) 18 | 19 | (defn times-called 20 | "Returns the number of times a `call-counter` function has been 21 | invoked." 22 | [f] 23 | (deref (:ncalls (meta f)))) 24 | 25 | (defmacro with-no-jvm-shutdown-hooks 26 | [& body] 27 | `(with-redefs [ks/add-shutdown-hook! (fn [_#] nil)] 28 | ~@body)) 29 | -------------------------------------------------------------------------------- /test/puppetlabs/kitchensink/testutils/fixtures.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.testutils.fixtures 2 | (:require [puppetlabs.kitchensink.core :as kitchensink])) 3 | 4 | (defn with-no-jvm-shutdown-hooks 5 | "Test fixture to prevent JVM shutdown hooks from being added. 6 | Only works if the shutdown hook is being added by a call to the 7 | utility function `puppetlabs.kitchensink.core/add-shutdown-hook!`." 8 | [f] 9 | (with-redefs [kitchensink/add-shutdown-hook! (fn [_] nil)] 10 | (f))) 11 | -------------------------------------------------------------------------------- /test/puppetlabs/kitchensink/time_test.clj: -------------------------------------------------------------------------------- 1 | (ns puppetlabs.kitchensink.time-test 2 | (:require [clojure.test :refer :all]) 3 | (:require [puppetlabs.kitchensink.time :as time]) 4 | (:import (java.time Duration))) 5 | 6 | (deftest duration-str->seconds-test 7 | (testing "returns an integer when given an integer" 8 | (are [x] (= x (time/duration-str->seconds x)) 9 | -1 0 1 2 3 4 5 6 7 8 9 10)) 10 | (testing "returns expected values for simple input" 11 | (are [x y] (= x (time/duration-str->seconds y)) 12 | 94608000 "3y" 13 | 259200 "3d" 14 | 10800 "3h" 15 | 180 "3m" 16 | 3 "3s")) 17 | (testing "Throws exceptions on invalid input" 18 | (is (thrown? IllegalArgumentException (time/duration-str->seconds "6y3m3y"))) 19 | (is (thrown? IllegalArgumentException (time/duration-str->seconds "3y6d7d8d3d"))) 20 | (is (thrown? IllegalArgumentException (time/duration-str->seconds "3y3y6d7d3h"))) 21 | (is (thrown? IllegalArgumentException (time/duration-str->seconds "3y6y5h6d3m"))) 22 | (is (thrown? IllegalArgumentException (time/duration-str->seconds "3y6y5h6d3m3s"))) 23 | (is (thrown? IllegalArgumentException (time/duration-str->seconds "not-a-duration"))))) 24 | 25 | (deftest duration-str->Duration-test 26 | (testing "returns a duration with the expected number of seconds when given an integer" 27 | (testing "returns an integer when given an integer" 28 | (are [x] (= (Duration/ofSeconds x) (time/duration-str->Duration x)) 29 | -1 0 1 2 3 4 5 6 7 8 9 10)) 30 | (testing "returns expected values for simple input" 31 | (are [x y] (= (Duration/ofSeconds x) (time/duration-str->Duration y)) 32 | 94608000 "3y" 33 | 259200 "3d" 34 | 10800 "3h" 35 | 180 "3m" 36 | 3 "3s")) 37 | (testing "Throws exceptions on invalid input" 38 | (is (thrown? IllegalArgumentException (time/duration-str->Duration "6y3m3y"))) 39 | (is (thrown? IllegalArgumentException (time/duration-str->Duration "3y6d7d8d3d"))) 40 | (is (thrown? IllegalArgumentException (time/duration-str->Duration "3y3y6d7d3h"))) 41 | (is (thrown? IllegalArgumentException (time/duration-str->Duration "3y6y5h6d3m"))) 42 | (is (thrown? IllegalArgumentException (time/duration-str->Duration "3y6y5h6d3m3s"))) 43 | (is (thrown? IllegalArgumentException (time/duration-str->Duration "not-a-duration")))))) 44 | 45 | (deftest days->hours-test 46 | (testing "expected input provides expected output" 47 | (are [x y] (= x (time/days->hours y)) 48 | 0 0 49 | -24 -1 50 | 24 1 51 | 48 2 52 | (* 24 1.1) 1.1))) 53 | 54 | (deftest hours->days-test 55 | (testing "expected input provides expected output" 56 | (are [x y] (= x (time/hours->days y)) 57 | 0 0 58 | -1 -24 59 | 1 24 60 | 2 48 61 | 1.1 (* 24 1.1)))) 62 | 63 | (deftest hours->min-test 64 | (testing "expected input provides expected output" 65 | (are [x y] (= x (time/hours->min y)) 66 | 0 0 67 | -60 -1 68 | 60 1 69 | 120 2 70 | (* 60 1.1) 1.1))) 71 | 72 | (deftest min->hours-test 73 | (testing "expected input provides expected output" 74 | (are [x y] (= x (time/min->hours y)) 75 | 0 0 76 | -1 -60 77 | 1 60 78 | 2 120 79 | 1.1 (* 60 1.1)))) 80 | 81 | (deftest min->sec-test 82 | (testing "expected input provides expected output" 83 | (are [x y] (= x (time/min->sec y)) 84 | 0 0 85 | -60 -1 86 | 60 1 87 | 120 2 88 | (* 60 1.1) 1.1))) 89 | 90 | (deftest sec->min-test 91 | (testing "expected input provides expected output" 92 | (are [x y] (= x (time/sec->min y)) 93 | 0 0 94 | -1 -60 95 | 1 60 96 | 2 120 97 | 1.1 (* 60 1.1)))) 98 | 99 | (deftest sec->ms-test 100 | (testing "expected input provides expected output" 101 | (are [x y] (= x (time/sec->ms y)) 102 | 0 0 103 | -1000 -1 104 | 1000 1 105 | 2000 2 106 | 1100.0 1.1))) 107 | 108 | (deftest ms->sec-test 109 | (testing "expected input provides expected output" 110 | (are [x y] (= x (time/ms->sec y)) 111 | 0 0 112 | -1 -1000 113 | 1 1000 114 | 2 2000 115 | 11/10 1100))) 116 | 117 | (deftest min->ms-test 118 | (testing "expected input provides expected output" 119 | (are [x y] (= x (time/min->ms y)) 120 | 0 0 121 | -60000 -1 122 | 60000 1 123 | 120000 2 124 | 66000.0 1.1))) 125 | 126 | (deftest ms->min-test 127 | (testing "expected input provides expected output" 128 | (are [x y] (= x (time/ms->min y)) 129 | 0 0 130 | -1 -60000 131 | 1 60000 132 | 2 120000 133 | 11/10 66000))) 134 | 135 | (deftest hours->ms-test 136 | (testing "expected input provides expected output" 137 | (are [x y] (= x (time/hours->ms y)) 138 | 0 0 139 | -3600000 -1 140 | 3600000 1 141 | 7200000 2 142 | 3960000.0 1.1))) 143 | 144 | (deftest ms->hours-test 145 | (testing "expected input provides expected output" 146 | (are [x y] (= x (time/ms->hours y)) 147 | 0 0 148 | -1 -3600000 149 | 1 3600000 150 | 2 7200000 151 | 11/10 3960000))) 152 | 153 | (deftest days->ms-test 154 | (testing "expected input provides expected output" 155 | (are [x y] (= x (time/days->ms y)) 156 | 0 0 157 | -86400000 -1 158 | 86400000 1 159 | 172800000 2 160 | (* 1.1 24 60 60 1000) 1.1))) 161 | 162 | (deftest ms->days-test 163 | (testing "expected input provides expected output" 164 | (are [x y] (= x (time/ms->days y)) 165 | 0 0 166 | -1 -86400000 167 | 1 86400000 168 | 2 172800000 169 | 1.1 (* 1.1 24 60 60 1000)))) 170 | --------------------------------------------------------------------------------