├── .circleci ├── config.yml └── deploy │ └── deploy_release.clj ├── .clj-kondo └── config.edn ├── .dir-locals.el ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.org ├── project.clj ├── src └── haystack │ ├── analyzer.clj │ ├── parser.cljc │ └── parser │ ├── aviso.cljc │ ├── clojure │ ├── repl.cljc │ ├── stacktrace.cljc │ ├── tagged_literal.cljc │ └── throwable.cljc │ ├── java.cljc │ └── util.cljc ├── test-resources └── haystack │ └── parser │ ├── boom.aviso.full.txt │ ├── boom.aviso.txt │ ├── boom.clojure.repl.txt │ ├── boom.clojure.stacktrace.txt │ ├── boom.clojure.tagged-literal.txt │ ├── boom.java.txt │ ├── divide-by-zero.aviso.txt │ ├── divide-by-zero.clojure.repl.txt │ ├── divide-by-zero.clojure.stacktrace.txt │ ├── divide-by-zero.clojure.tagged-literal.txt │ ├── divide-by-zero.java.txt │ ├── short.aviso.txt │ ├── short.clojure.repl.txt │ ├── short.clojure.stacktrace.txt │ ├── short.clojure.tagged-literal.println.txt │ ├── short.clojure.tagged-literal.txt │ └── short.java.txt └── test └── haystack ├── analyzer_test.clj ├── parser ├── aviso_test.cljc ├── clojure │ ├── repl_test.cljc │ ├── stacktrace_test.cljc │ ├── tagged_literal_test.cljc │ └── throwable_test.cljc ├── java_test.cljc ├── test.cljc ├── test │ └── fixtures.cljc └── util_test.cljc ├── parser_test.cljc └── test └── runner.cljs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | ###################################################################### 4 | # 5 | # Start of general purpose config. These can plausibly go into orbs 6 | # 7 | ###################################################################### 8 | 9 | # Default settings for executors 10 | 11 | defaults: &defaults 12 | working_directory: ~/repo 13 | 14 | # Runners for OpenJDK 8/11/16/17 15 | 16 | executors: 17 | openjdk8: 18 | docker: 19 | - image: circleci/clojure:openjdk-8-lein-2.9.1-node 20 | environment: 21 | LEIN_ROOT: "true" # we intended to run lein as root 22 | JVM_OPTS: -Xmx3200m # limit the maximum heap size to prevent out of memory errors 23 | <<: *defaults 24 | 25 | openjdk8_deploy: 26 | docker: 27 | - image: circleci/clojure:openjdk-8-lein-2.9.5 28 | environment: 29 | LEIN_ROOT: "true" # we intended to run lein as root 30 | JVM_OPTS: -Xmx3200m # limit the maximum heap size to prevent out of memory errors 31 | <<: *defaults 32 | 33 | openjdk11: 34 | docker: 35 | - image: circleci/clojure:openjdk-11-lein-2.9.3-buster-node 36 | environment: 37 | LEIN_ROOT: "true" # we intended to run lein as root 38 | JVM_OPTS: -Xmx3200m --illegal-access=deny # forbid reflective access (this flag doesn't exist for JDK8 or JDK17+) 39 | <<: *defaults 40 | 41 | openjdk17: 42 | docker: 43 | - image: circleci/clojure:openjdk-17-lein-2.9.5-buster-node 44 | environment: 45 | LEIN_ROOT: "true" # we intended to run lein as root 46 | JVM_OPTS: -Xmx3200m 47 | <<: *defaults 48 | 49 | # Runs a given set of steps, with some standard pre- and post- 50 | # steps, including restoring of cache, saving of cache. 51 | # 52 | # we also install `make` here. 53 | # 54 | # Adapted from https://github.com/lambdaisland/meta/blob/master/circleci/clojure_orb.yml 55 | 56 | commands: 57 | with_cache: 58 | description: | 59 | Run a set of steps with Maven dependencies and Clojure classpath cache 60 | files cached. 61 | This command restores ~/.m2 and .cpcache if they were previously cached, 62 | then runs the provided steps, and finally saves the cache. 63 | The cache-key is generated based on the contents of `deps.edn` present in 64 | the `working_directory`. 65 | parameters: 66 | steps: 67 | type: steps 68 | files: 69 | description: Files to consider when creating the cache key 70 | type: string 71 | default: "deps.edn project.clj build.boot" 72 | cache_version: 73 | type: string 74 | description: "Change this value to force a cache update" 75 | default: "1" 76 | steps: 77 | - run: 78 | name: Install make 79 | command: | 80 | sudo apt-get install make 81 | - run: 82 | name: Generate Cache Checksum 83 | command: | 84 | for file in << parameters.files >> 85 | do 86 | find . -name $file -exec cat {} + 87 | done | shasum | awk '{print $1}' > /tmp/clojure_cache_seed 88 | - restore_cache: 89 | key: clojure-<< parameters.cache_version >>-{{ checksum "/tmp/clojure_cache_seed" }} 90 | - steps: << parameters.steps >> 91 | - save_cache: 92 | paths: 93 | - ~/.m2 94 | - .cpcache 95 | key: clojure-<< parameters.cache_version >>-{{ checksum "/tmp/clojure_cache_seed" }} 96 | 97 | # The jobs are relatively simple. One runs utility commands against 98 | # latest stable JDK + Clojure, the other against specified versions 99 | 100 | jobs: 101 | 102 | util_job: 103 | description: | 104 | Running utility commands/checks (linter etc.) 105 | Always uses Java LTS latest and Clojure 1.11 106 | parameters: 107 | steps: 108 | type: steps 109 | executor: openjdk17 110 | environment: 111 | VERSION: "1.11" 112 | steps: 113 | - checkout 114 | - with_cache: 115 | cache_version: "1.11" 116 | steps: << parameters.steps >> 117 | 118 | deploy: 119 | executor: openjdk8_deploy 120 | steps: 121 | - checkout 122 | - restore_cache: 123 | keys: 124 | - v2-dependencies-{{ checksum "project.clj" }} 125 | # fallback to using the latest cache if no exact match is found 126 | - v2-dependencies- 127 | - run: lein with-profile -user,+test deps 128 | - save_cache: 129 | paths: 130 | - ~/.m2 131 | key: v2-dependencies-{{ checksum "project.clj" }} 132 | - run: 133 | name: Deploy 134 | command: | 135 | lein with-profile -user,+deploy run -m deploy-release make deploy 136 | 137 | test_code: 138 | description: | 139 | Run tests against given version of JDK and Clojure 140 | parameters: 141 | jdk_version: 142 | description: Version of JDK to test against 143 | type: string 144 | clojure_version: 145 | description: Version of Clojure to test against 146 | type: string 147 | executor: << parameters.jdk_version >> 148 | environment: 149 | VERSION: << parameters.clojure_version >> 150 | steps: 151 | - checkout 152 | - with_cache: 153 | cache_version: << parameters.clojure_version >>|<< parameters.jdk_version >> 154 | steps: 155 | - run: 156 | name: Running JVM tests 157 | command: make test 158 | - run: 159 | name: Running cljs tests 160 | command: make test-cljs 161 | 162 | ###################################################################### 163 | # 164 | # End general purpose configs 165 | # 166 | ###################################################################### 167 | 168 | 169 | # The ci-test-matrix does the following: 170 | # 171 | # - run tests against the target matrix 172 | # - All our defined JDKs 173 | # - Clojure 1.8, 1.9, 1.10, 1.11, master 174 | # - linter, eastwood and cljfmt 175 | # - runs code coverage report 176 | 177 | workflows: 178 | version: 2.1 179 | ci-test-matrix: 180 | jobs: 181 | - test_code: 182 | matrix: 183 | parameters: 184 | clojure_version: ["1.8", "1.9", "1.10", "1.11", "master"] 185 | jdk_version: [openjdk8, openjdk11, openjdk17] 186 | filters: 187 | branches: 188 | only: /.*/ 189 | tags: 190 | only: /^v\d+\.\d+\.\d+(-alpha\d+)?$/ 191 | - util_job: 192 | name: Code Linting 193 | filters: 194 | branches: 195 | only: /.*/ 196 | tags: 197 | only: /^v\d+\.\d+\.\d+(-alpha\d+)?$/ 198 | steps: 199 | - run: 200 | name: Running cljfmt 201 | command: | 202 | make cljfmt 203 | - run: 204 | name: Running clj-kondo 205 | command: | 206 | make kondo 207 | - run: 208 | name: Running Eastwood 209 | command: | 210 | make eastwood 211 | - deploy: 212 | requires: 213 | - test_code 214 | - "Code Linting" 215 | filters: 216 | branches: 217 | ignore: /.*/ 218 | tags: 219 | only: /^v\d+\.\d+\.\d+(-alpha\d+)?$/ 220 | -------------------------------------------------------------------------------- /.circleci/deploy/deploy_release.clj: -------------------------------------------------------------------------------- 1 | (ns deploy-release 2 | (:require 3 | [clojure.java.shell :refer [sh]] 4 | [clojure.string :as str])) 5 | 6 | (def release-marker "v") 7 | 8 | (defn make-version [tag] 9 | (str/replace-first tag release-marker "")) 10 | 11 | (defn log-result [m] 12 | (println m) 13 | m) 14 | 15 | (defn -main [& _] 16 | (let [tag (System/getenv "CIRCLE_TAG")] 17 | (if-not tag 18 | (do 19 | (println "No CIRCLE_TAG found.") 20 | (System/exit 1)) 21 | (if-not (re-find (re-pattern release-marker) tag) 22 | (do 23 | (println (format "The `%s` marker was not found in %s." release-marker tag)) 24 | (System/exit 1)) 25 | (do 26 | (apply println "Executing" *command-line-args*) 27 | (->> [:env (-> {} 28 | (into (System/getenv)) 29 | (assoc "PROJECT_VERSION" (make-version tag)) 30 | (dissoc "CLASSPATH"))] 31 | (into (vec *command-line-args*)) 32 | (apply sh) 33 | log-result 34 | :exit 35 | (System/exit))))))) 36 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {instaparse.core/defparser clojure.core/def} 2 | :linters {:discouraged-var {clojure.core/read-string {:message "Please prefer clojure.edn/read-string"}} 3 | :unresolved-var {:exclude [instaparse.core/transform]} 4 | ;; Enable some disabled-by-default linters. 5 | :docstring-leading-trailing-whitespace {:level :warning} 6 | :keyword-binding {:level :warning} 7 | :reduce-without-init {:level :warning} 8 | :redundant-fn-wrapper {:level :warning} 9 | :single-key-in {:level :warning} 10 | :unsorted-required-namespaces {:level :warning} 11 | :used-underscored-binding {:level :warning}}} 12 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables 2 | ;;; For more information see (info "(emacs) Directory Variables") 3 | 4 | ((clojure-mode 5 | (clojure-indent-style . :always-align) 6 | (indent-tabs-mode . nil) 7 | (fill-column . 80))) 8 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Do you have an issue to report or an idea to submit? That's great! 4 | We're eager to make Haystack better. Please 5 | report issues to the [issue tracker][1] of the repository or submit 6 | a pull request. 7 | 8 | To help us, please, follow these guidelines: 9 | 10 | ## Issue reporting 11 | 12 | * Check that the issue has not already been reported. 13 | * Check that the issue has not already been fixed in the latest code 14 | (a.k.a. `master`). 15 | * Be clear, concise and precise in your description of the problem. 16 | 17 | ## Pull requests 18 | 19 | * Read [how to properly contribute to open source projects on Github][2]. 20 | * Use a topic branch to easily amend a pull request later, if necessary. 21 | * Write [good commit messages][3]. 22 | * Mention related tickets in the commit messages (e.g. `[Fix #N] Add command ...`) 23 | * Update the [changelog][6]. 24 | * Use the same coding conventions as the rest of the project. 25 | * [Squash related commits together][5]. 26 | * Open a [pull request][4] that relates to *only* one subject with a clear title and description in grammatically correct, complete sentences. 27 | 28 | [1]: https://github.com/clojure-emacs/haystack/issues 29 | [2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request 30 | [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 31 | [4]: https://help.github.com/articles/using-pull-requests 32 | [5]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 33 | [6]: https://github.com/clojure-emacs/haystack/blob/master/CHANGELOG.md 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Use the template below when reporting bugs. Please, make sure that 2 | you're running the latest stable version and that the problem 3 | you're reporting hasn't been reported (and potentially fixed) already.* 4 | 5 | *When requesting new features or improvements to existing features you can 6 | discard the template completely. Just make sure to make a good case for your 7 | request.* 8 | 9 | **Remove all of the placeholder text in your final report!** 10 | 11 | ## Expected behavior 12 | 13 | ## Actual behavior 14 | 15 | ## Steps to reproduce the problem 16 | 17 | *This is extremely important! Providing us with a reliable way to reproduce 18 | a problem will expedite its solution.* 19 | 20 | ## Environment & Version information 21 | 22 | ### version information 23 | 24 | ### CIDER version information 25 | 26 | _Optional_ 27 | 28 | *Include here the version string displayed when 29 | CIDER's REPL is launched. Here's an example:* 30 | 31 | ``` 32 | ;; CIDER 0.12.0snapshot (package: 20160331.421), nREPL 0.2.12 33 | ;; Clojure 1.8.0, Java 1.8.0_31 34 | ``` 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before submitting a PR make sure the following things have been done (and denote this 2 | by checking the relevant checkboxes): 3 | 4 | - [ ] The commits are consistent with our [contribution guidelines](CONTRIBUTING.md) 5 | - [ ] You've added tests (if possible) to cover your change(s) 6 | - [ ] All tests are passing (run `lein do clean, test`) 7 | - [ ] Code inlining with mranderson works and tests pass with inlined code (run `./build.sh install` -- takes a long time) 8 | - [ ] You've updated the changelog (if adding/changing user-visible functionality) 9 | - [ ] You've updated the readme (if adding/changing user-visible functionality) 10 | 11 | Thanks! 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | .nrepl-port 14 | *~ 15 | \#*\# 16 | .\#* 17 | install.cmd 18 | /install.sh 19 | /deploy.sh 20 | /.cljs_nashorn_repl/ 21 | /.cljs_rhino_repl/ 22 | /nashorn_code_cache/ 23 | /.cljs_rhino_repl/ 24 | /.cljs_node_repl/ 25 | /out/ 26 | /.lein-env 27 | .inline-deps 28 | .clj-kondo/.cache/ 29 | /.lsp/ 30 | /.eastwood 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## master (unreleased) 4 | 5 | ## 0.3.3 (2023-11-11) 6 | 7 | * Bind Orchard's `java/*analyze-sources* false` in order to fix a performance regression. 8 | 9 | ## 0.3.1 (2023-09-29) 10 | 11 | * `analyzer`: include a `:compile-like` key which indicates if the error happened at a "compile-like" phase. 12 | * It represents exceptions that happen at runtime (and therefore never include a `:phase`) which however, represent code that cannot possibly work, and therefore are a "compile-like" exception (i.e. a linter could have caught them). 13 | * The set of conditions which are considered a 'compile-like' exception is private and subject to change. 14 | * [#13](https://github.com/clojure-emacs/haystack/issues/13): Expand the set of frames considered as `:tooling`. 15 | * Use Orchard [0.15.1](https://github.com/clojure-emacs/orchard/blob/v0.15.1/CHANGELOG.md#0151-2023-09-21). 16 | 17 | ## 0.2.0 (2023-08-20) 18 | 19 | ## Changes 20 | 21 | * `analyzer`: include a `:phase` key for the causes that include a `:clojure.error/phase`. 22 | * Categorize more frames as `:tooling` 23 | * `:tooling` now intends to more broadly hide things that are commonly Clojure-internal / irrelevant to the application programmer. 24 | * New exhaustive list: 25 | * `cider.*` 26 | * `clojure.core/apply` 27 | * `clojure.core/binding-conveyor-fn` 28 | * `clojure.core/eval` 29 | * `clojure.core/with-bindings` 30 | * `clojure.lang.Compiler` 31 | * `clojure.lang.RT` 32 | * `clojure.main/repl` 33 | * `nrepl.*` 34 | * `java.lang.Thread/run` (if it's the root element of the stacktrace) 35 | 36 | ## 0.1.0 (2023-08-18) 37 | 38 | ### Bugs fixed 39 | 40 | * [#9](https://github.com/clojure-emacs/haystack/issues/9): handle unloadable classes. 41 | 42 | ## 0.0.1 (2022-11-25) 43 | 44 | **Note:** First "official" release. 45 | 46 | ### New features 47 | 48 | * Extract stacktrace code from `cider-nrepl`. 49 | * Add stacktrace analyzer and parsers. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test deploy clean 2 | 3 | VERSION ?= 1.11 4 | 5 | clean: 6 | lein clean 7 | 8 | test: clean 9 | lein with-profile -user,-dev,+$(VERSION) test 10 | 11 | test-cljs: clean 12 | lein with-profile -user,-dev,+cljsbuild cljsbuild once 13 | node target/cljs/test.js 14 | 15 | cljfmt: 16 | lein with-profile -user,+$(VERSION),+cljfmt cljfmt check 17 | 18 | cljfmt-fix: 19 | lein with-profile -user,+$(VERSION),+cljfmt cljfmt fix 20 | 21 | eastwood: 22 | lein with-profile -user,+$(VERSION),+deploy,+eastwood eastwood 23 | 24 | kondo: 25 | lein with-profile -dev,+$(VERSION),+clj-kondo run -m clj-kondo.main --lint src test .circleci/deploy 26 | 27 | 28 | # Deployment is performed via CI by creating a git tag prefixed with "v". 29 | # Please do not deploy locally as it skips various measures. 30 | deploy: check-env 31 | lein with-profile -user,-dev,+$(VERSION) deploy clojars 32 | 33 | # Usage: PROJECT_VERSION=0.3.3 make install 34 | # PROJECT_VERSION is needed because it's not computed dynamically. 35 | install: check-install-env 36 | lein with-profile -user,-dev,+$(VERSION) install 37 | 38 | check-env: 39 | ifndef CLOJARS_USERNAME 40 | $(error CLOJARS_USERNAME is undefined) 41 | endif 42 | ifndef CLOJARS_PASSWORD 43 | $(error CLOJARS_PASSWORD is undefined) 44 | endif 45 | ifndef CIRCLE_TAG 46 | $(error CIRCLE_TAG is undefined) 47 | endif 48 | 49 | check-install-env: 50 | ifndef PROJECT_VERSION 51 | $(error Please set PROJECT_VERSION as an env var beforehand.) 52 | endif 53 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | [[https://circleci.com/gh/clojure-emacs/haystack/tree/master][https://circleci.com/gh/clojure-emacs/haystack/tree/master.svg?style=svg]] 2 | [[https://clojars.org/mx.cider/haystack][https://img.shields.io/clojars/v/mx.cider/haystack.svg]] 3 | [[https://versions.deps.co/clojure-emacs/haystack][https://versions.deps.co/clojure-emacs/haystack/status.svg]] 4 | [[https://codecov.io/gh/clojure-emacs/haystack/][https://codecov.io/gh/clojure-emacs/haystack/branch/master/graph/badge.svg]] 5 | [[https://cljdoc.org/d/mx.cider/haystack/CURRENT][https://cljdoc.org/badge/mx.cider/haystack]] 6 | [[https://clojars.org/mx.cider/haystack][https://versions.deps.co/mx.cider/haystack/downloads.svg]] 7 | 8 | * Haystack 9 | ** Introduction 10 | 11 | Stacktraces are a hot topic in the Clojure community. As a Clojurist 12 | you deal with them in different situations. Sometimes you catch them 13 | "live", like an exception just thrown in a REPL. Other times you find 14 | them as text, printed in a REPL, or in a log file. Or worst, a printed 15 | exception buried inside another string, almost impossible to read. And 16 | of course, there are different kinds of formats. 17 | 18 | Haystack is a library that can parse and analyze Clojure 19 | stacktraces. The parser transforms printed stacktraces back into data 20 | and the analyzer enriches stacktrace data with run-time information 21 | from the class path. 22 | 23 | Haystack was previously used in [[https://docs.cider.mx][CIDER]] for 24 | stacktrace analysis. It is not included in CIDER anymore but can still 25 | be used as an individual library. 26 | 27 | ** Parser 28 | 29 | The Haystack stacktrace parser transforms a string that contains a 30 | stacktrace printed in one of the supported formats back into a Clojure 31 | data structure. Given an input, the parser applies some 32 | transformations to it (unwrapping an EDN string for example) and 33 | passes the result to the parser functions registered in the 34 | =haystack.parser/default-parsers= var. Each of the registered parsers 35 | is tried in order and the first parser that succeeds wins. 36 | 37 | On success the parser returns a Clojure map with a similar structure 38 | as Clojure's =Throwable->map= function. 39 | 40 | On failure the parser returns a map with an =:error= key, and possibly 41 | other keys describing the error. 42 | 43 | A successful parse result can be given to the Haystack analyzer to 44 | enrich it with more information. 45 | 46 | *** Stacktrace data format 47 | 48 | An Haystack stacktrace parser transforms input into a parse result. On 49 | success, the parse result is a enhanced version of the Clojure data 50 | representation of a Throwable, a map with the following keys: 51 | 52 | - =:cause= The root cause message as a string. 53 | - =:phase= The error phase (optional). 54 | - =:via= The cause chain, with each cause having the keys: 55 | - =:at= The top stack element of the cause as a vector (optional). 56 | - =:data= The =ex-data= of the cause as a map (optional). 57 | - =:message= The exception message of the cause as a string. 58 | - =:type= The exception of the cause as a symbol. 59 | - =:trace= The stack elements (optional, extended by Haystack). 60 | - =:trace= The root cause stack elements 61 | 62 | This is mostly the same format as used by =Throwable->map= in newer 63 | Clojure versions, except for the additional =:trace= key in the cause 64 | maps of =:via=. We added this additional key to keep the trace of the 65 | causes. 66 | 67 | *** Supported formats 68 | 69 | Stacktraces are printed in different formats by tools and 70 | libraries. Haystack supports the following formats: 71 | 72 | - =:aviso= Stacktraces printed with the [[https://ioavisopretty.readthedocs.io/en/latest/exceptions.html][write-exception]] function of 73 | the [[https://github.com/AvisoNovate/pretty][Aviso]] library. 74 | 75 | - =:clojure.tagged-literal= Stacktraces printed as a [[https://clojure.org/reference/reader#tagged_literals][tagged literal]], 76 | like a [[https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html][java.lang.Throwable]] printed with the [[https://clojure.github.io/clojure/branch-master/clojure.core-api.html#clojure.core/pr][pr]] function. 77 | 78 | - =:clojure.stacktrace= Stacktraces printed with the [[https://clojure.github.io/clojure/branch-master/clojure.stacktrace-api.html#clojure.stacktrace/print-cause-trace][print-cause-trace]] 79 | function of the [[https://clojure.github.io/clojure/branch-master/clojure.stacktrace-api.html][clojure.stacktrace]] namespace. 80 | 81 | - =:clojure.repl= Stacktraces printed with the [[https://clojure.github.io/clojure/branch-master/clojure.repl-api.html#clojure.repl/pst][pst]] function of the 82 | [[https://clojure.github.io/clojure/branch-master/clojure.repl-api.html][clojure.repl]] namespace. 83 | 84 | - =:java= Stacktraces printed with the [[https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html#printStackTrace--][printStackTrace]] method of the 85 | [[https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html][java.lang.Throwable]] class. 86 | 87 | *** Usage 88 | 89 | Let's say you want to parse the following stacktrace string and turn 90 | it back into a data structure for further processing. 91 | 92 | #+begin_src clojure :exports code :results silent 93 | (def my-stacktrace-str 94 | (str "clojure.lang.ExceptionInfo: BOOM-1 {:boom \"1\"}\n" 95 | " at java.base/java.lang.Thread.run(Thread.java:829)")) 96 | #+end_src 97 | 98 | The easiest way to do this is to pass the string to the 99 | =haystack.parser/parse= function. It will try all registered 100 | parsers and returns the first successful parse result. 101 | 102 | #+begin_src clojure :exports code :results silent 103 | (require '[haystack.parser :as stacktrace.parser]) 104 | 105 | (def my-stacktrace-data 106 | (stacktrace.parser/parse my-stacktrace-str)) 107 | #+end_src 108 | 109 | On success the parser will return a Clojure map in the 110 | =Throwable->map= format. For the input used above, this data structure 111 | looks like this: 112 | 113 | #+begin_src clojure :exports both :results output :wrap src clojure 114 | (clojure.pprint/pprint my-stacktrace-data) 115 | #+end_src 116 | 117 | #+RESULTS: 118 | #+begin_src clojure 119 | {:cause "BOOM-1", 120 | :data {:boom "1"}, 121 | :trace [[java.base/java.lang.Thread run "Thread.java" 829]], 122 | :via 123 | [{:at [java.base/java.lang.Thread run "Thread.java" 829], 124 | :message "BOOM-1", 125 | :type clojure.lang.ExceptionInfo, 126 | :trace [[java.base/java.lang.Thread run "Thread.java" 829]], 127 | :data {:boom "1"}}], 128 | :stacktrace-type :java} 129 | #+end_src 130 | 131 | Tip: If you know in advance with what kind of stacktrace you are 132 | dealing with, pass it directly to the parser for the given format. 133 | 134 | ** Analyzer 135 | 136 | The Haystack stacktrace analyzer transforms a stacktrace into an 137 | analysis. An analysis is a sequence of Clojure maps, one for each of 138 | the causes of the stacktrace, with the following keys: 139 | 140 | - =:class= The exception class as a string. 141 | - =:message= The exception message as a string. 142 | - =:stacktrace= The stacktrace frames, a list of maps. 143 | - =:data= The exception data. 144 | - =:location= The location formation of the exception. 145 | 146 | A frame in the =:stacktrace= is a map with the following keys: 147 | 148 | - =:class= The class name of the frame invocation. 149 | - =:file-url= The URL of the frame source file. 150 | - =:file= The file name of the frame source. 151 | - =:flags= The flags of the frame. 152 | - =:line= The line number of the frame source. 153 | - =:method= The method or function name of the frame invocation. 154 | - =:name= The name of the frame, typically the class and method of the invocation. 155 | - =:type= The type of invocation (=:java=, =:tooling=, etc). 156 | 157 | The analyzer accepts either an instance of =java.lang.Throwable= or a 158 | Clojure map in the =Throwable->map= format as input. 159 | 160 | *** Usage 161 | 162 | We can analyze our previously parsed stacktrace by calling the 163 | =haystack.analyzer/analyze= function on it. 164 | 165 | #+begin_src clojure :exports both :results pp :wrap src clojure 166 | (require '[haystack.analyzer :as stacktrace.analyzer]) 167 | (stacktrace.analyzer/analyze my-stacktrace-data) 168 | #+end_src 169 | 170 | #+RESULTS: 171 | #+begin_src clojure 172 | [{:class "clojure.lang.ExceptionInfo", 173 | :message "BOOM-1", 174 | :stacktrace 175 | ({:name "java.lang.Thread/run", 176 | :file "Thread.java", 177 | :line 829, 178 | :class "java.lang.Thread", 179 | :method "run", 180 | :type :java, 181 | :flags #{:java}, 182 | :file-url 183 | "jar:file:/usr/lib/jvm/openjdk-11/lib/src.zip!/java.base/java/lang/Thread.java"}), 184 | :data "{:boom \"1\"}", 185 | :location {}}] 186 | #+end_src 187 | 188 | We get back a sequence of maps, one for each cause, which contain 189 | additional information about each frame discovered from the class path. 190 | 191 | ** Development 192 | 193 | *** Creating a parser 194 | 195 | To add support for another stacktrace format, please create a new 196 | parser under the =haystack.parser.= namespace and add it 197 | to the =haystack.parser/default-parsers= var. The parser should be a 198 | function that accepts a single argument, the input (typically a 199 | string), and returns a map. The parser function should follow the 200 | following rules: 201 | 202 | - On success, the parser should return the stacktrace as a map. The 203 | map should be in the =Throwable->map= format described above with a 204 | =:stacktrace-type= key that contains the type of stacktrace as a 205 | keyword. 206 | 207 | - On error, the parser should return a map with an =:error= key and 208 | possibly others describing why the input could not be parsed. We use 209 | =:incorrect= if the input does not match the grammar, and 210 | =:unsupported= if the input type is not supported by the parser. 211 | 212 | - Ideally, the parser should be tolerant to any garbage before and 213 | after the stacktrace to be parsed. This is to not put the burden of 214 | exactly figuring out where a stacktrace starts and ends onto 215 | clients. 216 | 217 | - When skipping garbage at the beginning of a stacktrace do it 218 | efficiently. For example, instead of skipping garbage character by 219 | character and trying your parser with the rest of the string, use 220 | the =haystack.parser.util/seek-to-regex= function to 221 | directly skip to the beginning of the stacktrace, if possible. 222 | 223 | - Most of the parsers in Haystack are implemented with [[https://github.com/Engelberg/instaparse][Instaparse]] and 224 | have a [[https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form][BNF]] grammar describing the format of the stacktrace. Try to 225 | come up with an Instagram grammar for the new stacktrace format as 226 | well, unless you have a better, simpler or more efficient way of 227 | parsing it (like the Clojure tagged literal parser for example). 228 | 229 | *** Instaparse Tips & Tricks 230 | 231 | Writing a grammar for a stacktrace format might be challenging at 232 | times, especially when garbage in the input is involved, which might 233 | introduce ambiguities in your grammar. Here are some tips and trick 234 | for writing Instaparse grammars: 235 | 236 | - Read the [[https://github.com/Engelberg/instaparse][documentation]], it is good and has many examples. 237 | 238 | - Start with the most simple parser, try to pass the exception class 239 | or name before building up. 240 | 241 | - Use the =:start= parameter of the Instaparse parser, to [[https://github.com/Engelberg/instaparse#parsing-from-another-start-rule][parse input 242 | from another start rule]]. This is useful if your grammar got complex, 243 | but you want to try parsing of an individual rule. 244 | 245 | - Be aware of [[https://github.com/Engelberg/instaparse#regular-expressions-a-word-of-warning][greedy regex behavior]]. 246 | 247 | - When testing input try it against the raw Instaparse parser first, 248 | and only apply the Instaparse [[https://github.com/Engelberg/instaparse#transforming-the-tree][transformations]] when the parser works. 249 | 250 | - If your parser fails on an input, [[https://github.com/Engelberg/instaparse#revealing-hidden-information][reveal hidden information]] to get a 251 | better understanding of what happened. 252 | 253 | *** Deployment 254 | 255 | Here's how to deploy to Clojars: 256 | 257 | #+begin_src sh 258 | git tag -a v0.3.3 -m "0.3.3" 259 | git push --tags 260 | #+end_src 261 | 262 | ** Changelog 263 | 264 | [[CHANGELOG.md][CHANGELOG.md]] 265 | 266 | ** Thanks 267 | 268 | The Haystack stacktrace analyzer was written by Jeff Valk ([[https://github.com/jeffvalk][@jeffvalk]]) 269 | and was originally part of the [[https://github.com/clojure-emacs/cider-nrepl][cider-nrepl]] project. 270 | 271 | ** License 272 | 273 | Copyright © 2022-23 Cider Contributors 274 | 275 | Distributed under the Eclipse Public License, the same as Clojure. 276 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | ;; PROJECT_VERSION is set by .circleci/deploy/deploy_release.clj, 2 | ;; whenever we perform a deployment. 3 | (defproject mx.cider/haystack (or (not-empty (System/getenv "PROJECT_VERSION")) 4 | "0.0.0") 5 | :description "Let's make the most of Clojure's infamous stacktraces!" 6 | :url "https://github.com/clojure-emacs/haystack" 7 | :license {:name "Eclipse Public License" 8 | :url "https://www.eclipse.org/legal/epl-v10.html"} 9 | :dependencies [[cider/orchard "0.20.0"] 10 | [instaparse "1.4.12" :exclusions [org.clojure/clojure]]] 11 | :pedantic? ~(if (System/getenv "CI") 12 | :abort 13 | ;; :pedantic? can be problematic for certain local dev workflows: 14 | false) 15 | :deploy-repositories [["clojars" {:url "https://clojars.org/repo" 16 | :username :env/clojars_username 17 | :password :env/clojars_password 18 | :sign-releases false}]] 19 | 20 | :profiles {:provided {:dependencies [[org.clojure/clojure "1.11.1"] 21 | [org.clojure/clojurescript "1.11.4"]]} 22 | 23 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 24 | 25 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} 26 | 27 | :1.10 {:dependencies [[org.clojure/clojure "1.10.3"]]} 28 | 29 | :1.11 {:dependencies [[org.clojure/clojure "1.11.1"]]} 30 | 31 | :master {:repositories [["snapshots" 32 | "https://oss.sonatype.org/content/repositories/snapshots"]] 33 | :dependencies [[org.clojure/clojure "1.12.0-master-SNAPSHOT"] 34 | [org.clojure/clojure "1.12.0-master-SNAPSHOT" :classifier "sources"]]} 35 | :cljsbuild {:plugins [[lein-cljsbuild "1.1.8"]] 36 | :dependencies [[lein-doo "0.1.11"]] 37 | :cljsbuild {:builds 38 | [{:id "test" 39 | :compiler 40 | {:main haystack.test.runner 41 | :output-dir "target/cljs/test" 42 | :output-to "target/cljs/test.js" 43 | :target :nodejs} 44 | :source-paths ["src" "test"]}]}} 45 | :cljfmt [:test 46 | {:plugins [[lein-cljfmt "0.9.0" :exclusions [org.clojure/clojure 47 | org.clojure/clojurescript]]]}] 48 | :eastwood {:plugins [[jonase/eastwood "1.3.0"]] 49 | :eastwood {;; :implicit-dependencies would fail spuriously when the CI matrix runs for Clojure < 1.10, 50 | ;; because :implicit-dependencies can only work for a certain corner case starting from 1.10. 51 | :exclude-linters [:implicit-dependencies] 52 | :exclude-namespaces [refactor-nrepl.plugin] 53 | :add-linters [:boxed-math :performance]}} 54 | :clj-kondo [:test 55 | {:dependencies [[clj-kondo "2022.10.14" 56 | :exclusions [com.cognitect/transit-clj 57 | org.clojure/tools.reader]]]}] 58 | 59 | :deploy {:source-paths [".circleci/deploy"]} 60 | :repl {:resource-paths ["test-resources"]} 61 | :test {:resource-paths ["test-resources"]}}) 62 | -------------------------------------------------------------------------------- /src/haystack/analyzer.clj: -------------------------------------------------------------------------------- 1 | (ns haystack.analyzer 2 | "Cause and stacktrace analysis for exceptions" 3 | {:added "0.1.0" 4 | :author "Jeff Valk"} 5 | (:require 6 | [clojure.pprint :as pp] 7 | [clojure.repl :as repl] 8 | [clojure.set :as set] 9 | [clojure.string :as str] 10 | [haystack.parser.clojure.throwable :as throwable] 11 | [orchard.info :as info] 12 | [orchard.java :as java] 13 | [orchard.java.resource :as resource] 14 | [orchard.namespace :as namespace]) 15 | (:import 16 | (java.io StringWriter))) 17 | 18 | ;;; ## Stacktraces 19 | 20 | (defn pprint 21 | "A simple wrapper around `clojure.pprint/write`." 22 | {:added "0.1.0"} 23 | ([value writer] 24 | (pprint value writer {})) 25 | ([value writer options] 26 | (apply pp/write value (mapcat identity (assoc options :stream writer))))) 27 | 28 | ;; Java stacktraces don't expose column number. 29 | (defn- stack-frame 30 | "Return a map describing the stack frame." 31 | [frame] 32 | (let [[class method file line] frame] 33 | (when (and class method file line) 34 | {:name (str (name class) "/" (name method)) 35 | :file file 36 | :line line 37 | :class (name class) 38 | :method (name method)}))) 39 | 40 | (defn- flag-frame 41 | "Update frame's flags vector to include the new flag." 42 | [frame flag] 43 | (update frame :flags (comp set conj) flag)) 44 | 45 | (defn- source-path 46 | "Return the relative source path for the class without extension." 47 | [class] 48 | (-> (str/replace (str class) #"\$.*" "") 49 | (str/replace "." "/"))) 50 | 51 | (defn- path->url 52 | "Return a url for the path, either relative to classpath, or absolute." 53 | [path] 54 | (or (info/file-path path) (second (resource/resource-path-tuple path)))) 55 | 56 | (defn- frame->url 57 | "Return a `java.net.URL` to the file referenced in the frame, if possible. 58 | Useful for handling clojure vars which may not exist. Uncomprehensive list of 59 | reasons for this: 60 | * Failed refresh 61 | * Top-level evaluation" 62 | [frame] 63 | (some-> (:name frame) 64 | source-path 65 | (str "." (last (.split ^String (:file frame) 66 | "\\."))) 67 | path->url 68 | str)) 69 | 70 | (defn- analyze-fn 71 | "Add namespace, fn, and var to the frame map when the source is a Clojure 72 | function." 73 | [{:keys [type class method] :as frame}] 74 | (if (or (= :clj type) 75 | (= :cljc type)) 76 | (let [[ns fn & anons] (-> (repl/demunge class) 77 | (str/replace #"--\d+" "") 78 | (str/split #"/")) 79 | fn (or fn method)] ; protocol functions are not munged 80 | (binding [java/*analyze-sources* false] 81 | (assoc frame 82 | :ns ns 83 | :fn (str/join "/" (cons fn anons)) 84 | :var (str ns "/" fn) 85 | ;; Full file path 86 | :file-url (or (some-> (info/info* {:ns 'user :sym (symbol ns fn)}) 87 | :file 88 | path->url 89 | str) 90 | (str (frame->url frame)))))) 91 | (assoc frame :file-url (try 92 | (binding [java/*analyze-sources* false] 93 | (some->> frame 94 | :name 95 | symbol 96 | (java/resolve-symbol 'user) 97 | :file 98 | path->url 99 | str)) 100 | (catch Throwable _ 101 | ;; `java/resolve-symbol` can throw exceptions when the underlying class cannot be loaded. 102 | ;; See https://github.com/clojure-emacs/haystack/issues/9 103 | nil))))) 104 | 105 | (defn- analyze-file 106 | "Associate the file type (extension) of the source file to the frame map, and 107 | add it as a flag. If the name is `NO_SOURCE_FILE`, type `clj` is assumed." 108 | [{:keys [file] :as frame}] 109 | (let [type (keyword 110 | (cond (nil? file) "unknown" 111 | (= file "NO_SOURCE_FILE") "clj" 112 | (neg? (.indexOf ^String file ".")) "unknown" 113 | :else (last (.split ^String file "\\."))))] 114 | (-> frame 115 | (assoc :type type) 116 | (flag-frame type)))) 117 | 118 | (defn- flag-repl 119 | "Flag the frame if its source is a REPL eval." 120 | [{:keys [file] :as frame}] 121 | (if (and file 122 | (or (= file "NO_SOURCE_FILE") 123 | (.startsWith ^String file "form-init"))) 124 | (flag-frame frame :repl) 125 | frame)) 126 | 127 | (def ^:private tooling-frame-re 128 | #"^clojure\.lang\.LazySeq|^clojure\.lang\.Var|^clojure\.lang\.MultiFn|^clojure\.lang\.AFn|^clojure\.lang\.RestFn|^clojure\.lang\.RT|clojure\.lang\.Compiler|^nrepl\.|^cider\.|^refactor-nrepl\.|^shadow.cljs\.|^clojure\.core/eval|^clojure\.core/apply|^clojure\.core/with-bindings|^clojure\.core\.protocols|^clojure\.core\.map/fn|^clojure\.core/binding-conveyor-fn|^clojure\.main/repl") 129 | 130 | (defn- tooling-frame-name? [frame-name last?] 131 | (let [demunged (repl/demunge frame-name)] 132 | (boolean (or (re-find tooling-frame-re demunged) 133 | (and last? 134 | ;; Everything runs from a Thread, so this frame, if at root, is irrelevant. 135 | ;; However one can invoke this method 'by hand', which is why we also observe `last?`. 136 | (re-find #"^java\.lang\.Thread/run|^java\.util\.concurrent" demunged)))))) 137 | 138 | (defn- flag-tooling 139 | "Given a collection of stack `frames`, marks the 'tooling' ones as such. 140 | 141 | A 'tooling' frame is one that generally represents Clojure, JVM, nREPL or CIDER internals, 142 | and that is therefore not relevant to application-level code." 143 | [frames] 144 | (let [results (volatile! {})] 145 | (->> frames 146 | reverse 147 | (into [] 148 | (map-indexed (fn [^long i {frame-name :name :as frame}] 149 | (let [;; A frame is considered the last if it's literally the last one, 150 | ;; or if the previous element was marked as tooling. 151 | last? (or (zero? i) 152 | (some-> @results (get (dec i)))) 153 | tooling? (some-> frame-name (tooling-frame-name? last?))] 154 | (vswap! results assoc i tooling?) 155 | (cond-> frame 156 | tooling? 157 | (flag-frame :tooling)))))) 158 | reverse 159 | vec))) 160 | 161 | (defn directory-namespaces 162 | "Looks for all namespaces inside of directories on the class 163 | path, ignoring jars. 164 | 165 | It's a defn because this set is always subject to change. 166 | 167 | NOTE: depending on the use case, you might want to filter out 168 | namespaces such as `user` which while belong to the project, 169 | don't share a common naming scheme with the other namespaces." 170 | {:added "0.1.0"} 171 | [] 172 | (into #{} (namespace/project-namespaces))) 173 | 174 | (defn- ns-common-prefix* [namespaces] 175 | (let [common 176 | (try 177 | (->> namespaces 178 | (pmap (fn [ns-sym] 179 | (let [segments (-> ns-sym 180 | str 181 | (str/split #"\."))] 182 | ;; remove single-segment namespaces 183 | ;; (such as `dev`, `test`, `test-runner`) 184 | ;; that would break the commonality: 185 | (when (-> segments count (> 1)) 186 | segments)))) 187 | (filter identity) 188 | (reduce (fn [prev curr] 189 | (if (= ::placeholder prev) 190 | curr 191 | (let [found-commonality 192 | (reduce-kv (fn [result k v] 193 | (if (= (get prev k) 194 | (get curr k)) 195 | (conj result v) 196 | (reduced result))) 197 | [] 198 | prev)] 199 | (if (seq found-commonality) 200 | found-commonality 201 | (reduced :missing))))) 202 | ::placeholder)) 203 | (catch Throwable _e :error))] 204 | (condp = common 205 | ::placeholder 206 | {:valid false :common :missing} 207 | 208 | :missing 209 | {:valid false :common :missing} 210 | 211 | :error 212 | {:valid false :common :error} 213 | 214 | {:valid true :common (str/join "." common)}))) 215 | 216 | (def ^{:added "0.1.0"} ns-common-prefix 217 | "In order to match more namespaces, we look for a common namespace 218 | prefix across the ones we have identified." 219 | (delay (ns-common-prefix* (directory-namespaces)))) 220 | 221 | (defn- flag-project 222 | "Flag the frame if it is from the users project. From a users 223 | project means that the namespace is one we have identified or it 224 | begins with the identified common prefix." 225 | [namespaces {:keys [ns] :as frame}] 226 | (if (and ns 227 | (or (contains? namespaces (symbol ns)) 228 | (when (:valid @ns-common-prefix) 229 | (.startsWith ^String ns (:common @ns-common-prefix))))) 230 | (flag-frame frame :project) 231 | frame)) 232 | 233 | (defn- flag-duplicates 234 | "Where a parent and child frame represent substantially the same source 235 | location, flag the parent as a duplicate." 236 | [frames] 237 | (into [(first frames)] 238 | (map (fn [[frame child]] 239 | (if (or (= (:name frame) (:name child)) 240 | (and (= (:file frame) (:file child)) 241 | (= (:line frame) (:line child)))) 242 | (flag-frame frame :dup) 243 | frame))) 244 | (mapv vector (rest frames) frames))) 245 | 246 | (defn analyze-frame 247 | "Return the stacktrace as a sequence of maps, each describing a stack frame." 248 | {:added "0.1.0"} 249 | [namespaces frame] 250 | (let [f (comp flag-repl (partial flag-project namespaces) analyze-fn analyze-file stack-frame)] 251 | (f frame))) 252 | 253 | ;;; ## Causes 254 | 255 | (defn- relative-path 256 | "If the path is under the project root, return the relative path; otherwise 257 | return the original path." 258 | [path] 259 | (let [dir (str (System/getProperty "user.dir") 260 | (System/getProperty "file.separator"))] 261 | (str/replace-first path dir ""))) 262 | 263 | (defn- extract-location 264 | "If the cause is a compiler exception, extract the useful location information 265 | from its message or from `:location` if provided. 266 | Include relative path for simpler reporting." 267 | [{:keys [class message location] :as cause}] 268 | (if (= class "clojure.lang.Compiler$CompilerException") 269 | (if (seq location) 270 | (assoc cause 271 | :file (:clojure.error/source location) 272 | :file-url (some-> (:clojure.error/source location) 273 | path->url 274 | str) 275 | :path (relative-path (:clojure.error/source location)) 276 | :line (:clojure.error/line location) 277 | :column (:clojure.error/column location)) 278 | (let [[_ msg file line column] 279 | (re-find #"(.*?), compiling:\((.*):(\d+):(\d+)\)" message)] 280 | (assoc cause 281 | :message msg 282 | :file file 283 | :file-url (some-> file 284 | path->url 285 | str) 286 | :path (relative-path file) 287 | :line (Integer/parseInt line) 288 | :column (Integer/parseInt column)))) 289 | cause)) 290 | 291 | ;; CLJS REPLs use :repl-env to store huge amounts of analyzer/compiler state 292 | (def ^:private ex-data-blacklist 293 | #{:repl-env}) 294 | 295 | (defn- filtered-ex-data 296 | "Filter keys from the exception `data` which are 297 | blacklisted (generally for containing data not intended for reading 298 | by a human)." 299 | [data] 300 | (when data 301 | (into {} (filter (comp (complement ex-data-blacklist) key) data)))) 302 | 303 | (def ^{:added "0.1.0"} spec-abbrev 304 | (delay (if (try (require 'clojure.spec) true 305 | (catch Throwable _ false)) 306 | (resolve 'clojure.spec/abbrev) 307 | (if (try (require 'clojure.spec.alpha) true 308 | (catch Throwable _ false)) 309 | (resolve 'clojure.spec.alpha/abbrev) 310 | #'identity)))) 311 | 312 | (defn- prepare-spec-data 313 | "Prepare spec problems for display in user stacktraces. 314 | Take in a map `ed` as returned by `clojure.spec/explain-data` and return a map 315 | of pretty printed problems. The content of the returned map is modeled after 316 | `clojure.spec/explain-printer`." 317 | [ed pprint-str] 318 | (let [problems (sort-by #(count (:path %)) 319 | (or (:clojure.spec/problems ed) 320 | (:clojure.spec.alpha/problems ed)))] 321 | {:spec (pr-str (or (:clojure.spec/spec ed) 322 | (:clojure.spec.alpha/spec ed))) 323 | :value (pprint-str (or (:clojure.spec/value ed) 324 | (:clojure.spec.alpha/value ed))) 325 | :problems (for [{:keys [in val 326 | pred reason 327 | via path] 328 | :as prob} problems] 329 | (->> {:in (when (seq in) (pr-str in)) 330 | :val (pprint-str val) 331 | :predicate (pr-str (@spec-abbrev pred)) 332 | :reason reason 333 | :spec (when (seq via) (pr-str (last via))) 334 | :at (when (seq path) (pr-str path)) 335 | :extra (let [extras (->> #{:in :val :pred :reason :via :path 336 | :clojure.spec/failure 337 | :clojure.spec.alpha/failure} 338 | (set/difference (set (keys prob))) 339 | (select-keys prob))] 340 | (when (seq extras) 341 | (pprint-str extras)))} 342 | (filter clojure.core/val) 343 | (into {})))})) 344 | 345 | (defn- analyze-stacktrace-data 346 | "Return the stacktrace as a sequence of maps, each describing a stack frame." 347 | [trace] 348 | (when (seq trace) 349 | (let [namespaces (directory-namespaces)] 350 | (-> (pmap (partial analyze-frame namespaces) trace) 351 | (flag-duplicates) 352 | (flag-tooling))))) 353 | 354 | (defn- compile-like-exception? 355 | "'Compile-like' exceptions are those that happen at runtime 356 | (and therefore never include a `:phase`) which however, 357 | represent code that cannot possibly work, 358 | and therefore are a 'compile-like' exception (i.e. a linter could have caught them)." 359 | [{cause-type :type 360 | ^String 361 | cause-message :message}] 362 | (and (= cause-type 'java.lang.IllegalArgumentException) 363 | (or (some-> cause-message (.startsWith "No matching field")) 364 | (some-> cause-message (.startsWith "No matching method"))))) 365 | 366 | (defn- analyze-cause 367 | "Analyze the `cause-data` of an exception, in `Throwable->map` format." 368 | [cause-data print-fn] 369 | (let [pprint-str #(let [writer (StringWriter.)] 370 | (print-fn % writer) 371 | (str writer)) 372 | phase (-> cause-data :data :clojure.error/phase) 373 | m {:class (name (:type cause-data)) 374 | :phase phase 375 | :compile-like (pr-str (boolean (and (not phase) 376 | (compile-like-exception? cause-data)))) 377 | :message (:message cause-data) 378 | :stacktrace (analyze-stacktrace-data 379 | (cond (seq (:trace cause-data)) 380 | (:trace cause-data) 381 | (:at cause-data) 382 | [(:at cause-data)]))}] 383 | (if-let [data (filtered-ex-data (:data cause-data))] 384 | (if (or (:clojure.spec/failure data) 385 | (:clojure.spec.alpha/failure data)) 386 | (assoc m 387 | :message "Spec assertion failed." 388 | :spec (prepare-spec-data data pprint-str)) 389 | (-> m 390 | (assoc :data (pprint-str data) 391 | :location (select-keys data [:clojure.error/line 392 | :clojure.error/column 393 | :clojure.error/phase 394 | :clojure.error/source 395 | :clojure.error/symbol])))) 396 | m))) 397 | 398 | (defn- analyze-causes 399 | "Analyze the cause chain of the `exception-data` in `Throwable->map` format." 400 | [exception-data print-fn] 401 | (let [causes (vec (:via exception-data))] 402 | (into [] (comp (map #(analyze-cause % print-fn)) 403 | (map extract-location)) 404 | (cond-> causes 405 | (not (:trace (first causes))) 406 | (assoc-in [0 :trace] (:trace exception-data)))))) 407 | 408 | (defn analyze 409 | "Return the analyzed cause chain for `exception` beginning with the 410 | thrown exception. `exception` can be an instance of `Throwable` or a 411 | map in the same format as `Throwable->map`. For `ex-info` 412 | exceptions, the response contains a `:data` slot with the pretty 413 | printed data. For clojure.spec asserts, the `:spec` slot contains a 414 | map of pretty printed components describing spec failures." 415 | {:added "0.1.0"} 416 | ([exception] 417 | (analyze exception pprint)) 418 | ([exception print-fn] 419 | (cond (instance? Throwable exception) 420 | (analyze-causes (throwable/Throwable->map exception) print-fn) 421 | (and (map? exception) (:trace exception)) 422 | (analyze-causes exception print-fn)))) 423 | -------------------------------------------------------------------------------- /src/haystack/parser.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser 2 | "The stacktrace parser." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:require [haystack.parser.aviso :as aviso] 6 | [haystack.parser.clojure.repl :as clojure.repl] 7 | [haystack.parser.clojure.stacktrace :as clojure.stacktrace] 8 | [haystack.parser.clojure.tagged-literal :as clojure.tagged-literal] 9 | [haystack.parser.clojure.throwable :as clojure.throwable] 10 | [haystack.parser.java :as java] 11 | [haystack.parser.util :as util])) 12 | 13 | (def ^{:added "0.1.0"} default-parsers 14 | "The default stacktrace parsers." 15 | [clojure.throwable/parse-stacktrace 16 | clojure.tagged-literal/parse-stacktrace 17 | clojure.stacktrace/parse-stacktrace 18 | java/parse-stacktrace 19 | clojure.repl/parse-stacktrace 20 | aviso/parse-stacktrace]) 21 | 22 | (def ^{:added "0.1.0"} default-input-transformations 23 | "The default input transformations. 24 | 25 | - `identity` Do nothing, forward input to the parser. 26 | - `safe-read-edn` Read input as EDN and pass it to the parser. 27 | - 2x `safe-read-edn` Read input as EDN twice and pass it to the parser." 28 | [identity util/safe-read-edn (comp util/safe-read-edn util/safe-read-edn)]) 29 | 30 | (defn parse 31 | "Parse the `stacktrace`. 32 | 33 | The `stacktrace` is parsed by applying each function in 34 | `input-transformations` on `stacktrace` and invoking each of the 35 | `parsers` on the result. The first successful parse result will be 36 | returned, or nil if none of the parsers succeeded. 37 | 38 | If `parsers` or `input-transformations` are nil, `default-parsers` 39 | and `default-input-transformations` will be used instead." 40 | {:added "0.1.0"} 41 | ([stacktrace] 42 | (parse stacktrace nil)) 43 | ([stacktrace {:keys [parsers input-transformations]}] 44 | (some (fn [transformation] 45 | (some (fn [parser] 46 | (let [result (parser (transformation stacktrace))] 47 | (when-not (:error result) 48 | result))) 49 | (or parsers default-parsers))) 50 | (or input-transformations default-input-transformations)))) 51 | -------------------------------------------------------------------------------- /src/haystack/parser/aviso.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.aviso 2 | "Parser for stacktraces in the Aviso format." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:require [haystack.parser.util :as util] 6 | [instaparse.core #?(:clj :refer :cljs :refer-macros) [defparser]])) 7 | 8 | (def ^:private stacktrace-start-regex 9 | "The regular expression matching the start of an Aviso stacktrace." 10 | #"(?s)([^\s]+\s+[^\s]+:\s+\d+[\s])") 11 | 12 | (defparser ^:private parser 13 | "S = stacktrace ? 14 | 15 | = (traces causes) 16 | = (newline | whitespace | #'.')+ 17 | 18 | traces = trace-seq 19 | = trace Epsilon | trace trace-seq 20 | trace = frames 21 | 22 | = Epsilon | | (frame frames) 23 | frame = ? call file frame-location? 24 | = number ( frame-repeats)? 25 | = <'repeats'> times 26 | 27 | = call-clojure / call-java 28 | = class method 29 | = class method 30 | 31 | causes = cause-seq 32 | = cause Epsilon | cause cause-seq 33 | cause = type message data 34 | message = #'.'+ 35 | 36 | data = data-block 37 | = Epsilon | data-entry data-block 38 | = data-key data-value 39 | data-key = simple-name 40 | data-value = #'.'+ 41 | 42 | type = class 43 | class = (simple-name (dot simple-name)+) 44 | file = 'REPL Input' | (simple-name (dot simple-name)) 45 | method = simple-name (slash simple-name)* 46 | more = 47 | = number <'time'> <'s'>? 48 | 49 | = #'[0-9]' 50 | = '.' 51 | = <#'\\Z'> 52 | = #'[a-zA-Z]' 53 | = '/' 54 | 55 | double-colon = ':' 56 | newline = #'[\\n\\r]' | 57 | number = '-'? digit+ 58 | lparen = '(' 59 | rparen = ')' 60 | simple-name = #'[a-zA-Z0-9_$*-]'+ 61 | whitespace = #'[^\\S\\r\\n]+'") 62 | 63 | (def ^:private transform-class 64 | "Transform a :class node into the `Throwable->map` format." 65 | (comp symbol (partial apply str))) 66 | 67 | (defn- transform-cause 68 | "Transform a :cause node into the `Throwable->map` format." 69 | [& args] 70 | (into {} args)) 71 | 72 | (defn- transform-data 73 | "Transform a :data node into the `Throwable->map` format." 74 | [& args] 75 | [:data (some->> args (apply hash-map))]) 76 | 77 | (defn- transform-exception 78 | "Transform a :exception node into the `Throwable->map` format." 79 | [& args] 80 | (into {} args)) 81 | 82 | (def ^:private transform-file 83 | "Transform a :file node into the `Throwable->map` format." 84 | (partial apply str)) 85 | 86 | (def ^:private transform-number 87 | "Transform a :number node into the `Throwable->map` format." 88 | (comp util/safe-read-edn (partial apply str))) 89 | 90 | (def ^:private transform-method 91 | "Transform a :method node into the `Throwable->map` format." 92 | (comp symbol (partial apply str))) 93 | 94 | (defn- transform-message 95 | "Transform a :message node into the `Throwable->map` format." 96 | [& content] 97 | [:message (apply str content)]) 98 | 99 | (def ^:private transform-stacktrace 100 | "Transform a stacktrace node into the `Throwable->map` format." 101 | (fn [[_ & traces] [_ & causes]] 102 | (let [causes (reverse causes) 103 | traces (remove empty? traces) 104 | root (last causes)] 105 | {:cause (:message root) 106 | :data (:data root) 107 | :trace (vec (reverse (apply concat traces))) 108 | :via (vec causes)}))) 109 | 110 | (defn- transform-trace 111 | "Transform a :trace node into the `Throwable->map` format." 112 | [& frames] 113 | (vec (mapcat (fn [frame] 114 | (if-let [n (nth frame 4 nil)] 115 | (repeat n (vec (butlast frame))) 116 | [frame])) 117 | frames))) 118 | 119 | (def ^:private transformations 120 | "The Aviso stacktrace transformations." 121 | {:S transform-stacktrace 122 | :cause transform-cause 123 | :class transform-class 124 | :data transform-data 125 | :data-key keyword 126 | :data-value (comp util/safe-read-edn (partial apply str)) 127 | :exception transform-exception 128 | :file transform-file 129 | :frame vector 130 | :message transform-message 131 | :method transform-method 132 | :number transform-number 133 | :simple-name str 134 | :simple-symbol symbol 135 | :trace transform-trace}) 136 | 137 | (defn parse-stacktrace 138 | "Parse `input` as a stacktrace in the Aviso format." 139 | {:added "0.1.0"} 140 | [input] 141 | (util/parse-stacktrace parser transformations :aviso stacktrace-start-regex input)) 142 | -------------------------------------------------------------------------------- /src/haystack/parser/clojure/repl.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.repl 2 | "Parser for stacktraces in the `clojure.repl` format." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:require [clojure.edn :as edn] 6 | [haystack.parser.util :as util] 7 | [instaparse.core #?(:clj :refer :cljs :refer-macros) [defparser]])) 8 | 9 | (def ^:private stacktrace-start-regex 10 | "The regular expression matching the start of a `clojure.repl/pst` stacktrace." 11 | #"(?s)([a-zA-Z0-9_$/-]+)\s+.*\n\s+([a-zA-Z0-9_$/.-]+)\s+\(([a-zA-Z0-9_$/.-]+):(\d+)\)") 12 | 13 | (defparser ^:private parser 14 | "S = stacktrace ? 15 | 16 | = (exception causes) 17 | = (newline | whitespace | #'.')+ 18 | 19 | exception = type message ( data)? trace 20 | type = class 21 | message = #'.'* 22 | data = lbrace #'.'* rbrace 23 | 24 | = Epsilon | cause causes 25 | = exception 26 | 27 | trace = frame frames 28 | = Epsilon | frame frames 29 | = call 30 | call = invocation file number 31 | = class ( | ) method 32 | 33 | caused-by = 'Caused by:' 34 | class = simple-name (dot simple-name)* 35 | file = simple-name (dot simple-name) 36 | method = simple-name 37 | more = 'more' 38 | 39 | = #'[0-9]' 40 | = '.' 41 | = '/' 42 | = <#'\\Z'> 43 | 44 | double-colon = ':' 45 | newline = #'[\\n\\r]' | 46 | number = '-'? digit+ 47 | lparen = '(' 48 | rparen = ')' 49 | = '{' 50 | = '}' 51 | simple-name = #'[a-zA-Z0-9_$/-]'+ 52 | whitespace = #'[^\\S\\r\\n]+'") 53 | 54 | (defn- transform-data 55 | "Transform a :data node into the `Throwable->map` format." 56 | [& data] 57 | (when-let [content (util/safe-read-edn (apply str data))] 58 | [:data content])) 59 | 60 | (defn- transform-stacktrace 61 | "Transform the :S node into the `Throwable->map` format." 62 | [& causes] 63 | (let [root (last causes)] 64 | {:cause (:message root) 65 | :data (:data root) 66 | :trace (:trace root) 67 | :via (mapv (fn [{:keys [data type message trace]}] 68 | (cond-> {:at (first trace) 69 | :message message 70 | :type type 71 | :trace trace} 72 | data (assoc :data data))) 73 | causes)})) 74 | 75 | (defn- transform-exception 76 | "Transform a :exception node into the `Throwable->map` format." 77 | [& exceptions] 78 | (reduce (fn [m [k v]] (assoc m k v)) {} exceptions)) 79 | 80 | (def ^:private transform-file 81 | "Transform a :file node into the `Throwable->map` format." 82 | (partial apply str)) 83 | 84 | (def ^:private transform-class 85 | "Transform a :class node into the `Throwable->map` format." 86 | (comp symbol (partial apply str))) 87 | 88 | (defn- transform-message 89 | "Transform a :message node into the `Throwable->map` format." 90 | [& content] 91 | [:message (apply str content)]) 92 | 93 | (def ^:private transform-number 94 | "Transform a :number node into the `Throwable->map` format." 95 | (comp edn/read-string (partial apply str))) 96 | 97 | (defn- transform-trace 98 | "Transform a :trace node into the `Throwable->map` format." 99 | [& frames] 100 | [:trace (vec frames)]) 101 | 102 | (def ^:private transformations 103 | "The Instaparse `clojure.repl` transformations." 104 | {:S transform-stacktrace 105 | :call vector 106 | :class transform-class 107 | :data transform-data 108 | :exception transform-exception 109 | :file transform-file 110 | :message transform-message 111 | :method symbol 112 | :number transform-number 113 | :simple-name str 114 | :simple-symbol symbol 115 | :trace transform-trace}) 116 | 117 | (defn parse-stacktrace 118 | "Parse `input` as a stacktrace in `clojure.repl` format." 119 | {:added "0.1.0"} 120 | [input] 121 | (util/parse-stacktrace parser transformations :clojure.repl stacktrace-start-regex input)) 122 | -------------------------------------------------------------------------------- /src/haystack/parser/clojure/stacktrace.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.stacktrace 2 | "Parser for stacktraces in the `clojure.stacktrace` format." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:require [clojure.edn :as edn] 6 | [haystack.parser.util :as util] 7 | [instaparse.core #?(:clj :refer :cljs :refer-macros) [defparser]])) 8 | 9 | (def ^:private stacktrace-start-regex 10 | "The regular expression matching the start of a `clojure.stacktrace` 11 | formatted stacktrace." 12 | #"(?s)(([^\s]+):\s([^\n\r]+).*at+)") 13 | 14 | (defparser ^:private parser 15 | "S = stacktrace ? 16 | 17 | = (exception causes) 18 | = (newline | whitespace | #'.')+ 19 | 20 | exception = type message ( data)? trace 21 | type = class 22 | message = #'.'* 23 | data = lbrace #'.'* rbrace 24 | 25 | = Epsilon | cause causes 26 | = exception 27 | 28 | trace = frame frames 29 | = Epsilon | frame frames 30 | = call 31 | call = invocation file number 32 | = class ( | ) method 33 | 34 | at = 'at' 35 | caused-by = 'Caused by:' 36 | class = simple-name (dot simple-name)* 37 | file = simple-name (dot simple-name) 38 | method = simple-name 39 | more = 'more' 40 | 41 | = #'[0-9]' 42 | = '.' 43 | = '/' 44 | = <#'\\Z'> 45 | 46 | double-colon = ':' 47 | newline = #'[\\n\\r]' | 48 | number = '-'? digit+ 49 | lparen = '(' 50 | rparen = ')' 51 | = '{' 52 | = '}' 53 | simple-name = #'[a-zA-Z0-9_$/-]'+ 54 | whitespace = #'[^\\S\\r\\n]+'") 55 | 56 | (defn- transform-data 57 | "Transform a :data node into the `Throwable->map` format." 58 | [& data] 59 | (when-let [content (util/safe-read-edn (apply str data))] 60 | [:data content])) 61 | 62 | (defn- transform-stacktrace 63 | "Transform the :S node into the `Throwable->map` format." 64 | [& causes] 65 | (let [root (last causes)] 66 | {:cause (:message root) 67 | :data (:data root) 68 | :trace (:trace root) 69 | :via (mapv (fn [{:keys [data type message trace]}] 70 | (cond-> {:at (first trace) 71 | :message message 72 | :type type 73 | :trace trace} 74 | data (assoc :data data))) 75 | causes)})) 76 | 77 | (defn- transform-exception 78 | "Transform a :exception node into the `Throwable->map` format." 79 | [& exceptions] 80 | (reduce (fn [m [k v]] (assoc m k v)) {} exceptions)) 81 | 82 | (def ^:private transform-file 83 | "Transform a :file node into the `Throwable->map` format." 84 | (partial apply str)) 85 | 86 | (def ^:private transform-class 87 | "Transform a :class node into the `Throwable->map` format." 88 | (comp symbol (partial apply str))) 89 | 90 | (defn- transform-message 91 | "Transform a :message node into the `Throwable->map` format." 92 | [& content] 93 | [:message (apply str content)]) 94 | 95 | (def ^:private transform-number 96 | "Transform a :number node into the `Throwable->map` format." 97 | (comp edn/read-string (partial apply str))) 98 | 99 | (defn- transform-trace 100 | "Transform a :trace node into the `Throwable->map` format." 101 | [& frames] 102 | [:trace (vec frames)]) 103 | 104 | (def ^:private transformations 105 | "The Instaparse `clojure.stacktrace` transformations." 106 | {:S transform-stacktrace 107 | :call vector 108 | :class transform-class 109 | :data transform-data 110 | :exception transform-exception 111 | :file transform-file 112 | :message transform-message 113 | :method symbol 114 | :number transform-number 115 | :simple-name str 116 | :simple-symbol symbol 117 | :trace transform-trace}) 118 | 119 | (defn parse-stacktrace 120 | "Parse `input` as a stacktrace in `clojure.stacktrace` format." 121 | {:added "0.1.0"} 122 | [input] 123 | (util/parse-stacktrace parser transformations :clojure.stacktrace stacktrace-start-regex input)) 124 | -------------------------------------------------------------------------------- /src/haystack/parser/clojure/tagged_literal.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.tagged-literal 2 | "Parser for stacktraces in Clojure's tagged literal format." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:require [clojure.edn :as edn] 6 | [haystack.parser.util :as util])) 7 | 8 | (def ^:private read-options 9 | "The options used when reading a stacktrace in EDN format." 10 | {:default tagged-literal :eof nil}) 11 | 12 | (def ^:private stacktrace-start-regex 13 | "The regular expression matching the start of a Clojure stacktrace." 14 | #"(?s)#error\s*\{") 15 | 16 | (defn- transform-trace-element 17 | "Normalize the stacktrace `element`." 18 | [element] 19 | (if (sequential? element) 20 | (let [[class method file line-number] element] 21 | [(some-> class symbol) 22 | (some-> method symbol) 23 | (some-> file str) 24 | line-number]) 25 | element)) 26 | 27 | (defn- transform-cause 28 | "Normalize the stacktrace `cause`." 29 | [{:keys [at message trace] :as cause}] 30 | (cond-> cause 31 | (sequential? at) 32 | (update :at transform-trace-element) 33 | (and message (not (string? message))) 34 | (update :message str) 35 | (seq trace) 36 | (update :trace #(mapv transform-trace-element %)))) 37 | 38 | (defn- transform 39 | "Normalize the `stacktrace`." 40 | [{:keys [cause phase via trace] :as stacktrace}] 41 | (cond-> stacktrace 42 | (and cause (not (string? cause))) 43 | (update :cause str) 44 | (and phase (not (keyword? phase))) 45 | (update :phase (comp keyword str)) 46 | (seq trace) 47 | (update :trace #(mapv transform-trace-element %)) 48 | (seq via) 49 | (update :via #(mapv transform-cause %)))) 50 | 51 | (defn parse-stacktrace 52 | "Parse `input` as a stacktrace in Clojure's tagged literal format." 53 | {:added "0.1.0"} 54 | [input] 55 | (try (let [s (util/seek-to-regex input stacktrace-start-regex) 56 | {:keys [form tag]} (edn/read-string read-options s)] 57 | (if (= 'error tag) 58 | (assoc (transform form) :stacktrace-type :clojure.tagged-literal) 59 | (util/error-incorrect-input input))) 60 | (catch #?(:clj Exception :cljs js/Error) e 61 | (util/error-unsupported-input input e)))) 62 | -------------------------------------------------------------------------------- /src/haystack/parser/clojure/throwable.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.throwable 2 | "Convert `java.lang.Throwable` instances into the `Throwable->map` data format." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:refer-clojure :exclude [StackTraceElement->vec Throwable->map]) 6 | (:require [haystack.parser.util :as util] 7 | #?(:cljs [cljs.repl :as repl]))) 8 | 9 | ;; Throwable 10 | 11 | ;; The `StackTraceElement->vec` and `Throwable->map` functions were copied from 12 | ;; Clojure, because `StackTraceElement->vec` was introduced in Clojure version 13 | ;; 1.9 and we want to support it in older Clojure versions as well. 14 | 15 | #?(:clj (defn StackTraceElement->vec 16 | "Constructs a data representation for a StackTraceElement: [class method file line]" 17 | {:added "0.1.0"} 18 | [^StackTraceElement o] 19 | [(symbol (.getClassName o)) (symbol (.getMethodName o)) (.getFileName o) (.getLineNumber o)])) 20 | 21 | #?(:clj (defn Throwable->map 22 | "Constructs a data representation for a Throwable with keys: 23 | :cause - root cause message 24 | :phase - error phase 25 | :via - cause chain, with cause keys: 26 | :type - exception class symbol 27 | :message - exception message 28 | :data - ex-data 29 | :at - top stack element 30 | :trace - root cause stack elements" 31 | {:added "0.1.0"} 32 | [^Throwable o] 33 | (let [base (fn [^Throwable t] 34 | (merge {:type (symbol (.getName (class t)))} 35 | (when-let [msg (.getLocalizedMessage t)] 36 | {:message msg}) 37 | (when-let [ed (ex-data t)] 38 | {:data ed}) 39 | (let [st (.getStackTrace t)] 40 | (when (pos? (alength st)) 41 | {:at (StackTraceElement->vec (aget st 0)) 42 | ;; This is an additional key not present in 43 | ;; Throwable->map. We added it to have the complete 44 | ;; trace available for analysis. 45 | :trace (vec (map StackTraceElement->vec st))})))) 46 | via (loop [via [], ^Throwable t o] 47 | (if t 48 | (recur (conj via t) (.getCause t)) 49 | via)) 50 | ^Throwable root (peek via)] 51 | (merge {:via (vec (map base via)) 52 | :trace (vec (map StackTraceElement->vec 53 | (.getStackTrace (or root o))))} 54 | (when-let [root-msg (.getLocalizedMessage root)] 55 | {:cause root-msg}) 56 | (when-let [data (ex-data root)] 57 | {:data data}) 58 | (when-let [phase (-> o ex-data :clojure.error/phase)] 59 | {:phase phase}))))) 60 | 61 | (defn parse-stacktrace 62 | "Parse `input` as a `java.lang.Throwable` instance." 63 | {:added "0.1.0"} 64 | [input] 65 | (if #?(:clj (instance? Throwable input) 66 | :cljs (or (instance? ExceptionInfo input) 67 | (instance? js/Error input))) 68 | (assoc (#?(:clj Throwable->map :cljs repl/Error->map) input) 69 | :stacktrace-type :throwable) 70 | (util/error-unsupported-input input))) 71 | -------------------------------------------------------------------------------- /src/haystack/parser/java.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.java 2 | "Parser for stacktraces in the Java format." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:require [clojure.edn :as edn] 6 | [haystack.parser.util :as util] 7 | [instaparse.core #?(:clj :refer :cljs :refer-macros) [defparser]])) 8 | 9 | (def ^:private stacktrace-start-regex 10 | "The regular expression matching the start of an Java stacktrace." 11 | #"(?s)(([^\s]+):\s([^\n\r]+)\s+at+)") 12 | 13 | (defparser ^:private parser 14 | "S = stacktrace ? 15 | 16 | = (exception causes) 17 | = (newline | whitespace | #'.')+ 18 | 19 | exception = type message ( data)? trace 20 | type = class 21 | message = #'.'* 22 | data = lbrace #'.'* rbrace 23 | 24 | = Epsilon | cause causes 25 | = exception 26 | cause-more = number 27 | 28 | trace = frame frames 29 | = Epsilon | frame frames 30 | = call 31 | call = class method file number 32 | 33 | at = 'at' 34 | caused-by = 'Caused by: ' 35 | class = (simple-name (dot simple-name)*) 36 | file = simple-name (dot simple-name) 37 | method = simple-name 38 | more = 'more' 39 | 40 | = #'[0-9]' 41 | = '.' 42 | 43 | double-colon = ':' 44 | newline = #'[\\n\\r]' 45 | number = '-'? digit+ 46 | lparen = '(' 47 | rparen = ')' 48 | = '{' 49 | = '}' 50 | simple-name = #'[a-zA-Z0-9_$/-]'+ 51 | whitespace = #'[^\\S\\r\\n]+'") 52 | 53 | (defn- transform-data 54 | "Transform a :data node into the `Throwable->map` format." 55 | [& data] 56 | (when-let [content (util/safe-read-edn (apply str data))] 57 | [:data content])) 58 | 59 | (defn- transform-stacktrace 60 | "Transform the :S node into the `Throwable->map` format." 61 | [& causes] 62 | (let [root (last causes)] 63 | {:cause (:message root) 64 | :data (:data root) 65 | :trace (:trace root) 66 | :via (mapv (fn [{:keys [data type message trace]}] 67 | (cond-> {:at (first trace) 68 | :message message 69 | :type type 70 | :trace trace} 71 | data (assoc :data data))) 72 | causes)})) 73 | 74 | (defn- transform-exception 75 | "Transform a :exception node into the `Throwable->map` format." 76 | [& exceptions] 77 | (reduce (fn [m [k v]] (assoc m k v)) {} exceptions)) 78 | 79 | (def ^:private transform-file 80 | "Transform a :file node into the `Throwable->map` format." 81 | (partial apply str)) 82 | 83 | (def ^:private transform-class 84 | "Transform a :class node into the `Throwable->map` format." 85 | (comp symbol (partial apply str))) 86 | 87 | (defn- transform-message 88 | "Transform a :message node into the `Throwable->map` format." 89 | [& content] 90 | [:message (apply str content)]) 91 | 92 | (def ^:private transform-number 93 | "Transform a :number node into the `Throwable->map` format." 94 | (comp edn/read-string (partial apply str))) 95 | 96 | (defn- transform-trace 97 | "Transform a :trace node into the `Throwable->map` format." 98 | [& frames] 99 | [:trace (vec frames)]) 100 | 101 | (def ^:private transformations 102 | "The Instaparse Java transformations." 103 | {:S transform-stacktrace 104 | :call vector 105 | :class transform-class 106 | :data transform-data 107 | :exception transform-exception 108 | :file transform-file 109 | :message transform-message 110 | :method symbol 111 | :number transform-number 112 | :simple-name str 113 | :simple-symbol symbol 114 | :trace transform-trace}) 115 | 116 | (defn parse-stacktrace 117 | "Parse `input` as a stacktrace in the Java format." 118 | {:added "0.1.0"} 119 | [input] 120 | (util/parse-stacktrace parser transformations :java stacktrace-start-regex input)) 121 | -------------------------------------------------------------------------------- /src/haystack/parser/util.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.util 2 | "Utility functions used by the stacktrace parsers." 3 | {:added "0.1.0" 4 | :author "r0man"} 5 | (:require [clojure.edn :as edn] 6 | [clojure.string :as str] 7 | [instaparse.core :as insta])) 8 | 9 | (defn error-incorrect-input 10 | "Return the incorrect input error." 11 | {:added "0.1.0"} 12 | [input & [failure]] 13 | (cond-> {:error :incorrect 14 | :type :incorrect-input 15 | :input input} 16 | failure (assoc :failure failure))) 17 | 18 | (defn error-unsupported-input 19 | "Return the unsupported input error." 20 | {:added "0.1.0"} 21 | [input & [exception]] 22 | (cond-> {:error :unsupported 23 | :type :unsupported-input 24 | :input input} 25 | exception (assoc :exception exception))) 26 | 27 | (defn seek-to-regex 28 | "Return the first substring in `s` matching `regexp`." 29 | {:added "0.1.0"} 30 | [^String s regex] 31 | (when-let [match (first (re-find regex s))] 32 | (when-let [index (str/index-of s match)] 33 | (.substring s index)))) 34 | 35 | (defn instaparse 36 | "Invoke `insta/parse` with `parser` and `input`. 37 | 38 | Returns the parsed tree on success, or a map with an :error key and 39 | the Instaparse :failure on error." 40 | {:added "0.1.0"} 41 | [parser input] 42 | (let [result (try (insta/parse parser input) 43 | (catch #?(:clj Exception :cljs js/Error) e 44 | (error-unsupported-input input e)))] 45 | (if-let [failure (insta/get-failure result)] 46 | (error-incorrect-input input failure) 47 | result))) 48 | 49 | (defn parse-try 50 | "Skip over `input` to the start of `regex` and parse the rest of the 51 | string. Keep doing this repeatedly until the first match." 52 | {:added "0.1.0"} 53 | [parser input regex] 54 | (if-not (string? input) 55 | (error-unsupported-input input) 56 | (let [result (instaparse parser input)] 57 | (or (when-not (:error result) 58 | result) 59 | (loop [input (seek-to-regex input regex)] 60 | (when (seq input) 61 | (let [result (instaparse parser input)] 62 | (if (:error result) 63 | (let [next-input (seek-to-regex input regex)] 64 | (if (= input next-input) 65 | result 66 | (recur next-input))) 67 | result)))) 68 | result)))) 69 | 70 | (defn parse-stacktrace 71 | "Parse a stacktrace with AST transformations applied and input skipped." 72 | {:added "0.1.0"} 73 | [parser transformations stacktrace-type start-regex input] 74 | (let [result (parse-try parser input start-regex)] 75 | (if (:error result) 76 | result 77 | (-> (insta/transform transformations result) 78 | (assoc :stacktrace-type stacktrace-type))))) 79 | 80 | (defn safe-read-edn 81 | "Read the string `s` in EDN format in a safe way. 82 | 83 | The `tagged-literal` function is used as the default tagged literal 84 | reader. Any exception thrown while reading is catched and nil will 85 | be returned instead." 86 | {:added "0.1.0"} 87 | [s] 88 | (try (edn/read-string {:default tagged-literal} s) 89 | (catch #?(:clj Exception :cljs js/Error) _))) 90 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/boom.aviso.full.txt: -------------------------------------------------------------------------------- 1 | java.lang.Thread.run Thread.java: 829 2 | clojure.lang.AFn.run AFn.java: 22 3 | nrepl.middleware.session/session-exec/main-loop session.clj: 217 4 | nrepl.middleware.session/session-exec/main-loop/fn session.clj: 218 5 | clojure.lang.AFn.run AFn.java: 22 6 | nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn interruptible_eval.clj: 152 7 | nrepl.middleware.interruptible-eval/evaluate interruptible_eval.clj: 84 8 | clojure.lang.RestFn.invoke RestFn.java: 1523 9 | clojure.main/repl main.clj: 368 10 | clojure.main/repl main.clj: 458 11 | clojure.main/repl/fn main.clj: 458 12 | clojure.main/repl/read-eval-print main.clj: 437 13 | clojure.main/repl/read-eval-print/fn main.clj: 437 14 | nrepl.middleware.interruptible-eval/evaluate/fn interruptible_eval.clj: 87 15 | clojure.lang.RestFn.invoke RestFn.java: 425 16 | clojure.core/with-bindings* core.clj: 1977 (repeats 2 times) 17 | clojure.core/apply core.clj: 667 18 | clojure.lang.AFn.applyTo AFn.java: 144 19 | clojure.lang.AFn.applyToHelper AFn.java: 152 20 | nrepl.middleware.interruptible-eval/evaluate/fn/fn interruptible_eval.clj: 87 21 | clojure.core/eval core.clj: 3202 22 | clojure.lang.Compiler.eval Compiler.java: 7136 23 | clojure.lang.Compiler.eval Compiler.java: 7181 24 | haystack.parser.throwable-test/eval40283 REPL Input 25 | clojure.lang.Compiler.load Compiler.java: 7640 26 | clojure.lang.Compiler.eval Compiler.java: 7186 27 | clojure.lang.Compiler$DefExpr.eval Compiler.java: 457 28 | clojure.lang.Compiler$InvokeExpr.eval Compiler.java: 3705 (repeats 2 times) 29 | clojure.lang.Compiler$InvokeExpr.eval Compiler.java: 3706 30 | clojure.lang.AFn.applyTo AFn.java: 144 31 | clojure.lang.AFn.applyToHelper AFn.java: 156 32 | clojure.lang.ExceptionInfo: BOOM-3 33 | boom: "3" 34 | clojure.lang.ExceptionInfo: BOOM-2 35 | boom: "2" 36 | clojure.lang.ExceptionInfo: BOOM-1 37 | boom: "1" 38 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/boom.aviso.txt: -------------------------------------------------------------------------------- 1 | nrepl.middleware.interruptible-eval/evaluate/fn interruptible_eval.clj: 87 2 | ... 3 | clojure.core/with-bindings* core.clj: 1977 (repeats 2 times) 4 | clojure.core/apply core.clj: 667 5 | ... 6 | nrepl.middleware.interruptible-eval/evaluate/fn/fn interruptible_eval.clj: 87 7 | clojure.core/eval core.clj: 3202 8 | ... 9 | haystack.parser.throwable-test/eval12321 REPL Input 10 | ... 11 | clojure.lang.ExceptionInfo: BOOM-3 12 | boom: "3" 13 | clojure.lang.ExceptionInfo: BOOM-2 14 | boom: "2" 15 | clojure.lang.ExceptionInfo: BOOM-1 16 | boom: "1" 17 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/boom.clojure.repl.txt: -------------------------------------------------------------------------------- 1 | ExceptionInfo BOOM-1 {:boom "1"} 2 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 3 | clojure.lang.Compiler$DefExpr.eval (Compiler.java:457) 4 | clojure.lang.Compiler.eval (Compiler.java:7186) 5 | clojure.lang.Compiler.load (Compiler.java:7640) 6 | user/eval8462 (form-init11438175174013931757.clj:1) 7 | user/eval8462 (form-init11438175174013931757.clj:1) 8 | clojure.lang.Compiler.eval (Compiler.java:7181) 9 | clojure.lang.Compiler.eval (Compiler.java:7136) 10 | clojure.core/eval (core.clj:3202) 11 | clojure.core/eval (core.clj:3198) 12 | nrepl.middleware.interruptible-eval/evaluate/fn--1933/fn--1934 (interruptible_eval.clj:87) 13 | clojure.core/apply (core.clj:667) 14 | Caused by: 15 | ExceptionInfo BOOM-2 {:boom "2"} 16 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 17 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3705) 18 | clojure.lang.Compiler$DefExpr.eval (Compiler.java:457) 19 | Caused by: 20 | ExceptionInfo BOOM-3 {:boom "3"} 21 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 22 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3705) 23 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3705) 24 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/boom.clojure.stacktrace.txt: -------------------------------------------------------------------------------- 1 | clojure.lang.ExceptionInfo: BOOM-1 2 | {:boom "1"} 3 | at clojure.lang.AFn.applyToHelper (AFn.java:160) 4 | clojure.lang.AFn.applyTo (AFn.java:144) 5 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 6 | clojure.lang.Compiler$DefExpr.eval (Compiler.java:457) 7 | clojure.lang.Compiler.eval (Compiler.java:7186) 8 | clojure.lang.Compiler.load (Compiler.java:7640) 9 | user$eval22216.invokeStatic (form-init16789459847038522943.clj:1) 10 | user$eval22216.invoke (form-init16789459847038522943.clj:1) 11 | clojure.lang.Compiler.eval (Compiler.java:7181) 12 | clojure.lang.Compiler.eval (Compiler.java:7136) 13 | clojure.core$eval.invokeStatic (core.clj:3202) 14 | clojure.core$eval.invoke (core.clj:3198) 15 | nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934.invoke (interruptible_eval.clj:87) 16 | clojure.lang.AFn.applyToHelper (AFn.java:152) 17 | clojure.lang.AFn.applyTo (AFn.java:144) 18 | clojure.core$apply.invokeStatic (core.clj:667) 19 | clojure.core$with_bindings_STAR_.invokeStatic (core.clj:1977) 20 | clojure.core$with_bindings_STAR_.doInvoke (core.clj:1977) 21 | clojure.lang.RestFn.invoke (RestFn.java:425) 22 | nrepl.middleware.interruptible_eval$evaluate$fn__1933.invoke (interruptible_eval.clj:87) 23 | clojure.main$repl$read_eval_print__9110$fn__9113.invoke (main.clj:437) 24 | clojure.main$repl$read_eval_print__9110.invoke (main.clj:437) 25 | clojure.main$repl$fn__9119.invoke (main.clj:458) 26 | clojure.main$repl.invokeStatic (main.clj:458) 27 | clojure.main$repl.doInvoke (main.clj:368) 28 | clojure.lang.RestFn.invoke (RestFn.java:1523) 29 | nrepl.middleware.interruptible_eval$evaluate.invokeStatic (interruptible_eval.clj:84) 30 | nrepl.middleware.interruptible_eval$evaluate.invoke (interruptible_eval.clj:56) 31 | nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970.invoke (interruptible_eval.clj:152) 32 | clojure.lang.AFn.run (AFn.java:22) 33 | nrepl.middleware.session$session_exec$main_loop__2036$fn__2040.invoke (session.clj:218) 34 | nrepl.middleware.session$session_exec$main_loop__2036.invoke (session.clj:217) 35 | clojure.lang.AFn.run (AFn.java:22) 36 | java.lang.Thread.run (Thread.java:829) 37 | Caused by: clojure.lang.ExceptionInfo: BOOM-2 38 | {:boom "2"} 39 | at clojure.lang.AFn.applyToHelper (AFn.java:160) 40 | clojure.lang.AFn.applyTo (AFn.java:144) 41 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 42 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3705) 43 | clojure.lang.Compiler$DefExpr.eval (Compiler.java:457) 44 | clojure.lang.Compiler.eval (Compiler.java:7186) 45 | clojure.lang.Compiler.load (Compiler.java:7640) 46 | user$eval22216.invokeStatic (form-init16789459847038522943.clj:1) 47 | user$eval22216.invoke (form-init16789459847038522943.clj:1) 48 | clojure.lang.Compiler.eval (Compiler.java:7181) 49 | clojure.lang.Compiler.eval (Compiler.java:7136) 50 | clojure.core$eval.invokeStatic (core.clj:3202) 51 | clojure.core$eval.invoke (core.clj:3198) 52 | nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934.invoke (interruptible_eval.clj:87) 53 | clojure.lang.AFn.applyToHelper (AFn.java:152) 54 | clojure.lang.AFn.applyTo (AFn.java:144) 55 | clojure.core$apply.invokeStatic (core.clj:667) 56 | clojure.core$with_bindings_STAR_.invokeStatic (core.clj:1977) 57 | clojure.core$with_bindings_STAR_.doInvoke (core.clj:1977) 58 | clojure.lang.RestFn.invoke (RestFn.java:425) 59 | nrepl.middleware.interruptible_eval$evaluate$fn__1933.invoke (interruptible_eval.clj:87) 60 | clojure.main$repl$read_eval_print__9110$fn__9113.invoke (main.clj:437) 61 | clojure.main$repl$read_eval_print__9110.invoke (main.clj:437) 62 | clojure.main$repl$fn__9119.invoke (main.clj:458) 63 | clojure.main$repl.invokeStatic (main.clj:458) 64 | clojure.main$repl.doInvoke (main.clj:368) 65 | clojure.lang.RestFn.invoke (RestFn.java:1523) 66 | nrepl.middleware.interruptible_eval$evaluate.invokeStatic (interruptible_eval.clj:84) 67 | nrepl.middleware.interruptible_eval$evaluate.invoke (interruptible_eval.clj:56) 68 | nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970.invoke (interruptible_eval.clj:152) 69 | clojure.lang.AFn.run (AFn.java:22) 70 | nrepl.middleware.session$session_exec$main_loop__2036$fn__2040.invoke (session.clj:218) 71 | nrepl.middleware.session$session_exec$main_loop__2036.invoke (session.clj:217) 72 | clojure.lang.AFn.run (AFn.java:22) 73 | java.lang.Thread.run (Thread.java:829) 74 | Caused by: clojure.lang.ExceptionInfo: BOOM-3 75 | {:boom "3"} 76 | at clojure.lang.AFn.applyToHelper (AFn.java:156) 77 | clojure.lang.AFn.applyTo (AFn.java:144) 78 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 79 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3705) 80 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3705) 81 | clojure.lang.Compiler$DefExpr.eval (Compiler.java:457) 82 | clojure.lang.Compiler.eval (Compiler.java:7186) 83 | clojure.lang.Compiler.load (Compiler.java:7640) 84 | user$eval22216.invokeStatic (form-init16789459847038522943.clj:1) 85 | user$eval22216.invoke (form-init16789459847038522943.clj:1) 86 | clojure.lang.Compiler.eval (Compiler.java:7181) 87 | clojure.lang.Compiler.eval (Compiler.java:7136) 88 | clojure.core$eval.invokeStatic (core.clj:3202) 89 | clojure.core$eval.invoke (core.clj:3198) 90 | nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934.invoke (interruptible_eval.clj:87) 91 | clojure.lang.AFn.applyToHelper (AFn.java:152) 92 | clojure.lang.AFn.applyTo (AFn.java:144) 93 | clojure.core$apply.invokeStatic (core.clj:667) 94 | clojure.core$with_bindings_STAR_.invokeStatic (core.clj:1977) 95 | clojure.core$with_bindings_STAR_.doInvoke (core.clj:1977) 96 | clojure.lang.RestFn.invoke (RestFn.java:425) 97 | nrepl.middleware.interruptible_eval$evaluate$fn__1933.invoke (interruptible_eval.clj:87) 98 | clojure.main$repl$read_eval_print__9110$fn__9113.invoke (main.clj:437) 99 | clojure.main$repl$read_eval_print__9110.invoke (main.clj:437) 100 | clojure.main$repl$fn__9119.invoke (main.clj:458) 101 | clojure.main$repl.invokeStatic (main.clj:458) 102 | clojure.main$repl.doInvoke (main.clj:368) 103 | clojure.lang.RestFn.invoke (RestFn.java:1523) 104 | nrepl.middleware.interruptible_eval$evaluate.invokeStatic (interruptible_eval.clj:84) 105 | nrepl.middleware.interruptible_eval$evaluate.invoke (interruptible_eval.clj:56) 106 | nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970.invoke (interruptible_eval.clj:152) 107 | clojure.lang.AFn.run (AFn.java:22) 108 | nrepl.middleware.session$session_exec$main_loop__2036$fn__2040.invoke (session.clj:218) 109 | nrepl.middleware.session$session_exec$main_loop__2036.invoke (session.clj:217) 110 | clojure.lang.AFn.run (AFn.java:22) 111 | java.lang.Thread.run (Thread.java:829) 112 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/boom.clojure.tagged-literal.txt: -------------------------------------------------------------------------------- 1 | #error { 2 | :cause "BOOM-3" 3 | :data {:boom "3"} 4 | :via 5 | [{:type clojure.lang.ExceptionInfo 6 | :message "BOOM-1" 7 | :data {:boom "1"} 8 | :at [clojure.lang.AFn applyToHelper "AFn.java" 160]} 9 | {:type clojure.lang.ExceptionInfo 10 | :message "BOOM-2" 11 | :data {:boom "2"} 12 | :at [clojure.lang.AFn applyToHelper "AFn.java" 160]} 13 | {:type clojure.lang.ExceptionInfo 14 | :message "BOOM-3" 15 | :data {:boom "3"} 16 | :at [clojure.lang.AFn applyToHelper "AFn.java" 156]}] 17 | :trace 18 | [[clojure.lang.AFn applyToHelper "AFn.java" 156] 19 | [clojure.lang.AFn applyTo "AFn.java" 144] 20 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] 21 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705] 22 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705] 23 | [clojure.lang.Compiler$DefExpr eval "Compiler.java" 457] 24 | [clojure.lang.Compiler eval "Compiler.java" 7186] 25 | [clojure.lang.Compiler load "Compiler.java" 7640] 26 | [haystack.parser.throwable_test$eval12321 invokeStatic "form-init16320452348649927506.clj" 1] 27 | [haystack.parser.throwable_test$eval12321 invoke "form-init16320452348649927506.clj" 1] 28 | [clojure.lang.Compiler eval "Compiler.java" 7181] 29 | [clojure.lang.Compiler eval "Compiler.java" 7136] 30 | [clojure.core$eval invokeStatic "core.clj" 3202] 31 | [clojure.core$eval invoke "core.clj" 3198] 32 | [nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934 invoke "interruptible_eval.clj" 87] 33 | [clojure.lang.AFn applyToHelper "AFn.java" 152] 34 | [clojure.lang.AFn applyTo "AFn.java" 144] 35 | [clojure.core$apply invokeStatic "core.clj" 667] 36 | [clojure.core$with_bindings_STAR_ invokeStatic "core.clj" 1977] 37 | [clojure.core$with_bindings_STAR_ doInvoke "core.clj" 1977] 38 | [clojure.lang.RestFn invoke "RestFn.java" 425] 39 | [nrepl.middleware.interruptible_eval$evaluate$fn__1933 invoke "interruptible_eval.clj" 87] 40 | [clojure.main$repl$read_eval_print__9110$fn__9113 invoke "main.clj" 437] 41 | [clojure.main$repl$read_eval_print__9110 invoke "main.clj" 437] 42 | [clojure.main$repl$fn__9119 invoke "main.clj" 458] 43 | [clojure.main$repl invokeStatic "main.clj" 458] 44 | [clojure.main$repl doInvoke "main.clj" 368] 45 | [clojure.lang.RestFn invoke "RestFn.java" 1523] 46 | [nrepl.middleware.interruptible_eval$evaluate invokeStatic "interruptible_eval.clj" 84] 47 | [nrepl.middleware.interruptible_eval$evaluate invoke "interruptible_eval.clj" 56] 48 | [nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970 invoke "interruptible_eval.clj" 152] 49 | [clojure.lang.AFn run "AFn.java" 22] 50 | [nrepl.middleware.session$session_exec$main_loop__2036$fn__2040 invoke "session.clj" 218] 51 | [nrepl.middleware.session$session_exec$main_loop__2036 invoke "session.clj" 217] 52 | [clojure.lang.AFn run "AFn.java" 22] 53 | [java.lang.Thread run "Thread.java" 829]]} 54 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/boom.java.txt: -------------------------------------------------------------------------------- 1 | clojure.lang.ExceptionInfo: BOOM-1 {:boom "1"} 2 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 3 | at clojure.lang.AFn.applyTo(AFn.java:144) 4 | at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3706) 5 | at clojure.lang.Compiler$DefExpr.eval(Compiler.java:457) 6 | at clojure.lang.Compiler.eval(Compiler.java:7186) 7 | at clojure.lang.Compiler.load(Compiler.java:7640) 8 | at haystack.parser.throwable_test$eval12321.invokeStatic(form-init16320452348649927506.clj:1) 9 | at haystack.parser.throwable_test$eval12321.invoke(form-init16320452348649927506.clj:1) 10 | at clojure.lang.Compiler.eval(Compiler.java:7181) 11 | at clojure.lang.Compiler.eval(Compiler.java:7136) 12 | at clojure.core$eval.invokeStatic(core.clj:3202) 13 | at clojure.core$eval.invoke(core.clj:3198) 14 | at nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934.invoke(interruptible_eval.clj:87) 15 | at clojure.lang.AFn.applyToHelper(AFn.java:152) 16 | at clojure.lang.AFn.applyTo(AFn.java:144) 17 | at clojure.core$apply.invokeStatic(core.clj:667) 18 | at clojure.core$with_bindings_STAR_.invokeStatic(core.clj:1977) 19 | at clojure.core$with_bindings_STAR_.doInvoke(core.clj:1977) 20 | at clojure.lang.RestFn.invoke(RestFn.java:425) 21 | at nrepl.middleware.interruptible_eval$evaluate$fn__1933.invoke(interruptible_eval.clj:87) 22 | at clojure.main$repl$read_eval_print__9110$fn__9113.invoke(main.clj:437) 23 | at clojure.main$repl$read_eval_print__9110.invoke(main.clj:437) 24 | at clojure.main$repl$fn__9119.invoke(main.clj:458) 25 | at clojure.main$repl.invokeStatic(main.clj:458) 26 | at clojure.main$repl.doInvoke(main.clj:368) 27 | at clojure.lang.RestFn.invoke(RestFn.java:1523) 28 | at nrepl.middleware.interruptible_eval$evaluate.invokeStatic(interruptible_eval.clj:84) 29 | at nrepl.middleware.interruptible_eval$evaluate.invoke(interruptible_eval.clj:56) 30 | at nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970.invoke(interruptible_eval.clj:152) 31 | at clojure.lang.AFn.run(AFn.java:22) 32 | at nrepl.middleware.session$session_exec$main_loop__2036$fn__2040.invoke(session.clj:218) 33 | at nrepl.middleware.session$session_exec$main_loop__2036.invoke(session.clj:217) 34 | at clojure.lang.AFn.run(AFn.java:22) 35 | at java.base/java.lang.Thread.run(Thread.java:829) 36 | Caused by: clojure.lang.ExceptionInfo: BOOM-2 {:boom "2"} 37 | at clojure.lang.AFn.applyToHelper(AFn.java:160) 38 | at clojure.lang.AFn.applyTo(AFn.java:144) 39 | at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3706) 40 | at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3705) 41 | ... 31 more 42 | Caused by: clojure.lang.ExceptionInfo: BOOM-3 {:boom "3"} 43 | at clojure.lang.AFn.applyToHelper(AFn.java:156) 44 | at clojure.lang.AFn.applyTo(AFn.java:144) 45 | at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3706) 46 | at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3705) 47 | ... 32 more 48 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/divide-by-zero.aviso.txt: -------------------------------------------------------------------------------- 1 | nrepl.middleware.interruptible-eval/evaluate/fn interruptible_eval.clj: 87 2 | ... 3 | clojure.core/with-bindings* core.clj: 1977 (repeats 2 times) 4 | clojure.core/apply core.clj: 667 5 | ... 6 | nrepl.middleware.interruptible-eval/evaluate/fn/fn interruptible_eval.clj: 87 7 | clojure.core/eval core.clj: 3202 8 | ... 9 | haystack.parser.throwable-test/eval12321 REPL Input 10 | ... 11 | haystack.parser.throwable-test/fn throwable_test.clj: 13 12 | ... 13 | java.lang.ArithmeticException: Divide by zero 14 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/divide-by-zero.clojure.repl.txt: -------------------------------------------------------------------------------- 1 | ArithmeticException Divide by zero 2 | clojure.lang.Numbers.divide (Numbers.java:188) 3 | clojure.lang.Numbers.divide (Numbers.java:3901) 4 | haystack.parser.throwable-test/fn--8516 (throwable_test.clj:13) 5 | haystack.parser.throwable-test/fn--8516 (throwable_test.clj:13) 6 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 7 | clojure.lang.Compiler$DefExpr.eval (Compiler.java:457) 8 | clojure.lang.Compiler.eval (Compiler.java:7186) 9 | clojure.lang.Compiler.load (Compiler.java:7640) 10 | user/eval8462 (form-init11438175174013931757.clj:1) 11 | user/eval8462 (form-init11438175174013931757.clj:1) 12 | clojure.lang.Compiler.eval (Compiler.java:7181) 13 | clojure.lang.Compiler.eval (Compiler.java:7136) 14 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/divide-by-zero.clojure.stacktrace.txt: -------------------------------------------------------------------------------- 1 | java.lang.ArithmeticException: Divide by zero 2 | at clojure.lang.Numbers.divide (Numbers.java:188) 3 | clojure.lang.Numbers.divide (Numbers.java:3901) 4 | haystack.parser.throwable_test$fn__22228.invokeStatic (throwable_test.clj:13) 5 | haystack.parser.throwable_test/fn (throwable_test.clj:13) 6 | clojure.lang.AFn.applyToHelper (AFn.java:152) 7 | clojure.lang.AFn.applyTo (AFn.java:144) 8 | clojure.lang.Compiler$InvokeExpr.eval (Compiler.java:3706) 9 | clojure.lang.Compiler$DefExpr.eval (Compiler.java:457) 10 | clojure.lang.Compiler.eval (Compiler.java:7186) 11 | clojure.lang.Compiler.load (Compiler.java:7640) 12 | user$eval22216.invokeStatic (form-init16789459847038522943.clj:1) 13 | user$eval22216.invoke (form-init16789459847038522943.clj:1) 14 | clojure.lang.Compiler.eval (Compiler.java:7181) 15 | clojure.lang.Compiler.eval (Compiler.java:7136) 16 | clojure.core$eval.invokeStatic (core.clj:3202) 17 | clojure.core$eval.invoke (core.clj:3198) 18 | nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934.invoke (interruptible_eval.clj:87) 19 | clojure.lang.AFn.applyToHelper (AFn.java:152) 20 | clojure.lang.AFn.applyTo (AFn.java:144) 21 | clojure.core$apply.invokeStatic (core.clj:667) 22 | clojure.core$with_bindings_STAR_.invokeStatic (core.clj:1977) 23 | clojure.core$with_bindings_STAR_.doInvoke (core.clj:1977) 24 | clojure.lang.RestFn.invoke (RestFn.java:425) 25 | nrepl.middleware.interruptible_eval$evaluate$fn__1933.invoke (interruptible_eval.clj:87) 26 | clojure.main$repl$read_eval_print__9110$fn__9113.invoke (main.clj:437) 27 | clojure.main$repl$read_eval_print__9110.invoke (main.clj:437) 28 | clojure.main$repl$fn__9119.invoke (main.clj:458) 29 | clojure.main$repl.invokeStatic (main.clj:458) 30 | clojure.main$repl.doInvoke (main.clj:368) 31 | clojure.lang.RestFn.invoke (RestFn.java:1523) 32 | nrepl.middleware.interruptible_eval$evaluate.invokeStatic (interruptible_eval.clj:84) 33 | nrepl.middleware.interruptible_eval$evaluate.invoke (interruptible_eval.clj:56) 34 | nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970.invoke (interruptible_eval.clj:152) 35 | clojure.lang.AFn.run (AFn.java:22) 36 | nrepl.middleware.session$session_exec$main_loop__2036$fn__2040.invoke (session.clj:218) 37 | nrepl.middleware.session$session_exec$main_loop__2036.invoke (session.clj:217) 38 | clojure.lang.AFn.run (AFn.java:22) 39 | java.lang.Thread.run (Thread.java:829) 40 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/divide-by-zero.clojure.tagged-literal.txt: -------------------------------------------------------------------------------- 1 | #error { 2 | :cause "Divide by zero" 3 | :via 4 | [{:type java.lang.ArithmeticException 5 | :message "Divide by zero" 6 | :at [clojure.lang.Numbers divide "Numbers.java" 188]}] 7 | :trace 8 | [[clojure.lang.Numbers divide "Numbers.java" 188] 9 | [clojure.lang.Numbers divide "Numbers.java" 3901] 10 | [haystack.parser.throwable_test$fn__12333 invokeStatic "throwable_test.clj" 13] 11 | [haystack.parser.throwable_test$fn__12333 invoke "throwable_test.clj" 13] 12 | [clojure.lang.AFn applyToHelper "AFn.java" 152] 13 | [clojure.lang.AFn applyTo "AFn.java" 144] 14 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] 15 | [clojure.lang.Compiler$DefExpr eval "Compiler.java" 457] 16 | [clojure.lang.Compiler eval "Compiler.java" 7186] 17 | [clojure.lang.Compiler load "Compiler.java" 7640] 18 | [haystack.parser.throwable_test$eval12321 invokeStatic "form-init16320452348649927506.clj" 1] 19 | [haystack.parser.throwable_test$eval12321 invoke "form-init16320452348649927506.clj" 1] 20 | [clojure.lang.Compiler eval "Compiler.java" 7181] 21 | [clojure.lang.Compiler eval "Compiler.java" 7136] 22 | [clojure.core$eval invokeStatic "core.clj" 3202] 23 | [clojure.core$eval invoke "core.clj" 3198] 24 | [nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934 invoke "interruptible_eval.clj" 87] 25 | [clojure.lang.AFn applyToHelper "AFn.java" 152] 26 | [clojure.lang.AFn applyTo "AFn.java" 144] 27 | [clojure.core$apply invokeStatic "core.clj" 667] 28 | [clojure.core$with_bindings_STAR_ invokeStatic "core.clj" 1977] 29 | [clojure.core$with_bindings_STAR_ doInvoke "core.clj" 1977] 30 | [clojure.lang.RestFn invoke "RestFn.java" 425] 31 | [nrepl.middleware.interruptible_eval$evaluate$fn__1933 invoke "interruptible_eval.clj" 87] 32 | [clojure.main$repl$read_eval_print__9110$fn__9113 invoke "main.clj" 437] 33 | [clojure.main$repl$read_eval_print__9110 invoke "main.clj" 437] 34 | [clojure.main$repl$fn__9119 invoke "main.clj" 458] 35 | [clojure.main$repl invokeStatic "main.clj" 458] 36 | [clojure.main$repl doInvoke "main.clj" 368] 37 | [clojure.lang.RestFn invoke "RestFn.java" 1523] 38 | [nrepl.middleware.interruptible_eval$evaluate invokeStatic "interruptible_eval.clj" 84] 39 | [nrepl.middleware.interruptible_eval$evaluate invoke "interruptible_eval.clj" 56] 40 | [nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970 invoke "interruptible_eval.clj" 152] 41 | [clojure.lang.AFn run "AFn.java" 22] 42 | [nrepl.middleware.session$session_exec$main_loop__2036$fn__2040 invoke "session.clj" 218] 43 | [nrepl.middleware.session$session_exec$main_loop__2036 invoke "session.clj" 217] 44 | [clojure.lang.AFn run "AFn.java" 22] 45 | [java.lang.Thread run "Thread.java" 829]]} 46 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/divide-by-zero.java.txt: -------------------------------------------------------------------------------- 1 | java.lang.ArithmeticException: Divide by zero 2 | at clojure.lang.Numbers.divide(Numbers.java:188) 3 | at clojure.lang.Numbers.divide(Numbers.java:3901) 4 | at haystack.parser.throwable_test$fn__12333.invokeStatic(throwable_test.clj:13) 5 | at haystack.parser.throwable_test$fn__12333.invoke(throwable_test.clj:13) 6 | at clojure.lang.AFn.applyToHelper(AFn.java:152) 7 | at clojure.lang.AFn.applyTo(AFn.java:144) 8 | at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3706) 9 | at clojure.lang.Compiler$DefExpr.eval(Compiler.java:457) 10 | at clojure.lang.Compiler.eval(Compiler.java:7186) 11 | at clojure.lang.Compiler.load(Compiler.java:7640) 12 | at haystack.parser.throwable_test$eval12321.invokeStatic(form-init16320452348649927506.clj:1) 13 | at haystack.parser.throwable_test$eval12321.invoke(form-init16320452348649927506.clj:1) 14 | at clojure.lang.Compiler.eval(Compiler.java:7181) 15 | at clojure.lang.Compiler.eval(Compiler.java:7136) 16 | at clojure.core$eval.invokeStatic(core.clj:3202) 17 | at clojure.core$eval.invoke(core.clj:3198) 18 | at nrepl.middleware.interruptible_eval$evaluate$fn__1933$fn__1934.invoke(interruptible_eval.clj:87) 19 | at clojure.lang.AFn.applyToHelper(AFn.java:152) 20 | at clojure.lang.AFn.applyTo(AFn.java:144) 21 | at clojure.core$apply.invokeStatic(core.clj:667) 22 | at clojure.core$with_bindings_STAR_.invokeStatic(core.clj:1977) 23 | at clojure.core$with_bindings_STAR_.doInvoke(core.clj:1977) 24 | at clojure.lang.RestFn.invoke(RestFn.java:425) 25 | at nrepl.middleware.interruptible_eval$evaluate$fn__1933.invoke(interruptible_eval.clj:87) 26 | at clojure.main$repl$read_eval_print__9110$fn__9113.invoke(main.clj:437) 27 | at clojure.main$repl$read_eval_print__9110.invoke(main.clj:437) 28 | at clojure.main$repl$fn__9119.invoke(main.clj:458) 29 | at clojure.main$repl.invokeStatic(main.clj:458) 30 | at clojure.main$repl.doInvoke(main.clj:368) 31 | at clojure.lang.RestFn.invoke(RestFn.java:1523) 32 | at nrepl.middleware.interruptible_eval$evaluate.invokeStatic(interruptible_eval.clj:84) 33 | at nrepl.middleware.interruptible_eval$evaluate.invoke(interruptible_eval.clj:56) 34 | at nrepl.middleware.interruptible_eval$interruptible_eval$fn__1966$fn__1970.invoke(interruptible_eval.clj:152) 35 | at clojure.lang.AFn.run(AFn.java:22) 36 | at nrepl.middleware.session$session_exec$main_loop__2036$fn__2040.invoke(session.clj:218) 37 | at nrepl.middleware.session$session_exec$main_loop__2036.invoke(session.clj:217) 38 | at clojure.lang.AFn.run(AFn.java:22) 39 | at java.base/java.lang.Thread.run(Thread.java:829) 40 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/short.aviso.txt: -------------------------------------------------------------------------------- 1 | java.lang.Thread.run Thread.java: 829 2 | clojure.lang.ExceptionInfo: BOOM-1 3 | boom: "1" 4 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/short.clojure.repl.txt: -------------------------------------------------------------------------------- 1 | ExceptionInfo BOOM-1 {:boom "1"} 2 | java.lang.Thread.run (Thread.java:829) 3 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/short.clojure.stacktrace.txt: -------------------------------------------------------------------------------- 1 | clojure.lang.ExceptionInfo: BOOM-1 2 | {:boom "1"} 3 | at java.lang.Thread.run (Thread.java:829) 4 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/short.clojure.tagged-literal.println.txt: -------------------------------------------------------------------------------- 1 | #error { 2 | :cause BOOM-1 3 | :data {:boom 1} 4 | :via 5 | [{:type clojure.lang.ExceptionInfo 6 | :message BOOM-1 7 | :data {:boom 1} 8 | :at [java.lang.Thread run Thread.java 829]}] 9 | :trace 10 | [[java.lang.Thread run Thread.java 829]]} 11 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/short.clojure.tagged-literal.txt: -------------------------------------------------------------------------------- 1 | #error { 2 | :cause "BOOM-1" 3 | :data {:boom "1"} 4 | :via 5 | [{:type clojure.lang.ExceptionInfo 6 | :message "BOOM-1" 7 | :data {:boom "1"} 8 | :at [java.lang.Thread run "Thread.java" 829]}] 9 | :trace 10 | [[java.lang.Thread run "Thread.java" 829]]} 11 | #error { 12 | :cause "BOOM-1" 13 | :data {:boom "1"} 14 | :via 15 | [{:type clojure.lang.ExceptionInfo 16 | :message "BOOM-1" 17 | :data {:boom "1"} 18 | :at [java.lang.Thread run "Thread.java" 829]}] 19 | :trace 20 | [[java.lang.Thread run "Thread.java" 829]]} 21 | -------------------------------------------------------------------------------- /test-resources/haystack/parser/short.java.txt: -------------------------------------------------------------------------------- 1 | clojure.lang.ExceptionInfo: BOOM-1 {:boom "1"} 2 | at java.base/java.lang.Thread.run(Thread.java:829) 3 | -------------------------------------------------------------------------------- /test/haystack/analyzer_test.clj: -------------------------------------------------------------------------------- 1 | (ns haystack.analyzer-test 2 | (:require 3 | [clojure.java.io :as io] 4 | [clojure.test :refer [are deftest is testing]] 5 | [haystack.analyzer :as sut] 6 | [haystack.parser :as parser] 7 | [orchard.spec :as spec])) 8 | 9 | ;; # Utils 10 | 11 | (defn- fixture [resource] 12 | (str (io/file "haystack" "parser" (str (name resource) ".txt")))) 13 | 14 | (defn- read-fixture [name] 15 | (some-> name fixture io/resource slurp)) 16 | 17 | (defn- analyze-resource [name] 18 | (some-> name read-fixture parser/parse sut/analyze)) 19 | 20 | (defn causes 21 | [form] 22 | (sut/analyze 23 | (try (eval form) 24 | (catch Exception e 25 | e)) 26 | sut/pprint)) 27 | 28 | (defn stack-frames 29 | [form] 30 | (-> (try (eval form) 31 | (catch Exception e 32 | e)) 33 | sut/analyze first :stacktrace)) 34 | 35 | ;; ## Test fixtures 36 | 37 | (def form1 '(throw (ex-info "oops" {:x 1} (ex-info "cause" {:y 2})))) 38 | (def form2 '(let [^long num "2"] ;; Type hint to make eastwood happy 39 | (defn oops [] (+ 1 num)) 40 | (oops))) 41 | (def form3 '(not-defined)) 42 | (def form4 '(divi 1 0)) 43 | 44 | (def frames1 (stack-frames form1)) 45 | (def frames2 (stack-frames form2)) 46 | (def frames4 (stack-frames form4)) 47 | (def causes1 (causes form1)) 48 | (def causes2 (causes form2)) 49 | (def causes3 (causes form3)) 50 | 51 | (def email-regex 52 | #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") 53 | 54 | (def broken-musk {::first-name "Elon" 55 | ::last-name "Musk" 56 | ::email "n/a"}) 57 | 58 | (def broken-musk-causes 59 | (when (resolve (symbol "clojure.spec.alpha" "get-spec")) 60 | (eval `(do 61 | ~(cond 62 | spec/clojure-spec-alpha? 63 | (require '[clojure.spec.alpha :as s]) 64 | spec/clojure-spec? 65 | (require '[clojure.spec :as s])) 66 | (s/check-asserts true) 67 | (s/def ::email-type 68 | (s/and string? #(re-matches email-regex %))) 69 | 70 | (s/def ::first-name string?) 71 | (s/def ::last-name string?) 72 | (s/def ::email ::email-type) 73 | 74 | (s/def ::person 75 | (s/keys :req [::first-name ::last-name ::email] 76 | :opt [::phone])) 77 | 78 | (causes `(s/assert ::person broken-musk)))))) 79 | 80 | ;; ## Tests 81 | 82 | (when spec/spec? 83 | (deftest spec-assert-stacktrace-test 84 | (testing "Spec assert components" 85 | (is (= 1 (count broken-musk-causes))) 86 | (is (:stacktrace (first broken-musk-causes))) 87 | (is (:message (first broken-musk-causes))) 88 | (is (:spec (first broken-musk-causes)))) 89 | 90 | (testing "Spec assert data components" 91 | (let [spec (:spec (first broken-musk-causes))] 92 | (is (:spec spec)) 93 | (is (string? (:value spec))) 94 | (is (= 1 (count (:problems spec)))))) 95 | 96 | (testing "Spec assert problems components" 97 | (let [probs (->> broken-musk-causes first :spec :problems first)] 98 | (is (:in probs)) 99 | (is (:val probs)) 100 | (is (:predicate probs)) 101 | (is (:spec probs)) 102 | (is (:at probs)))))) 103 | 104 | (deftest stacktrace-frames-test 105 | (testing "File types" 106 | ;; Should be clj and java only. 107 | (let [ts1 (group-by :type frames1) 108 | ts2 (group-by :type frames2)] 109 | (is (= #{:clj :java} (set (keys ts1)))) 110 | (is (= #{:clj :java} (set (keys ts2)))))) 111 | (testing "Full file mappings" 112 | (is (every? 113 | #(-> % ^String (:file-url) (.endsWith "!/clojure/core.clj")) 114 | (filter #(= "clojure.core" (:ns %)) 115 | frames1))) 116 | (is (->> (filter #(some-> % ^String (:ns) (.contains "cider")) frames1) 117 | (remove (comp #{"invoke" "invokeStatic"} :method)) ;; these don't have a file-url 118 | (every? 119 | #(-> % ^String (:file-url) (.startsWith "file:/")))))) 120 | (testing "Clojure ns, fn, and var" 121 | ;; All Clojure frames should have non-nil :ns :fn and :var attributes. 122 | (is (every? #(every? identity ((juxt :ns :fn :var) %)) 123 | (filter #(= :clj (:type %)) frames1))) 124 | (is (every? #(every? identity ((juxt :ns :fn :var) %)) 125 | (filter #(= :clj (:type %)) frames2)))) 126 | (testing "Clojure name demunging" 127 | ;; Clojure fn names should be free of munging characters. 128 | (is (not-any? #(re-find #"[_$]|(--\d+)" (:fn %)) 129 | (filter :fn frames1))) 130 | (is (not-any? #(re-find #"[_$]|(--\d+)" (:fn %)) 131 | (filter :fn frames2))))) 132 | 133 | (deftest stacktrace-frame-flags-test 134 | (testing "Flags" 135 | (testing "for file type" 136 | ;; Every frame should have its file type added as a flag. 137 | (is (every? #(contains? (:flags %) (:type %)) frames1)) 138 | (is (every? #(contains? (:flags %) (:type %)) frames2))) 139 | (testing "for tooling" 140 | ;; Tooling frames are classes named with 'clojure' or 'nrepl', 141 | ;; or are java thread runners...or calls made from these. 142 | (is (some #(re-find #"(clojure|nrepl|run)" (:name %)) 143 | (filter (comp :tooling :flags) frames1))) 144 | (is (some #(re-find #"(clojure|nrepl|run)" (:name %)) 145 | (filter (comp :tooling :flags) frames2)))) 146 | (testing "for project" 147 | (is (seq (filter (comp :project :flags) frames4)))) 148 | (testing "for duplicate frames" 149 | ;; Index frames. For all frames flagged as :dup, the frame above it in 150 | ;; the stack (index i - 1) should be substantially the same source info. 151 | (let [ixd1 (zipmap (iterate inc 0) frames1) 152 | ixd2 (zipmap (iterate inc 0) frames2) 153 | dup? #(or (= (:name %1) (:name %2)) 154 | (and (= (:file %1) (:file %2)) 155 | (= (:line %1) (:line %2))))] 156 | (is (every? (fn [[i v]] (dup? v (get ixd1 (dec ^long i)))) 157 | (filter (comp :dup :flags val) ixd1))) 158 | (is (every? (fn [[i v]] (dup? v (get ixd2 (dec ^long i)))) 159 | (filter (comp :dup :flags val) ixd2))))))) 160 | 161 | (deftest exception-causes-test 162 | (testing "Exception cause unrolling" 163 | (is (= 2 (count causes1))) 164 | (is (= 1 (count causes2)))) 165 | (testing "Exception data" 166 | ;; If ex-data is present, the cause should have a :data attribute. 167 | (is (:data (first causes1))) 168 | (is (not (:data (first causes2)))))) 169 | 170 | (deftest ex-data-filtering-test 171 | (is (= {:a :b :c :d} 172 | (#'sut/filtered-ex-data {:a :b :c :d :repl-env :e})))) 173 | 174 | (deftest cause-data-pretty-printing-test 175 | (testing "print-length" 176 | (is (= "{:a (0 1 2 ...)}" 177 | (:data (first (sut/analyze (ex-info "" {:a (range)}) 178 | (fn [value writer] 179 | (sut/pprint value writer {:length 3})))))))) 180 | (testing "print-level" 181 | (is (= "{:a {#}}" 182 | (:data (first (sut/analyze (ex-info "" {:a {:b {:c {:d {:e nil}}}}}) 183 | (fn [value writer] 184 | (sut/pprint value writer {:level 3}))))))))) 185 | 186 | (deftest compilation-errors-test 187 | (let [clojure-version ((juxt :major :minor) *clojure-version*)] 188 | (if (< (compare clojure-version [1 10]) 0) 189 | ;; 1.8 / 1.9 190 | (is (re-find #"Unable to resolve symbol: not-defined in this context" 191 | (:message (first causes3)))) 192 | 193 | ;; 1.10+ 194 | (is (re-find #"Syntax error compiling at \(haystack/analyzer_test\.clj:" 195 | (:message (first causes3)))))) 196 | 197 | (testing "extract-location" 198 | (is (= {:class "clojure.lang.Compiler$CompilerException" 199 | :message "java.lang.RuntimeException: Unable to resolve symbol: foo in this context" 200 | :file "/foo/bar/baz.clj" 201 | :file-url nil 202 | :path "/foo/bar/baz.clj" 203 | :line 1 204 | :column 42} 205 | (#'sut/extract-location {:class "clojure.lang.Compiler$CompilerException" 206 | :message "java.lang.RuntimeException: Unable to resolve symbol: foo in this context, compiling:(/foo/bar/baz.clj:1:42)"}))) 207 | 208 | (is (= {:class "clojure.lang.Compiler$CompilerException" 209 | :message "java.lang.NegativeArraySizeException" 210 | :file "/foo/bar/baz.clj" 211 | :file-url nil 212 | :path "/foo/bar/baz.clj" 213 | :line 1 214 | :column 42} 215 | (#'sut/extract-location {:class "clojure.lang.Compiler$CompilerException" 216 | :message "java.lang.NegativeArraySizeException, compiling:(/foo/bar/baz.clj:1:42)"})))) 217 | (testing "extract-location with location-data already present" 218 | (is (= {:class "clojure.lang.Compiler$CompilerException" 219 | :location {:clojure.error/line 1 220 | :clojure.error/column 42 221 | :clojure.error/source "/foo/bar/baz.clj" 222 | :clojure.error/phase :macroexpand 223 | :clojure.error/symbol 'clojure.core/let}, 224 | :message "Syntax error macroexpanding clojure.core/let at (1:1)." 225 | :file "/foo/bar/baz.clj" 226 | :file-url nil 227 | :path "/foo/bar/baz.clj" 228 | :line 1 229 | :column 42} 230 | (#'sut/extract-location {:class "clojure.lang.Compiler$CompilerException" 231 | :location {:clojure.error/line 1 232 | :clojure.error/column 42 233 | :clojure.error/source "/foo/bar/baz.clj" 234 | :clojure.error/phase :macroexpand 235 | :clojure.error/symbol 'clojure.core/let} 236 | :message "Syntax error macroexpanding clojure.core/let at (1:1)."}))))) 237 | 238 | (deftest analyze-cause-test 239 | (testing "check that location-data is returned" 240 | (let [e (ex-info "wat?" {:clojure.error/line 1 241 | :clojure.error/column 42 242 | :clojure.error/source "/foo/bar/baz.clj" 243 | :clojure.error/phase :macroexpand 244 | :clojure.error/symbol 'clojure.core/let}) 245 | cause (first (sut/analyze e (fn [v _] v)))] 246 | (is (= {:clojure.error/line 1 247 | :clojure.error/column 42 248 | :clojure.error/source "/foo/bar/baz.clj" 249 | :clojure.error/phase :macroexpand 250 | :clojure.error/symbol 'clojure.core/let} 251 | (:location cause)))))) 252 | 253 | (deftest ns-common-prefix*-test 254 | (are [input expected] (= expected 255 | (#'sut/ns-common-prefix* input)) 256 | [] {:valid false :common :missing} 257 | '[a b] {:valid false :common :missing} 258 | '[a.c b.c] {:valid false :common :missing} 259 | ::not-a-coll {:valid false :common :error} 260 | 261 | ;; single-segment namespaces are considered to never have a common part: 262 | '[user] {:valid false :common :missing} 263 | '[dev] {:valid false :common :missing} 264 | '[test-runner] {:valid false :common :missing} 265 | 266 | '[a.a] {:valid true :common "a.a"} 267 | '[a.a a.b] {:valid true :common "a"} 268 | '[a.a.b a.a.c] {:valid true :common "a.a"} 269 | 270 | ;; single-segment namespaces cannot foil the rest of the calculation: 271 | '[dev user test-runner a.a] {:valid true :common "a.a"} 272 | '[dev user test-runner a.a a.b] {:valid true :common "a"} 273 | '[dev user test-runner a.a.b a.a.c] {:valid true :common "a.a"})) 274 | 275 | (deftest test-analyze-aviso 276 | (let [causes (analyze-resource :boom.aviso)] 277 | (is (= 3 (count causes))) 278 | (testing "first cause" 279 | (let [{:keys [class data message stacktrace]} (first causes)] 280 | (testing "class" 281 | (is (= "clojure.lang.ExceptionInfo" class))) 282 | (testing "message" 283 | (is (= "BOOM-1" message))) 284 | (testing "data" 285 | (is (= "{:boom \"1\"}" data))) 286 | (testing "stacktrace" 287 | (is (= 7 (count stacktrace))) 288 | (testing "first frame" 289 | (is (= {:type :unknown, :flags #{:unknown}} 290 | (dissoc (first stacktrace) :file-url)))) 291 | (testing "last frame" 292 | (is (= {:fn "fn" 293 | :method "fn" 294 | :ns "nrepl.middleware.interruptible-eval" 295 | :name "nrepl.middleware.interruptible-eval/fn" 296 | :file "interruptible_eval.clj" 297 | :type :clj 298 | :line 87 299 | :var "nrepl.middleware.interruptible-eval/fn" 300 | :class "nrepl.middleware.interruptible-eval" 301 | :flags #{:tooling :clj}} 302 | (dissoc (last stacktrace) :file-url))))))) 303 | (testing "second cause" 304 | (let [{:keys [class data message stacktrace]} (second causes)] 305 | (testing "class" 306 | (is (= "clojure.lang.ExceptionInfo" class))) 307 | (testing "message" 308 | (is (= "BOOM-2" message))) 309 | (testing "data" 310 | (is (= "{:boom \"2\"}" data))) 311 | (testing "stacktrace" 312 | (is (= 0 (count stacktrace)))))) 313 | (testing "thrid cause" 314 | (let [{:keys [class data message stacktrace]} (nth causes 2)] 315 | (testing "class" 316 | (is (= "clojure.lang.ExceptionInfo" class))) 317 | (testing "message" 318 | (is (= "BOOM-3" message))) 319 | (testing "data" 320 | (is (= "{:boom \"3\"}" data))) 321 | (testing "stacktrace" 322 | (is (= 0 (count stacktrace)))))))) 323 | 324 | (deftest test-analyze-clojure-tagged-literal 325 | (let [causes (analyze-resource :boom.clojure.tagged-literal)] 326 | (is (= 3 (count causes))) 327 | (testing "first cause" 328 | (let [{:keys [class data message stacktrace]} (first causes)] 329 | (testing "class" 330 | (is (= "clojure.lang.ExceptionInfo" class))) 331 | (testing "message" 332 | (is (= "BOOM-1" message))) 333 | (testing "data" 334 | (is (= "{:boom \"1\"}" data))) 335 | (testing "stacktrace" 336 | (is (= 36 (count stacktrace))) 337 | (testing "first frame" 338 | (is (= {:name "clojure.lang.AFn/applyToHelper" 339 | :file "AFn.java" 340 | :line 156 341 | :class "clojure.lang.AFn" 342 | :method "applyToHelper" 343 | :type :java 344 | :flags #{:java :tooling}} 345 | (dissoc (first stacktrace) :file-url)))) 346 | (testing "last frame" 347 | (is (= {:name "java.lang.Thread/run" 348 | :file "Thread.java" 349 | :line 829 350 | :class "java.lang.Thread" 351 | :method "run" 352 | :type :java 353 | :flags #{:java :tooling}} 354 | (dissoc (last stacktrace) :file-url))))))) 355 | (testing "second cause" 356 | (let [{:keys [class data message stacktrace]} (second causes)] 357 | (testing "class" 358 | (is (= "clojure.lang.ExceptionInfo" class))) 359 | (testing "message" 360 | (is (= "BOOM-2" message))) 361 | (testing "data" 362 | (is (= "{:boom \"2\"}" data))) 363 | (testing "stacktrace" 364 | (is (= 1 (count stacktrace))) 365 | (testing "first frame" 366 | (is (= {:name "clojure.lang.AFn/applyToHelper" 367 | :file "AFn.java" 368 | :line 160 369 | :class "clojure.lang.AFn" 370 | :method "applyToHelper" 371 | :type :java 372 | :flags #{:java :tooling}} 373 | (dissoc (first stacktrace) :file-url))))))) 374 | (testing "third cause" 375 | (let [{:keys [class data message stacktrace]} (nth causes 2)] 376 | (testing "class" 377 | (is (= "clojure.lang.ExceptionInfo" class))) 378 | (testing "message" 379 | (is (= "BOOM-3" message))) 380 | (testing "data" 381 | (is (= "{:boom \"3\"}" data))) 382 | (testing "stacktrace" 383 | (is (= 1 (count stacktrace))) 384 | (testing "first frame" 385 | (is (= {:name "clojure.lang.AFn/applyToHelper" 386 | :file "AFn.java" 387 | :line 156 388 | :class "clojure.lang.AFn" 389 | :method "applyToHelper" 390 | :type :java 391 | :flags #{:java :tooling}} 392 | (dissoc (first stacktrace) :file-url))))))))) 393 | 394 | (deftest test-analyze-short-clojure-tagged-literal-println 395 | (let [causes (analyze-resource :short.clojure.tagged-literal.println)] 396 | (is (= 1 (count causes))) 397 | (testing "first cause" 398 | (let [{:keys [class data message stacktrace]} (first causes)] 399 | (testing "class" 400 | (is (= "clojure.lang.ExceptionInfo" class))) 401 | (testing "message" 402 | (is (= "BOOM-1" message))) 403 | (testing "data" 404 | (is (= "{:boom 1}" data))) 405 | (testing "stacktrace" 406 | (is (= 1 (count stacktrace))) 407 | (testing "first frame" 408 | (is (= {:name "java.lang.Thread/run" 409 | :file "Thread.java" 410 | :line 829 411 | :class "java.lang.Thread" 412 | :method "run" 413 | :type :java 414 | :flags #{:java :tooling}} 415 | (dissoc (first stacktrace) :file-url))))))))) 416 | 417 | (deftest test-analyze-java 418 | (let [causes (analyze-resource :boom.java)] 419 | (is (= 3 (count causes))) 420 | (testing "first cause" 421 | (let [{:keys [class data message stacktrace]} (first causes)] 422 | (testing "class" 423 | (is (= "clojure.lang.ExceptionInfo" class))) 424 | (testing "message" 425 | (is (= "BOOM-1" message))) 426 | (testing "data" 427 | (is (= "{:boom \"1\"}" data))) 428 | (testing "stacktrace" 429 | (is (= 34 (count stacktrace))) 430 | (testing "first frame" 431 | (is (= {:name "clojure.lang.AFn/applyToHelper" 432 | :file "AFn.java" 433 | :line 160 434 | :class "clojure.lang.AFn" 435 | :method "applyToHelper" 436 | :type :java 437 | :flags #{:java :tooling}} 438 | (dissoc (first stacktrace) :file-url)))) 439 | (testing "last frame" 440 | (is (= {:name "java.lang.Thread/run" 441 | :file "Thread.java" 442 | :line 829 443 | :class "java.lang.Thread" 444 | :method "run" 445 | :type :java 446 | :flags #{:java :tooling}} 447 | (dissoc (last stacktrace) :file-url))))))) 448 | (testing "second cause" 449 | (let [{:keys [class data message stacktrace]} (second causes)] 450 | (testing "class" 451 | (is (= "clojure.lang.ExceptionInfo" class))) 452 | (testing "message" 453 | (is (= "BOOM-2" message))) 454 | (testing "data" 455 | (is (= "{:boom \"2\"}" data))) 456 | (testing "stacktrace" 457 | (is (= 4 (count stacktrace))) 458 | (testing "first frame" 459 | (is (= {:name "clojure.lang.AFn/applyToHelper" 460 | :file "AFn.java" 461 | :line 160 462 | :class "clojure.lang.AFn" 463 | :method "applyToHelper" 464 | :type :java 465 | :flags #{:java :tooling}} 466 | (dissoc (first stacktrace) :file-url)))) 467 | (testing "last frame" 468 | (is (= {:name "clojure.lang.Compiler$InvokeExpr/eval" 469 | :file "Compiler.java" 470 | :line 3705 471 | :class "clojure.lang.Compiler$InvokeExpr" 472 | :method "eval" 473 | :type :java 474 | :flags #{:dup :tooling :java}} 475 | (dissoc (last stacktrace) :file-url))))))) 476 | (testing "third cause" 477 | (let [{:keys [class data message stacktrace]} (nth causes 2 nil)] 478 | (testing "class" 479 | (is (= "clojure.lang.ExceptionInfo" class))) 480 | (testing "message" 481 | (is (= "BOOM-3" message))) 482 | (testing "data" 483 | (is (= "{:boom \"3\"}" data))) 484 | (testing "stacktrace" 485 | (is (= 4 (count stacktrace))) 486 | (testing "first frame" 487 | (is (= {:name "clojure.lang.AFn/applyToHelper" 488 | :file "AFn.java" 489 | :line 156 490 | :class "clojure.lang.AFn" 491 | :method "applyToHelper" 492 | :type :java 493 | :flags #{:java :tooling}} 494 | (dissoc (first stacktrace) :file-url)))) 495 | (testing "last frame" 496 | (is (= {:name "clojure.lang.Compiler$InvokeExpr/eval" 497 | :file "Compiler.java" 498 | :line 3705 499 | :class "clojure.lang.Compiler$InvokeExpr" 500 | :method "eval" 501 | :type :java 502 | :flags #{:dup :tooling :java}} 503 | (dissoc (last stacktrace) :file-url))))))))) 504 | 505 | (deftest test-analyze-throwable 506 | (let [causes (sut/analyze 507 | '{:via 508 | [{:type clojure.lang.ExceptionInfo 509 | :message "BOOM-1" 510 | :data {:boom "1"} 511 | :at [clojure.lang.AFn applyToHelper "AFn.java" 160]} 512 | {:type clojure.lang.ExceptionInfo 513 | :message "BOOM-2" 514 | :data {:boom "2"} 515 | :at [clojure.lang.AFn applyToHelper "AFn.java" 160]} 516 | {:type clojure.lang.ExceptionInfo 517 | :message "BOOM-3" 518 | :data {:boom "3"} 519 | :at [clojure.lang.AFn applyToHelper "AFn.java" 156]}] 520 | :trace 521 | [[clojure.lang.AFn applyToHelper "AFn.java" 156] 522 | [clojure.lang.AFn applyTo "AFn.java" 144] 523 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] 524 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705] 525 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705]] 526 | :cause "BOOM-3" 527 | :data {:boom "3"} 528 | :stacktrace-type :throwable})] 529 | (is (= 3 (count causes))) 530 | (testing "first cause" 531 | (let [{:keys [class data message stacktrace]} (first causes)] 532 | (testing "class" 533 | (is (= "clojure.lang.ExceptionInfo" class))) 534 | (testing "message" 535 | (is (= "BOOM-1" message))) 536 | (testing "data" 537 | (is (= "{:boom \"1\"}" data))) 538 | (testing "stacktrace" 539 | (is (= 5 (count stacktrace))) 540 | (testing "first frame" 541 | (is (= {:name "clojure.lang.AFn/applyToHelper" 542 | :file "AFn.java" 543 | :line 156 544 | :class "clojure.lang.AFn" 545 | :method "applyToHelper" 546 | :type :java 547 | :flags #{:java :tooling}} 548 | (dissoc (nth stacktrace 0) :file-url)))) 549 | (testing "2nd frame" 550 | (is (= {:class "clojure.lang.AFn" 551 | :file "AFn.java" 552 | :flags #{:java :tooling} 553 | :line 144 554 | :method "applyTo" 555 | :name "clojure.lang.AFn/applyTo" 556 | :type :java} 557 | (dissoc (nth stacktrace 1) :file-url))))))) 558 | (testing "second cause" 559 | (let [{:keys [class data message stacktrace]} (second causes)] 560 | (testing "class" 561 | (is (= "clojure.lang.ExceptionInfo" class))) 562 | (testing "message" 563 | (is (= "BOOM-2" message))) 564 | (testing "data" 565 | (is (= "{:boom \"2\"}" data))) 566 | (testing "stacktrace" 567 | (is (= 1 (count stacktrace))) 568 | (testing "first frame" 569 | (is (= {:name "clojure.lang.AFn/applyToHelper" 570 | :file "AFn.java" 571 | :class "clojure.lang.AFn" 572 | :line 160 573 | :method "applyToHelper" 574 | :type :java 575 | :flags #{:java :tooling}} 576 | (dissoc (nth stacktrace 0) :file-url))))))) 577 | (testing "third cause" 578 | (let [{:keys [class data message stacktrace]} (nth causes 2 nil)] 579 | (testing "class" 580 | (is (= "clojure.lang.ExceptionInfo" class))) 581 | (testing "message" 582 | (is (= "BOOM-3" message))) 583 | (testing "data" 584 | (is (= "{:boom \"3\"}" data))) 585 | (testing "stacktrace" 586 | (is (= 1 (count stacktrace))) 587 | (testing "first frame" 588 | (is (= {:name "clojure.lang.AFn/applyToHelper" 589 | :file "AFn.java" 590 | :class "clojure.lang.AFn" 591 | :line 156 592 | :method "applyToHelper" 593 | :type :java 594 | :flags #{:java :tooling}} 595 | (dissoc (nth stacktrace 0) :file-url)))))))) 596 | 597 | (let [{:keys [major minor]} *clojure-version*] 598 | (when-not (and (= 1 major) 599 | (< (long minor) 10)) 600 | (testing "Includes a `:phase` for the causes that include it" 601 | (is (= [:macro-syntax-check nil] 602 | (->> (try 603 | (eval '(let [1])) 604 | (catch Throwable e 605 | (sut/analyze e))) 606 | (map :phase))))) 607 | (testing "Does not include `:phase` for vanilla runtime exceptions" 608 | (is (= [nil] 609 | (->> (try 610 | (throw (ex-info "" {})) 611 | (catch Throwable e 612 | (sut/analyze e))) 613 | (map :phase))))))) 614 | 615 | (testing "`:compile-like`" 616 | (testing "For non-existing fields" 617 | (is (= ["true"] 618 | (->> (try 619 | (eval '(.-foo "")) 620 | (catch Throwable e 621 | (sut/analyze e))) 622 | (map :compile-like))))) 623 | (testing "For non-existing methods" 624 | (is (= ["true"] 625 | (->> (try 626 | (eval '(-> "" (.foo 1 2))) 627 | (catch Throwable e 628 | (sut/analyze e))) 629 | (map :compile-like))))) 630 | (testing "For vanilla exceptions" 631 | (is (= ["false"] 632 | (->> (try 633 | (throw (ex-info "." {})) 634 | (catch Throwable e 635 | (sut/analyze e))) 636 | (map :compile-like))))) 637 | (testing "For vanilla `IllegalArgumentException`s" 638 | (is (= ["false"] 639 | (->> (try 640 | (throw (IllegalArgumentException. "foo")) 641 | (catch Throwable e 642 | (sut/analyze e))) 643 | (map :compile-like))))) 644 | (testing "For exceptions with a `:phase`" 645 | (is (#{["false" "false"] ;; normal expectation 646 | ["false"]} ;; clojure 1.8 647 | (->> (try 648 | (eval '(let [1])) 649 | (catch Throwable e 650 | (sut/analyze e))) 651 | (map :compile-like))))))) 652 | 653 | (deftest tooling-frame-name? 654 | (are [frame-name expected] (testing frame-name 655 | (is (= expected 656 | (#'sut/tooling-frame-name? frame-name false))) 657 | true) 658 | "cider.foo" true 659 | "refactor-nrepl.middleware/wrap-refactor" true 660 | "shadow.cljs.devtools.server.nrepl/shadow-inint" true 661 | "acider.foo" false 662 | ;; `+` is "application" level, should not be hidden: 663 | "clojure.core/+" false 664 | ;; `apply` typically is internal, should be hidden: 665 | "clojure.core/apply" true 666 | "clojure.core/binding-conveyor-fn/fn" true 667 | "clojure.core.protocols/iter-reduce" true 668 | "clojure.core/eval" true 669 | "clojure.core/with-bindings*" true 670 | "clojure.lang.MultiFn/invoke" true 671 | "clojure.lang.LazySeq/sval" true 672 | "clojure.lang.Var/invoke" true 673 | "clojure.lang.AFn/applyTo" true 674 | "clojure.lang.AFn/applyToHelper" true 675 | "clojure.lang.RestFn/invoke" true 676 | "clojure.main/repl" true 677 | "clojure.main$repl$read_eval_print__9234$fn__9235/invoke" true 678 | "nrepl.foo" true 679 | "nrepl.middleware.interruptible_eval$evaluate/invokeStatic" true 680 | "anrepl.foo" false 681 | ;; important case - `Numbers` is relevant, should not be hidden: 682 | "clojure.lang.Numbers/divide" false) 683 | 684 | (is (not (#'sut/tooling-frame-name? "java.lang.Thread/run" false))) 685 | (is (#'sut/tooling-frame-name? "java.lang.Thread/run" true))) 686 | 687 | (deftest flag-tooling 688 | (is (= [{:name "cider.foo", :flags #{:tooling}} 689 | {:name "java.lang.Thread/run"} ;; does not get the flag because it's not the root frame 690 | {:name "don't touch me 1"} 691 | {:name "nrepl.foo", :flags #{:tooling}} 692 | {:name "clojure.lang.RestFn/invoke", :flags #{:tooling}} 693 | {:name "don't touch me 2"} 694 | ;; gets the flag because it's the root frame: 695 | {:name "java.lang.Thread/run", :flags #{:tooling}}] 696 | (#'sut/flag-tooling [{:name "cider.foo"} 697 | {:name "java.lang.Thread/run"} 698 | {:name "don't touch me 1"} 699 | {:name "nrepl.foo"} 700 | {:name "clojure.lang.RestFn/invoke"} 701 | {:name "don't touch me 2"} 702 | {:name "java.lang.Thread/run"}])) 703 | "Adds the flag when appropiate, leaving other entries untouched") 704 | 705 | (let [frames [{:name "don't touch me"} 706 | {:name "java.util.concurrent.FutureTask/run"} 707 | {:name "java.util.concurrent.ThreadPoolExecutor/runWorker"} 708 | {:name "java.util.concurrent.ThreadPoolExecutor$Worker/run"}]] 709 | (is (= [{:name "don't touch me"} 710 | {:name "java.util.concurrent.FutureTask/run", :flags #{:tooling}} 711 | {:name "java.util.concurrent.ThreadPoolExecutor/runWorker", :flags #{:tooling}} 712 | {:name "java.util.concurrent.ThreadPoolExecutor$Worker/run", :flags #{:tooling}}] 713 | (#'sut/flag-tooling frames)) 714 | "Three j.u.concurrent frames get the flag if they're at the bottom") 715 | (is (= [{:name "don't touch me"} 716 | {:name "java.util.concurrent.FutureTask/run"} 717 | {:name "java.util.concurrent.ThreadPoolExecutor/runWorker"} 718 | {:name "java.util.concurrent.ThreadPoolExecutor$Worker/run"} 719 | {:name "x"}] 720 | (#'sut/flag-tooling (conj frames {:name "x"}))) 721 | "The j.u.concurrent frames don't get the flag if they're not at the bottom"))) 722 | -------------------------------------------------------------------------------- /test/haystack/parser/aviso_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.aviso-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [haystack.parser.aviso :as parser] 4 | [haystack.parser.test :as test] 5 | [haystack.parser.test.fixtures :refer [fixtures]])) 6 | 7 | (defn- parse [s] 8 | (parser/parse-stacktrace s)) 9 | 10 | (deftest parse-stacktrace-boom-test 11 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :boom.aviso))] 12 | (testing ":stacktrace-type" 13 | (is (= :aviso stacktrace-type))) 14 | (testing "throwable cause" 15 | (is (= "BOOM-3" cause))) 16 | (testing ":data" 17 | (is (= {:boom "3"} data))) 18 | (testing ":via" 19 | (is (= 3 (count via))) 20 | (testing "first cause" 21 | (let [{:keys [at data message type]} (nth via 0)] 22 | (is (nil? at)) 23 | (is (= {:boom "1"} data)) 24 | (is (= "BOOM-1" message)) 25 | (is (= 'clojure.lang.ExceptionInfo type)))) 26 | (testing "second cause" 27 | (let [{:keys [at data message type]} (nth via 1)] 28 | (is (nil? at)) 29 | (is (= {:boom "2"} data)) 30 | (is (= "BOOM-2" message)) 31 | (is (= 'clojure.lang.ExceptionInfo type)))) 32 | (testing "third cause" 33 | (let [{:keys [at data message type]} (nth via 2)] 34 | (is (nil? at)) 35 | (is (= {:boom "3"} data)) 36 | (is (= "BOOM-3" message)) 37 | (is (= 'clojure.lang.ExceptionInfo type))))) 38 | (testing ":trace" 39 | (doseq [element trace] 40 | (is (test/stacktrace-element? element) (pr-str element))) 41 | (testing "first frame" 42 | (is (= '[haystack.parser.throwable-test eval12321 "REPL Input"] (first trace)))) 43 | (testing "last frame" 44 | (is (= '[nrepl.middleware.interruptible-eval evaluate/fn "interruptible_eval.clj" 87] (last trace))))))) 45 | 46 | (deftest parse-stacktrace-boom-full-test 47 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :boom.aviso.full))] 48 | (testing ":stacktrace-type" 49 | (is (= :aviso stacktrace-type))) 50 | (testing "throwable cause" 51 | (is (= "BOOM-3" cause))) 52 | (testing ":data" 53 | (is (= {:boom "3"} data))) 54 | (testing ":via" 55 | (is (= 3 (count via))) 56 | (testing "first cause" 57 | (let [{:keys [at data message type]} (nth via 0)] 58 | (is (nil? at)) 59 | (is (= {:boom "1"} data)) 60 | (is (= "BOOM-1" message)) 61 | (is (= 'clojure.lang.ExceptionInfo type)))) 62 | (testing "second cause" 63 | (let [{:keys [at data message type]} (nth via 1)] 64 | (is (nil? at)) 65 | (is (= {:boom "2"} data)) 66 | (is (= "BOOM-2" message)) 67 | (is (= 'clojure.lang.ExceptionInfo type)))) 68 | (testing "third cause" 69 | (let [{:keys [at data message type]} (nth via 2)] 70 | (is (nil? at)) 71 | (is (= {:boom "3"} data)) 72 | (is (= "BOOM-3" message)) 73 | (is (= 'clojure.lang.ExceptionInfo type))))) 74 | (testing ":trace" 75 | (doseq [element trace] 76 | (is (test/stacktrace-element? element) (pr-str element))) 77 | (testing "first frame" 78 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 156] (first trace)))) 79 | (testing "last frame" 80 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 81 | 82 | (deftest parse-stacktrace-divide-by-zero-test 83 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :divide-by-zero.aviso))] 84 | (testing ":stacktrace-type" 85 | (is (= :aviso stacktrace-type))) 86 | (testing "throwable cause" 87 | (is (= "Divide by zero" cause))) 88 | (testing ":data" 89 | (is (= nil data))) 90 | (testing ":via" 91 | (is (= 1 (count via))) 92 | (testing "first cause" 93 | (let [{:keys [at data message type]} (nth via 0)] 94 | (is (nil? at)) 95 | (is (= nil data)) 96 | (is (= "Divide by zero" message)) 97 | (is (= 'java.lang.ArithmeticException type))))) 98 | (testing ":trace" 99 | (doseq [element trace] 100 | (is (test/stacktrace-element? element) (pr-str element))) 101 | (testing "first frame" 102 | (is (= '[haystack.parser.throwable-test fn "throwable_test.clj" 13] 103 | (first trace)))) 104 | (testing "last frame" 105 | (is (= '[nrepl.middleware.interruptible-eval evaluate/fn "interruptible_eval.clj" 87] 106 | (last trace))))))) 107 | 108 | (deftest parse-stacktrace-short-test 109 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :short.aviso))] 110 | (testing ":stacktrace-type" 111 | (is (= :aviso stacktrace-type))) 112 | (testing "throwable cause" 113 | (is (= "BOOM-1" cause))) 114 | (testing ":data" 115 | (is (= {:boom "1"} data))) 116 | (testing ":via" 117 | (is (= 1 (count via))) 118 | (testing "first cause" 119 | (let [{:keys [at data message type]} (nth via 0)] 120 | (is (nil? at)) 121 | (is (= {:boom "1"} data)) 122 | (is (= "BOOM-1" message)) 123 | (is (= 'clojure.lang.ExceptionInfo type))))) 124 | (testing ":trace" 125 | (doseq [element trace] 126 | (is (test/stacktrace-element? element) (pr-str element))) 127 | (testing "first frame" 128 | (is (= '[java.lang.Thread run "Thread.java" 829] (first trace)))) 129 | (testing "last frame" 130 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 131 | 132 | (deftest parse-stacktrace-incorrect-input-test 133 | (testing "parsing a string not matching the grammar" 134 | (let [{:keys [error failure input type]} (parse "")] 135 | (is (= :incorrect error)) 136 | (is (= :incorrect-input type)) 137 | (is (= "" input)) 138 | (is (= {:index 0 139 | :reason 140 | #?(:clj 141 | [{:tag :regexp, :expecting "[a-zA-Z0-9_$*-]"} 142 | {:tag :regexp, :expecting "[^\\S\\r\\n]+"}] 143 | :cljs 144 | [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$*-]/"} 145 | {:tag :regexp, :expecting "/^[^\\S\\r\\n]+/"}]) 146 | :line 1 147 | :column 1 148 | :text #?(:clj nil :cljs "")} 149 | (test/stringify-regexp failure)))))) 150 | 151 | (deftest parse-stacktrace-unsupported-input-test 152 | (testing "parsing unsupported input" 153 | (let [{:keys [error input type]} (parse 1)] 154 | (is (= :unsupported error)) 155 | (is (= :unsupported-input type)) 156 | (is (= 1 input))))) 157 | -------------------------------------------------------------------------------- /test/haystack/parser/clojure/repl_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.repl-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [haystack.parser.clojure.repl :as parser] 4 | [haystack.parser.test :as test] 5 | [haystack.parser.test.fixtures :refer [fixtures]])) 6 | 7 | (defn- parse [s] 8 | (parser/parse-stacktrace s)) 9 | 10 | (deftest parse-throwable-test 11 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :boom.clojure.repl))] 12 | (testing ":stacktrace-type" 13 | (is (= :clojure.repl stacktrace-type))) 14 | (testing "throwable cause" 15 | (is (= "BOOM-3" cause))) 16 | (testing ":data" 17 | (is (= {:boom "3"} data))) 18 | (testing ":via" 19 | (is (= 3 (count via))) 20 | (testing "first cause" 21 | (let [{:keys [at data message trace type]} (nth via 0)] 22 | (is (= '[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] at)) 23 | (is (= {:boom "1"} data)) 24 | (is (= "BOOM-1" message)) 25 | (is (= 'ExceptionInfo type)) 26 | (is (= '[[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] 27 | [clojure.lang.Compiler$DefExpr eval "Compiler.java" 457] 28 | [clojure.lang.Compiler eval "Compiler.java" 7186]] 29 | (take 3 trace))))) 30 | (testing "second cause" 31 | (let [{:keys [at data message trace type]} (nth via 1)] 32 | (is (= '[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] at)) 33 | (is (= {:boom "2"} data)) 34 | (is (= "BOOM-2" message)) 35 | (is (= 'ExceptionInfo type)) 36 | (is (= '[[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] 37 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705] 38 | [clojure.lang.Compiler$DefExpr eval "Compiler.java" 457]] 39 | (take 3 trace))))) 40 | (testing "third cause" 41 | (let [{:keys [at data message trace type]} (nth via 2)] 42 | (is (= '[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] at)) 43 | (is (= {:boom "3"} data)) 44 | (is (= "BOOM-3" message)) 45 | (is (= 'ExceptionInfo type)) 46 | (is (= '[[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] 47 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705] 48 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705]] 49 | (take 3 trace)))))) 50 | (testing ":trace" 51 | (doseq [element trace] 52 | (is (test/stacktrace-element? element) (pr-str element))) 53 | (testing "first frame" 54 | (is (= '[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706] (first trace)))) 55 | (testing "last frame" 56 | (is (= '[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705] (last trace))))))) 57 | 58 | (deftest parse-stacktrace-divide-by-zero-test 59 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :divide-by-zero.clojure.repl))] 60 | (testing ":stacktrace-type" 61 | (is (= :clojure.repl stacktrace-type))) 62 | (testing "throwable cause" 63 | (is (= "Divide by zero" cause))) 64 | (testing ":data" 65 | (is (= nil data))) 66 | (testing ":via" 67 | (is (= 1 (count via))) 68 | (testing "first cause" 69 | (let [{:keys [at data message type]} (nth via 0)] 70 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] at)) 71 | (is (= nil data)) 72 | (is (= "Divide by zero" message)) 73 | (is (= 'ArithmeticException type))))) 74 | (testing ":trace" 75 | (doseq [element trace] 76 | (is (test/stacktrace-element? element) (pr-str element))) 77 | (testing "first frame" 78 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] (first trace)))) 79 | (testing "last frame" 80 | (is (= '[clojure.lang.Compiler eval "Compiler.java" 7136] (last trace))))))) 81 | 82 | (deftest parse-stacktrace-short-test 83 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :short.clojure.repl))] 84 | (testing ":stacktrace-type" 85 | (is (= :clojure.repl stacktrace-type))) 86 | (testing "throwable cause" 87 | (is (= "BOOM-1" cause))) 88 | (testing ":data" 89 | (is (= {:boom "1"} data))) 90 | (testing ":via" 91 | (is (= 1 (count via))) 92 | (testing "first cause" 93 | (let [{:keys [at data message type]} (nth via 0)] 94 | (is (= '[java.lang.Thread run "Thread.java" 829] at)) 95 | (is (= {:boom "1"} data)) 96 | (is (= "BOOM-1" message)) 97 | (is (= 'ExceptionInfo type))))) 98 | (testing ":trace" 99 | (doseq [element trace] 100 | (is (test/stacktrace-element? element) (pr-str element))) 101 | (testing "first frame" 102 | (is (= '[java.lang.Thread run "Thread.java" 829] (first trace)))) 103 | (testing "last frame" 104 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 105 | 106 | (deftest parse-stacktrace-incorrect-input-test 107 | (testing "parsing a string not matching the grammar" 108 | (let [{:keys [error failure input type]} (parse "")] 109 | (is (= :incorrect error)) 110 | (is (= :incorrect-input type)) 111 | (is (= "" input)) 112 | (is (= {:index 0 113 | :reason 114 | #?(:clj [{:tag :regexp :expecting "[a-zA-Z0-9_$/-]"} 115 | {:tag :regexp :expecting "[^\\S\\r\\n]+"}] 116 | :cljs [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$/-]/"} 117 | {:tag :regexp, :expecting "/^[^\\S\\r\\n]+/"}]) 118 | :line 1 119 | :column 1 120 | :text #?(:clj nil :cljs "")} 121 | (test/stringify-regexp failure)))))) 122 | 123 | (deftest parse-stacktrace-unsupported-input-test 124 | (testing "parsing unsupported input" 125 | (let [{:keys [error input type]} (parse 1)] 126 | (is (= :unsupported error)) 127 | (is (= :unsupported-input type)) 128 | (is (= 1 input))))) 129 | -------------------------------------------------------------------------------- /test/haystack/parser/clojure/stacktrace_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.stacktrace-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [haystack.parser.clojure.stacktrace :as parser] 4 | [haystack.parser.test :as test] 5 | [haystack.parser.test.fixtures :refer [fixtures]])) 6 | 7 | (defn- parse [s] 8 | (parser/parse-stacktrace s)) 9 | 10 | (deftest parse-stacktrace-boom-test 11 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :boom.clojure.stacktrace))] 12 | (testing ":stacktrace-type" 13 | (is (= :clojure.stacktrace stacktrace-type))) 14 | (testing "throwable cause" 15 | (is (= "BOOM-3" cause))) 16 | (testing ":data" 17 | (is (= {:boom "3"} data))) 18 | (testing ":via" 19 | (is (= 3 (count via))) 20 | (testing "first cause" 21 | (let [{:keys [at data message trace type]} (nth via 0)] 22 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 160] at)) 23 | (is (= {:boom "1"} data)) 24 | (is (= "BOOM-1" message)) 25 | (is (= 'clojure.lang.ExceptionInfo type)) 26 | (is (= '[[clojure.lang.AFn applyToHelper "AFn.java" 160] 27 | [clojure.lang.AFn applyTo "AFn.java" 144] 28 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706]] 29 | (take 3 trace))))) 30 | (testing "second cause" 31 | (let [{:keys [at data message trace type]} (nth via 1)] 32 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 160] at)) 33 | (is (= {:boom "2"} data)) 34 | (is (= "BOOM-2" message)) 35 | (is (= 'clojure.lang.ExceptionInfo type)) 36 | (is (= '[[clojure.lang.AFn applyToHelper "AFn.java" 160] 37 | [clojure.lang.AFn applyTo "AFn.java" 144] 38 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706]] 39 | (take 3 trace))))) 40 | (testing "third cause" 41 | (let [{:keys [at data message trace type]} (nth via 2)] 42 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 156] at)) 43 | (is (= {:boom "3"} data)) 44 | (is (= "BOOM-3" message)) 45 | (is (= 'clojure.lang.ExceptionInfo type)) 46 | (is (= '[[clojure.lang.AFn applyToHelper "AFn.java" 156] 47 | [clojure.lang.AFn applyTo "AFn.java" 144] 48 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706]] 49 | (take 3 trace)))))) 50 | (testing ":trace" 51 | (doseq [element trace] 52 | (is (test/stacktrace-element? element) (pr-str element))) 53 | (testing "first frame" 54 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 156] 55 | (first trace)))) 56 | (testing "last frame" 57 | (is (= '[java.lang.Thread run "Thread.java" 829] 58 | (last trace))))))) 59 | 60 | (deftest parse-stacktrace-divide-by-zero-test 61 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :divide-by-zero.clojure.stacktrace))] 62 | (testing ":stacktrace-type" 63 | (is (= :clojure.stacktrace stacktrace-type))) 64 | (testing "throwable cause" 65 | (is (= "Divide by zero" cause))) 66 | (testing ":data" 67 | (is (= nil data))) 68 | (testing ":via" 69 | (is (= 1 (count via))) 70 | (testing "first cause" 71 | (let [{:keys [at data message type]} (nth via 0)] 72 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] at)) 73 | (is (= nil data)) 74 | (is (= "Divide by zero" message)) 75 | (is (= 'java.lang.ArithmeticException type))))) 76 | (testing ":trace" 77 | (doseq [element trace] 78 | (is (test/stacktrace-element? element) (pr-str element))) 79 | (testing "first frame" 80 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] (first trace)))) 81 | (testing "last frame" 82 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 83 | 84 | (deftest parse-stacktrace-short-test 85 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :short.clojure.stacktrace))] 86 | (testing ":stacktrace-type" 87 | (is (= :clojure.stacktrace stacktrace-type))) 88 | (testing "throwable cause" 89 | (is (= "BOOM-1" cause))) 90 | (testing ":data" 91 | (is (= {:boom "1"} data))) 92 | (testing ":via" 93 | (is (= 1 (count via))) 94 | (testing "first cause" 95 | (let [{:keys [at data message type]} (nth via 0)] 96 | (is (= '[java.lang.Thread run "Thread.java" 829] at)) 97 | (is (= {:boom "1"} data)) 98 | (is (= "BOOM-1" message)) 99 | (is (= 'clojure.lang.ExceptionInfo type))))) 100 | (testing ":trace" 101 | (doseq [element trace] 102 | (is (test/stacktrace-element? element) (pr-str element))) 103 | (testing "first frame" 104 | (is (= '[java.lang.Thread run "Thread.java" 829] (first trace)))) 105 | (testing "last frame" 106 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 107 | 108 | (deftest parse-stacktrace-incorrect-input-test 109 | (testing "parsing a string not matching the grammar" 110 | (let [{:keys [error failure input type]} (parse "")] 111 | (is (= :incorrect error)) 112 | (is (= :incorrect-input type)) 113 | (is (= "" input)) 114 | (is (= {:index 0 115 | :reason 116 | #?(:clj [{:tag :regexp :expecting "[a-zA-Z0-9_$/-]"} 117 | {:tag :regexp :expecting "[^\\S\\r\\n]+"}] 118 | :cljs [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$/-]/"} 119 | {:tag :regexp, :expecting "/^[^\\S\\r\\n]+/"}]) 120 | :line 1 121 | :column 1 122 | :text #?(:clj nil :cljs "")} 123 | (test/stringify-regexp failure)))))) 124 | 125 | (deftest parse-stacktrace-unsupported-input-test 126 | (testing "parsing unsupported input" 127 | (let [{:keys [error input type]} (parse 1)] 128 | (is (= :unsupported error)) 129 | (is (= :unsupported-input type)) 130 | (is (= 1 input))))) 131 | -------------------------------------------------------------------------------- /test/haystack/parser/clojure/tagged_literal_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.tagged-literal-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [haystack.parser.clojure.tagged-literal :as parser] 4 | [haystack.parser.test :as test] 5 | [haystack.parser.test.fixtures :refer [fixtures]])) 6 | 7 | (defn- parse [s] 8 | (parser/parse-stacktrace s)) 9 | 10 | (deftest parse-stacktrace-boom-test 11 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :boom.clojure.tagged-literal))] 12 | (testing ":stacktrace-type" 13 | (is (= :clojure.tagged-literal stacktrace-type))) 14 | (testing "throwable cause" 15 | (is (= "BOOM-3" cause))) 16 | (testing ":data" 17 | (is (= {:boom "3"} data))) 18 | (testing ":via" 19 | (is (= 3 (count via))) 20 | (testing "first cause" 21 | (let [{:keys [at data message type]} (nth via 0)] 22 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 160] at)) 23 | (is (= {:boom "1"} data)) 24 | (is (= "BOOM-1" message)) 25 | (is (= 'clojure.lang.ExceptionInfo type)))) 26 | (testing "second cause" 27 | (let [{:keys [at data message type]} (nth via 1)] 28 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 160] at)) 29 | (is (= {:boom "2"} data)) 30 | (is (= "BOOM-2" message)) 31 | (is (= 'clojure.lang.ExceptionInfo type)))) 32 | (testing "third cause" 33 | (let [{:keys [at data message type]} (nth via 2)] 34 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 156] at)) 35 | (is (= {:boom "3"} data)) 36 | (is (= "BOOM-3" message)) 37 | (is (= 'clojure.lang.ExceptionInfo type))))) 38 | (testing ":trace" 39 | (doseq [element trace] 40 | (is (test/stacktrace-element? element) (pr-str element))) 41 | (testing "first frame" 42 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 156] (first trace)))) 43 | (testing "last frame" 44 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 45 | 46 | (deftest parse-stacktrace-divide-by-zero-test 47 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :divide-by-zero.clojure.tagged-literal))] 48 | (testing ":stacktrace-type" 49 | (is (= :clojure.tagged-literal stacktrace-type))) 50 | (testing "throwable cause" 51 | (is (= "Divide by zero" cause))) 52 | (testing ":data" 53 | (is (= nil data))) 54 | (testing ":via" 55 | (is (= 1 (count via))) 56 | (testing "first cause" 57 | (let [{:keys [at data message type]} (nth via 0)] 58 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] at)) 59 | (is (= nil data)) 60 | (is (= "Divide by zero" message)) 61 | (is (= 'java.lang.ArithmeticException type))))) 62 | (testing ":trace" 63 | (doseq [element trace] 64 | (is (test/stacktrace-element? element) (pr-str element))) 65 | (testing "first frame" 66 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] (first trace)))) 67 | (testing "last frame" 68 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 69 | 70 | (deftest parse-stacktrace-short-test 71 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :short.clojure.tagged-literal))] 72 | (testing ":stacktrace-type" 73 | (is (= :clojure.tagged-literal stacktrace-type))) 74 | (testing "throwable cause" 75 | (is (= "BOOM-1" cause))) 76 | (testing ":data" 77 | (is (= {:boom "1"} data))) 78 | (testing ":via" 79 | (is (= 1 (count via))) 80 | (testing "first cause" 81 | (let [{:keys [at data message type]} (nth via 0)] 82 | (is (= '[java.lang.Thread run "Thread.java" 829] at)) 83 | (is (= {:boom "1"} data)) 84 | (is (= "BOOM-1" message)) 85 | (is (= 'clojure.lang.ExceptionInfo type))))) 86 | (testing ":trace" 87 | (doseq [element trace] 88 | (is (test/stacktrace-element? element) (pr-str element))) 89 | (testing "first frame" 90 | (is (= '[java.lang.Thread run "Thread.java" 829] (first trace)))) 91 | (testing "last frame" 92 | (is (= '[java.lang.Thread run "Thread.java" 829] (last trace))))))) 93 | 94 | (deftest parse-short-clojure-tagged-literal-println-test 95 | (is (= '{:cause "BOOM-1" 96 | :data {:boom 1} 97 | :via 98 | [{:type clojure.lang.ExceptionInfo 99 | :message "BOOM-1" 100 | :data {:boom 1} 101 | :at [java.lang.Thread run "Thread.java" 829]}] 102 | :trace [[java.lang.Thread run "Thread.java" 829]] 103 | :stacktrace-type :clojure.tagged-literal} 104 | (parse (fixtures :short.clojure.tagged-literal.println))))) 105 | 106 | (deftest parse-stacktrace-incorrect-input-test 107 | (testing "parsing incorrect input" 108 | (let [{:keys [error input type]} (parse "")] 109 | (is (= :incorrect error)) 110 | (is (= :incorrect-input type)) 111 | (is (= "" input))))) 112 | 113 | (deftest parse-stacktrace-unsupported-input-test 114 | (testing "parsing unsupported input" 115 | (let [{:keys [error input type]} (parse 1)] 116 | (is (= :unsupported error)) 117 | (is (= :unsupported-input type)) 118 | (is (= 1 input))))) 119 | -------------------------------------------------------------------------------- /test/haystack/parser/clojure/throwable_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.clojure.throwable-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | [haystack.parser.clojure.throwable :as parser] 5 | [haystack.parser.test :as test])) 6 | 7 | (def boom 8 | (ex-info "BOOM-1" {:boom "1"} 9 | (ex-info "BOOM-2" {:boom "2"} 10 | (ex-info "BOOM-3" {:boom "3"})))) 11 | 12 | (def short-boom 13 | (ex-info "BOOM-1" {:boom "1"})) 14 | 15 | (defn- parse [s] 16 | (parser/parse-stacktrace s)) 17 | 18 | (deftest parse-throwable-test 19 | (let [{:keys [cause data trace stacktrace-type via]} (parse boom)] 20 | (testing ":stacktrace-type" 21 | (is (= :throwable stacktrace-type))) 22 | (testing "throwable cause" 23 | (is (= "BOOM-3" cause))) 24 | (testing ":data" 25 | (is (= {:boom "3"} data))) 26 | (testing ":via" 27 | (is (= 3 (count via))) 28 | (testing "first cause" 29 | (let [{:keys [data message type]} (nth via 0)] 30 | #?(:clj (is (test/stacktrace-element? (:at (nth via 0))))) 31 | (is (= {:boom "1"} data)) 32 | (is (= "BOOM-1" message)) 33 | (is (= #?(:clj 'clojure.lang.ExceptionInfo :cljs 'cljs.core/ExceptionInfo) type)))) 34 | (testing "second cause" 35 | (let [{:keys [data message type]} (nth via 1)] 36 | #?(:clj (is (test/stacktrace-element? (:at (nth via 1))))) 37 | (is (= {:boom "2"} data)) 38 | (is (= "BOOM-2" message)) 39 | (is (= #?(:clj 'clojure.lang.ExceptionInfo :cljs 'cljs.core/ExceptionInfo) type)))) 40 | (testing "third cause" 41 | (let [{:keys [data message type]} (nth via 2)] 42 | #?(:clj (is (test/stacktrace-element? (:at (nth via 2))))) 43 | (is (= {:boom "3"} data)) 44 | (is (= "BOOM-3" message)) 45 | (is (= #?(:clj 'clojure.lang.ExceptionInfo :cljs 'cljs.core/ExceptionInfo) type))))) 46 | (testing ":trace" 47 | (doseq [element trace] 48 | (is (test/stacktrace-element? element) (pr-str element)))))) 49 | 50 | #?(:clj (def divide-by-zero 51 | (try (/ 1 0) (catch Exception e e)))) 52 | 53 | #?(:clj (deftest parse-stacktrace-divide-by-zero-test 54 | (let [{:keys [cause data trace stacktrace-type via]} (parse divide-by-zero)] 55 | (testing ":stacktrace-type" 56 | (is (= :throwable stacktrace-type))) 57 | (testing "throwable cause" 58 | (is (= "Divide by zero" cause))) 59 | (testing ":data" 60 | (is (= nil data))) 61 | (testing ":via" 62 | (is (= 1 (count via))) 63 | (testing "first cause" 64 | (let [{:keys [at data message type]} (nth via 0)] 65 | (is (test/stacktrace-element? at)) 66 | (is (= nil data)) 67 | (is (= "Divide by zero" message)) 68 | (is (= 'java.lang.ArithmeticException type))))) 69 | (testing ":trace" 70 | (doseq [element trace] 71 | (is (test/stacktrace-element? element) (pr-str element))))))) 72 | 73 | (deftest parse-stacktrace-short-test 74 | (let [{:keys [cause data trace stacktrace-type via]} (parse short-boom)] 75 | (testing ":stacktrace-type" 76 | (is (= :throwable stacktrace-type))) 77 | (testing "throwable cause" 78 | (is (= "BOOM-1" cause))) 79 | (testing ":data" 80 | (is (= {:boom "1"} data))) 81 | (testing ":via" 82 | (is (= 1 (count via))) 83 | (testing "first cause" 84 | (let [{:keys [data message type]} (nth via 0)] 85 | #?(:clj (is (test/stacktrace-element? (:at (nth via 0))))) 86 | (is (= {:boom "1"} data)) 87 | (is (= "BOOM-1" message)) 88 | (is (= #?(:clj 'clojure.lang.ExceptionInfo :cljs 'cljs.core/ExceptionInfo) type))))) 89 | (testing ":trace" 90 | (doseq [element trace] 91 | (is (test/stacktrace-element? element) (pr-str element)))))) 92 | 93 | (deftest parse-stacktrace-error-test 94 | (testing "parsing a string not matching the grammar" 95 | (is (= {:error :unsupported 96 | :type :unsupported-input 97 | :input ""} 98 | (parse ""))))) 99 | -------------------------------------------------------------------------------- /test/haystack/parser/java_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.java-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [haystack.parser.java :as parser] 4 | [haystack.parser.test :as test] 5 | [haystack.parser.test.fixtures :refer [fixtures]])) 6 | 7 | (defn- parse [s] 8 | (parser/parse-stacktrace s)) 9 | 10 | (deftest parse-stacktrace-boom-test 11 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :boom.java))] 12 | (testing ":stacktrace-type" 13 | (is (= :java stacktrace-type))) 14 | (testing "throwable cause" 15 | (is (= "BOOM-3" cause))) 16 | (testing ":data" 17 | (is (= {:boom "3"} data))) 18 | (testing ":via" 19 | (is (= 3 (count via))) 20 | (testing "first cause" 21 | (let [{:keys [at data message trace type]} (nth via 0)] 22 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 160] at)) 23 | (is (= {:boom "1"} data)) 24 | (is (= "BOOM-1" message)) 25 | (is (= 'clojure.lang.ExceptionInfo type)) 26 | (is (= '[[clojure.lang.AFn applyToHelper "AFn.java" 160] 27 | [clojure.lang.AFn applyTo "AFn.java" 144] 28 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706]] 29 | (take 3 trace))))) 30 | (testing "second cause" 31 | (let [{:keys [at data message trace type]} (nth via 1)] 32 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 160] at)) 33 | (is (= {:boom "2"} data)) 34 | (is (= "BOOM-2" message)) 35 | (is (= 'clojure.lang.ExceptionInfo type)) 36 | (is (= '[[clojure.lang.AFn applyToHelper "AFn.java" 160] 37 | [clojure.lang.AFn applyTo "AFn.java" 144] 38 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706]] 39 | (take 3 trace))))) 40 | (testing "third cause" 41 | (let [{:keys [at data message trace type]} (nth via 2)] 42 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 156] at)) 43 | (is (= {:boom "3"} data)) 44 | (is (= "BOOM-3" message)) 45 | (is (= 'clojure.lang.ExceptionInfo type)) 46 | (is (= '[[clojure.lang.AFn applyToHelper "AFn.java" 156] 47 | [clojure.lang.AFn applyTo "AFn.java" 144] 48 | [clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3706]] 49 | (take 3 trace)))))) 50 | (testing ":trace" 51 | (doseq [element trace] 52 | (is (test/stacktrace-element? element) (pr-str element))) 53 | (testing "first frame" 54 | (is (= '[clojure.lang.AFn applyToHelper "AFn.java" 156] 55 | (first trace)))) 56 | (testing "last frame" 57 | (is (= '[clojure.lang.Compiler$InvokeExpr eval "Compiler.java" 3705] 58 | (last trace))))))) 59 | 60 | (deftest parse-stacktrace-divide-by-zero-test 61 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :divide-by-zero.java))] 62 | (testing ":stacktrace-type" 63 | (is (= :java stacktrace-type))) 64 | (testing "throwable cause" 65 | (is (= "Divide by zero" cause))) 66 | (testing ":data" 67 | (is (= nil data))) 68 | (testing ":via" 69 | (is (= 1 (count via))) 70 | (testing "first cause" 71 | (let [{:keys [at data message type]} (nth via 0)] 72 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] at)) 73 | (is (= nil data)) 74 | (is (= "Divide by zero" message)) 75 | (is (= 'java.lang.ArithmeticException type))))) 76 | (testing ":trace" 77 | (doseq [element trace] 78 | (is (test/stacktrace-element? element) (pr-str element))) 79 | (testing "first frame" 80 | (is (= '[clojure.lang.Numbers divide "Numbers.java" 188] (first trace)))) 81 | (testing "last frame" 82 | (is (= '[java.base/java.lang.Thread run "Thread.java" 829] (last trace))))))) 83 | 84 | (deftest parse-stacktrace-short-test 85 | (let [{:keys [cause data trace stacktrace-type via]} (parse (fixtures :short.java))] 86 | (testing ":stacktrace-type" 87 | (is (= :java stacktrace-type))) 88 | (testing "throwable cause" 89 | (is (= "BOOM-1" cause))) 90 | (testing ":data" 91 | (is (= {:boom "1"} data))) 92 | (testing ":via" 93 | (is (= 1 (count via))) 94 | (testing "first cause" 95 | (let [{:keys [at data message type]} (nth via 0)] 96 | (is (= '[java.base/java.lang.Thread run "Thread.java" 829] at)) 97 | (is (= {:boom "1"} data)) 98 | (is (= "BOOM-1" message)) 99 | (is (= 'clojure.lang.ExceptionInfo type))))) 100 | (testing ":trace" 101 | (doseq [element trace] 102 | (is (test/stacktrace-element? element) (pr-str element))) 103 | (testing "first frame" 104 | (is (= '[java.base/java.lang.Thread run "Thread.java" 829] (first trace)))) 105 | (testing "last frame" 106 | (is (= '[java.base/java.lang.Thread run "Thread.java" 829] (last trace))))))) 107 | 108 | (deftest parse-stacktrace-incorrect-input-test 109 | (testing "parsing a string not matching the grammar" 110 | (let [{:keys [error failure input type]} (parse "")] 111 | (is (= :incorrect error)) 112 | (is (= :incorrect-input type)) 113 | (is (= "" input)) 114 | (is (= {:index 0 115 | :reason 116 | #?(:clj [{:tag :regexp, :expecting "[a-zA-Z0-9_$/-]"} 117 | {:tag :regexp, :expecting "[^\\S\\r\\n]+"}] 118 | :cljs [{:tag :regexp, :expecting "/^[a-zA-Z0-9_$/-]/"} 119 | {:tag :regexp, :expecting "/^[^\\S\\r\\n]+/"}]) 120 | :line 1 121 | :column 1 122 | :text #?(:clj nil :cljs "")} 123 | (test/stringify-regexp failure)))))) 124 | 125 | (deftest parse-stacktrace-unsupported-input-test 126 | (testing "parsing unsupported input" 127 | (let [{:keys [error input type]} (parse 1)] 128 | (is (= :unsupported error)) 129 | (is (= :unsupported-input type)) 130 | (is (= 1 input))))) 131 | -------------------------------------------------------------------------------- /test/haystack/parser/test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.test 2 | (:require 3 | #?(:clj [clojure.java.io :as io]) 4 | [clojure.walk :as walk])) 5 | 6 | #?(:clj (defn fixture-path 7 | "Return the fixture path for the parser `resource`." 8 | [resource] 9 | (str (io/file "test-resources" "haystack" "parser" (str (name resource) ".txt"))))) 10 | 11 | #?(:clj (defmacro fixture 12 | "Read the fixture `name`." 13 | [name] 14 | (some-> name fixture-path slurp))) 15 | 16 | (defn- pattern? 17 | "Return true if `x` is a regular expression, otherwise false." 18 | [x] 19 | (instance? #?(:clj java.util.regex.Pattern :cljs js/RegExp) x)) 20 | 21 | (defn stacktrace-element? 22 | "Return true if `element` is a stacktrace element, otherwise false." 23 | [element] 24 | (let [[class method file] element] 25 | (and (symbol? class) 26 | (symbol? method) 27 | (string? file)))) 28 | 29 | (defn stringify-regexp 30 | "Post-walk `x` and replace all instances of `java.util.regex.Pattern` 31 | in it by applying `clojure.core/str` on them." 32 | [x] 33 | (cond->> (walk/postwalk #(if (pattern? %) (str %) %) x) 34 | (map? x) (into {}))) 35 | -------------------------------------------------------------------------------- /test/haystack/parser/test/fixtures.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.test.fixtures 2 | (:require [haystack.parser.test :as test #?(:clj :refer :cljs :refer-macros) [fixture]])) 3 | 4 | (def fixtures 5 | {:boom.aviso.full (fixture :boom.aviso.full) 6 | :boom.aviso (fixture :boom.aviso) 7 | :boom.clojure.repl (fixture :boom.clojure.repl) 8 | :boom.clojure.stacktrace (fixture :boom.clojure.stacktrace) 9 | :boom.clojure.tagged-literal (fixture :boom.clojure.tagged-literal) 10 | :boom.java (fixture :boom.java) 11 | :divide-by-zero.aviso (fixture :divide-by-zero.aviso) 12 | :divide-by-zero.clojure.repl (fixture :divide-by-zero.clojure.repl) 13 | :divide-by-zero.clojure.stacktrace (fixture :divide-by-zero.clojure.stacktrace) 14 | :divide-by-zero.clojure.tagged-literal (fixture :divide-by-zero.clojure.tagged-literal) 15 | :divide-by-zero.java (fixture :divide-by-zero.java) 16 | :short.aviso (fixture :short.aviso) 17 | :short.clojure.repl (fixture :short.clojure.repl) 18 | :short.clojure.stacktrace (fixture :short.clojure.stacktrace) 19 | :short.clojure.tagged-literal.println (fixture :short.clojure.tagged-literal.println) 20 | :short.clojure.tagged-literal (fixture :short.clojure.tagged-literal) 21 | :short.java (fixture :short.java)}) 22 | -------------------------------------------------------------------------------- /test/haystack/parser/util_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser.util-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [haystack.parser.util :as util])) 5 | 6 | (deftest safe-read-edn-test 7 | (is (= nil (util/safe-read-edn "["))) 8 | (is (= [1 2 3] (util/safe-read-edn "[1 2 3]"))) 9 | (let [{:keys [form tag]} (util/safe-read-edn "#error {:a 1}")] 10 | (is (= 'error tag)) 11 | (is (= {:a 1} form)))) 12 | -------------------------------------------------------------------------------- /test/haystack/parser_test.cljc: -------------------------------------------------------------------------------- 1 | (ns haystack.parser-test 2 | (:require 3 | #?(:clj [clojure.string :as str]) 4 | [clojure.test :refer [deftest is testing]] 5 | [haystack.parser :as parser] 6 | [haystack.parser.test :as test] 7 | [haystack.parser.test.fixtures :refer [fixtures]])) 8 | 9 | (deftest parse-test 10 | (doseq [[fixture text] fixtures] 11 | (testing (str "parse fixture " fixture) 12 | (let [{:keys [cause error trace]} (parser/parse text)] 13 | (testing "should succeed" 14 | (is (nil? error))) 15 | (testing "should parse the cause" 16 | (is (string? cause))) 17 | (testing "should parse the trace" 18 | (doseq [element trace] 19 | (is (test/stacktrace-element? element) (pr-str element)))))))) 20 | 21 | (deftest parse-garbage-test 22 | (doseq [[fixture text] fixtures] 23 | (testing (str "parse fixture " fixture " with") 24 | (let [expected (parser/parse text)] 25 | (testing "garbage at the beginning" 26 | (is (= expected (parser/parse (str "\n\n\n" text))))) 27 | (testing "garbage at the end" 28 | (is (= expected (parser/parse (str text "\n\n\n"))))) 29 | (testing "white space in front" 30 | (is (= expected (parser/parse (str " \t " text))))) 31 | (testing "white space at the end" 32 | (is (= expected (parser/parse (str text " \t "))))) 33 | (testing "newlines in front" 34 | (is (= expected (parser/parse (str text "\n\n"))))) 35 | (testing "newlines at the end" 36 | (is (= expected (parser/parse (str text "\n\n"))))))))) 37 | 38 | #?(:clj (deftest parse-trim-test 39 | (doseq [[fixture text] fixtures] 40 | (testing (str "parse fixture " fixture " with") 41 | (testing "trimmed input" 42 | (is (= (parser/parse text) 43 | (parser/parse (str/trim text))))))))) 44 | 45 | (deftest parse-input-transformation-test 46 | (doseq [[fixture text] fixtures] 47 | (testing (str "parse fixture " fixture " ") 48 | (testing "with input pr-str 1 level deep" 49 | (is (= (parser/parse text) (parser/parse (pr-str text))))) 50 | (testing "with input pr-str 2 levels deep" 51 | (is (= (parser/parse text) (parser/parse (pr-str (pr-str text))))))))) 52 | -------------------------------------------------------------------------------- /test/haystack/test/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns haystack.test.runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [haystack.parser-test] 4 | [haystack.parser.aviso-test] 5 | [haystack.parser.clojure.repl-test] 6 | [haystack.parser.clojure.stacktrace-test] 7 | [haystack.parser.clojure.tagged-literal-test] 8 | [haystack.parser.clojure.throwable-test] 9 | [haystack.parser.java-test] 10 | [haystack.parser.util-test])) 11 | 12 | (doo-tests 13 | 'haystack.parser-test 14 | 'haystack.parser.aviso-test 15 | 'haystack.parser.clojure.repl-test 16 | 'haystack.parser.clojure.stacktrace-test 17 | 'haystack.parser.clojure.tagged-literal-test 18 | 'haystack.parser.clojure.throwable-test 19 | 'haystack.parser.java-test 20 | 'haystack.parser.util-test) 21 | --------------------------------------------------------------------------------