├── .github ├── CODEOWNERS └── workflows │ └── clojure.yml ├── .gitignore ├── CHANGES.md ├── LICENSE-asl.txt ├── ORIGINATOR ├── README.md ├── VERSION.txt ├── build.clj ├── deps.edn ├── docs └── images │ ├── binary-delta.png │ ├── binary-output.png │ ├── formatted-exception.png │ ├── pedestal-with-pretty.png │ └── pedestal-without-pretty.png ├── epl-v10.html ├── src └── clj_commons │ ├── ansi.clj │ ├── format │ ├── binary.clj │ ├── exceptions.clj │ ├── table.clj │ └── table │ │ └── specs.clj │ ├── pretty │ ├── annotations.clj │ ├── repl.clj │ └── spec.clj │ └── pretty_impl.clj └── test ├── clj_commons ├── ansi_test.clj ├── binary_test.clj ├── exception_test.clj ├── pretty │ └── annotations_test.clj └── test_common.clj ├── demo.clj ├── demo_app_frames.clj ├── playground.clj ├── table_demo.clj ├── tiny-clojure.gif └── user.clj /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hlship 2 | -------------------------------------------------------------------------------- /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Java 21 | uses: actions/setup-java@v4.7.0 22 | with: 23 | java-version: '11' 24 | distribution: 'corretto' 25 | 26 | - name: Install clojure tools 27 | uses: DeLaGuardo/setup-clojure@13.2 28 | with: 29 | cli: 1.12.0.1530 30 | 31 | - name: Cache clojure dependencies 32 | uses: actions/cache@v4 33 | with: 34 | path: | 35 | ~/.m2/repository 36 | ~/.gitlibs 37 | ~/.deps.clj 38 | # List all files containing dependencies: 39 | key: cljdeps-${{ hashFiles('deps.edn') }} 40 | 41 | - name: Run tests (current, Clojure 1.12) 42 | run: clojure -X:test 43 | 44 | - name: Run tests (Clojure 1.11 45 | run: clojure -X:1.11:test 46 | 47 | - name: Run tests (Clojure 1.10) 48 | run: clojure -X:1.10:test 49 | 50 | - name: Lint 51 | run: clojure -M:lint 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | /target/ 6 | .lein-deps-sum 7 | *.iml 8 | .idea/ 9 | doc 10 | .dexy 11 | .lein-failures 12 | .lein-repl-history 13 | .nrepl-port 14 | pom.xml* 15 | manual/output-site 16 | *.asc 17 | .clj-kondo/.cache 18 | .lsp/.cache 19 | .portal/vs-code.edn 20 | .cpcache 21 | /.clj-kondo/imports/ 22 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 3.3.2 - 28 Mar 2025 2 | 3 | - Changed some default exception colors to look better against a light background 4 | 5 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/56?closed=1) 6 | 7 | ## 3.3.1 - 23 Jan 2025 8 | 9 | Minor bug fixes. 10 | 11 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/55?closed=1) 12 | 13 | ## 3.3.0 - 7 Dec 2024 14 | 15 | The new `clj-commons.pretty.annotations` namespace provides functions to help create pretty errors 16 | when parsing or interpretting text: 17 | 18 | ```text 19 | SELECT DATE, AMT FROM PAYMENTS WHEN AMT > 10000 20 | ▲▲▲ ▲▲▲▲ 21 | │ │ 22 | │ └╴ Unknown token 23 | │ 24 | └╴ Invalid column name 25 | ``` 26 | 27 | Here, the errors (called "annotations") are presented as callouts targetting specific portions of the input line. 28 | 29 | The `callouts` function can handle multiple annotations on a single line, with precise control over styling and layout. 30 | 31 | The `annotate-lines` function builds on `callouts` to produce output of multiple lines from some source, 32 | interspersed with callouts: 33 | 34 | ```text 35 | 1: SELECT DATE, AMT 36 | ▲▲▲ 37 | │ 38 | └╴ Invalid column name 39 | 2: FROM PAYMENTS WHEN AMT > 10000 40 | ▲▲▲▲ 41 | │ 42 | └╴ Unknown token 43 | ``` 44 | 45 | The new `clj-commons.pretty.spec` namespace provides type and function specs for the `clj-commons.ansi` and 46 | `clj-commons.pretty.annotations` namespaces. 47 | 48 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/54?closed=1) 49 | 50 | ## 3.2.0 - 20 Sep 2024 51 | 52 | Added `clj-commons.ansi/pout` to replace the `pcompose` function; they are identical, but the `pout` name makes more 53 | sense, given that `perr` exists. 54 | 55 | Changed how `clj-commons.ansi/compose` creates ANSI SGR strings; this works around an issue in many terminal emulators 56 | where changing boldness from faint to normal, or faint to bold, is not implemented correctly. `compose` now resets fonts 57 | before each font change, which allows such transitions to render correctly. 58 | 59 | Added `clj-commons.format.exceptions/default-frame-rules` to supply defaults for `*default-frame-rules*` 60 | which makes it much easier to override the default rules. 61 | 62 | Added function `clj-commons.format.exceptions/format-stack-trace-element` which can be used to convert a Java 63 | StackTraceElement into a demangled, readable string, using the same logic as `format-exception.` 64 | 65 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/52?closed=1) 66 | 67 | ## 3.1.1 - 22 Aug 2024 68 | 69 | In a Clojure stack frame, repeated elements may be abbreviated; for example, 70 | what was output in 3.0.0 as 71 | `integration.diplomat.components.github-api-test/fn/fn/fn/fn/fn/fn/fn/fn/fn/fn/fn/fn/fn/fn` 72 | will be output in 3.1.0 as `integration.diplomat.components.github-api-test/fn{x14}` 73 | (this is an actual test case!) 74 | These crazily nested functions occur when using macro-intensive libraries such as 75 | [nubank/state-flow](https://github.com/nubank/state-flow) and [funcool/cats](https://github.com/funcool/cats). 76 | 77 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/51?closed=1) 78 | 79 | ## 3.0.0 - 7 Jun 2024 80 | 81 | **BREAKING CHANGES**: 82 | 83 | Moved the io.aviso/pretty compatibility layer (introduced in 2.5.0) to new library 84 | [org.clj-commons/pretty-aviso-bridge](https://github.com/clj-commons/pretty-aviso-bridge). 85 | 86 | Other changes: 87 | - `clj-commons.format.exceptions` 88 | - Added a cache to speed up transforming Java StackTraceElements 89 | - Added new functions to make it easier to extract, filter, and format a stack trace outside of formatting an entire exception 90 | 91 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/50?closed=1) 92 | 93 | ## 2.6.0 - 25 Apr 2024 94 | 95 | - Font declaration in `compose` can now be a vector of individual terms, rather than a single keyword; e.g. `[:bold :red]` 96 | as an alternative to `:bold.red`. This can be useful when the font is computed, rather than a static literal. 97 | 98 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/49?closed=1) 99 | 100 | ## 2.5.1 - 12 Apr 2024 101 | 102 | Minor bug fixes. 103 | 104 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/48?closed=1) 105 | 106 | ## 2.5.0 - 27 Mar 2024 107 | 108 | *BREAKING CHANGES* 109 | 110 | - The function `clojure.core/apply` is now omitted (in formatted stack traces) 111 | - Properties inside exceptions are now pretty-printed to a default depth of 2; previously, the depth was unlimited 112 | 113 | Other changes: 114 | 115 | A limited number of vars and functions defined by the io.aviso/pretty artifact have been added, allowing org.clj-commons/pretty to swap in for io.aviso/pretty in many cases. 116 | 117 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/46?closed=1) 118 | 119 | ## 2.4.0 - 24 Mar 202 120 | 121 | *BREAKING CHANGES* 122 | 123 | - `clj-commons.format.table/print-table` now centers title columns by default, 124 | and adds a :title-pad key to the column map to control this explicitly. 125 | 126 | Other changes: 127 | 128 | `compose` now supports a new value for :pad; the value :both is used to 129 | center the content, adding spaces on both sides. 130 | 131 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=is%3Aclosed+milestone%3A2.4.0) 132 | 133 | ## 2.3.0 - 9 Mar 2024 134 | 135 | A new function, `clj-commons.ansi/perr`, composes its inputs and prints 136 | them to `*err*`, a common behavior for command line tools. 137 | 138 | A new namespace, `clj-commons.format.table`, is used to format tabular output; a 139 | prettier version of `clojure.pprint/print-table` 140 | 141 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/44?closed=1) 142 | 143 | ## 2.2.1 - 14 Nov 2023 144 | 145 | This release contains only minor bug fixes: 146 | 147 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/42?closed=1) 148 | 149 | ## 2.2 - 1 Sep 2023 150 | 151 | This release is bug fixes and minor improvements. 152 | 153 | The new `clj-commons.ansi.pcompose` function is used to compose an ANSI formatted string and then print it, 154 | an exceptionally common case. 155 | 156 | The prior restriction with `compose`, that spans nested within spans with a width could not also have a width, 157 | has been removed. 158 | 159 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=is%3Aclosed+milestone%3A2.2) 160 | 161 | ## 2.1.1 - 18 Aug 2023 162 | 163 | Bug fixes 164 | 165 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=is%3Aclosed+milestone%3A2.1.1) 166 | 167 | ## 2.1 - 11 Aug 2023 168 | 169 | `install-pretty-exceptions` has been changed to now extend 170 | `clojure.core/print-method` for Throwable, using `format-exception`. 171 | Exceptions printed by the REPL are now formatted using Pretty; 172 | further, when using `clojure.test`, when a `thrown-with-msg?` assertion fails, 173 | the actual exception is now formatted (as this also, indirectly, uses `print-method`). 174 | 175 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=is%3Aclosed+milestone%3A2.1) 176 | 177 | ## 2.0.2 - 7 Aug 2023 178 | 179 | Bug Fixes 180 | 181 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=is%3Aclosed+milestone%3A2.0.2) 182 | 183 | ## 2.0.1 -- 20 Jul 2023 184 | 185 | Bug Fixes 186 | 187 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/37?closed=1) 188 | 189 | ## 2.0 -- 14 Jul 2023 190 | 191 | This release moves the library to clj-commons, and changes the root namespace from 192 | `io.aviso` to `clj-commons`. It strips down the library to its essentials, removing 193 | the `columns`, `component`, and `logging` namespaces entirely. 194 | 195 | - Stripped out a lot of redundant documentation 196 | - Reworked the `ansi` namespace to primarily expose the `compose` function and not dozens of constants and functions 197 | - `ansi` determines whether to enable or disable ANSI codes at execution time 198 | - `ansi` now honors the `NO_COLOR` environment variable 199 | - Stripped out code for accessing the clipboard from the `repl` namespace 200 | - Some refactoring inside `exceptions` namespace, including changes to the `*fonts*` var 201 | - Removed the `logging` namespace and dependency on `org.clojure/tools.logging` 202 | - Removed the `component` namespace, but the example is still present in the documentation 203 | - Ensure compatible with Clojure 1.10 and above (now tested in GitHub action) 204 | - The "use -XX:-OmitStackTraceInFastThrow" warning is now formatted, and is output only once 205 | - `write-exception` was renamed to `print-exception` 206 | - `write-binary` and `write-binary-delta` renamed to `print-binary` and `print-binary-delta` 207 | - `compose` can now pad a span of text with spaces (on the left or right) to a desired width 208 | - Binary output now includes color coding 209 | 210 | ## 1.4.4 -- 20 Jun 2023 211 | 212 | - Fixed: Incorrectly named font terms with `compose` 213 | - Fixed: Incorrect ANSI codes for bright and bright background colors 214 | 215 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/36?closed=1) 216 | 217 | ## 1.4.3 -- 24 May 2023 218 | 219 | The `compose` function would collapse blank strings to empty strings. 220 | 221 | [Closed Issues](https://github.com/clj-commons/pretty/milestone/33?closed=1) 222 | 223 | ## 1.4.1, 1.4.2 -- 5 May 2023 224 | 225 | `io.aviso.ansi`: Add support for `faint`, `underlined`, and `not-underlined` text, and improvements 226 | to docstrings. 227 | 228 | [Closed issues](https://github.com/clj-commons/pretty/milestone/34?closed=1) 229 | 230 | ## 1.4 -- 27 Mar 2023 231 | 232 | A new function, `io.aviso.ansi/compose` uses a [Hiccup](https://github.com/weavejester/hiccup)-inspired 233 | syntax to make composing text with ANSI fonts (foreground and background colors, inverse, bold, and 234 | italic) easy and concise. 235 | 236 | The override to enable or disable ANSI text has been amended: the first check is for 237 | a JVM system property, `io.aviso.ansi.enable`, then if that is not set, the `ENABLE_ANSI_COLORS` 238 | environment variable. 239 | 240 | [Closed issues](https://github.com/clj-commons/pretty/milestone/32?closed=1) 241 | 242 | ## 1.3 -- 20 Oct 2022 243 | 244 | The default stack frame filter now terminates at any `speclj.*` namespace. 245 | 246 | The `io.aviso.ansi` namespace now determines whether output is connected to a terminal, 247 | and disables fonts and colors if so; this can be overridden with the `ENABLE_ANSI_COLORS` 248 | environment variable. 249 | 250 | Added a `-main` function to `io.aviso.repl`; this installs pretty exceptions before delegating 251 | to `clojure.main/main`. Thus, `clojure -m io.aviso.repl -m org.example.myapp` will ultimately 252 | pass any remaining command line arguments to `org.example.myapp/-main`. 253 | 254 | The pretty replacement for `clojure.repl/pst` now writes to `*err*`, not `*out*`. 255 | 256 | ## 1.2 -- 30 Sep 2022 257 | 258 | Output from `write-exception` is now buffered; this should reduce the 259 | interleaving of exception output when multiple threads are writing 260 | exceptions simultaneously. 261 | 262 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=is%3Aclosed+milestone%3A1.2) 263 | 264 | ## 1.1.1 -- 15 Dec 2021 265 | 266 | Prevent warnings when using with Clojure 1.11. 267 | 268 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=is%3Aclosed+milestone%3A1.1.1) 269 | 270 | ## 1.1 -- 16 May 2021 271 | 272 | Restore compatibility with Clojure 1.7.0. 273 | 274 | ## 1.0 - 16 May 2021 275 | 276 | BinaryData protocol extended onto java.nio.ByteBuffer. 277 | 278 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A1.0+is%3Aclosed) 279 | 280 | ## 0.1.37 - 30 Jan 2019 281 | 282 | *Incompatible Changes*: 283 | 284 | * Removed the `io.aviso.writer` namespace and changed many functions 285 | to simply write to `*out*` rather than take a writer parameter. 286 | 287 | * It is now necessary to setup explicit :middleware in your `project.clj`, as 288 | Leiningen is phasing out implicit middleware. 289 | See the manual for more details. 290 | 291 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.37+is%3Aclosed) 292 | 293 | ## 0.1.36 - 22 Dec 2018 294 | 295 | Support Clojure 1.10. 296 | 297 | Add support for highlighting application frames using `io.aviso.exception/*app-frame-names*`. 298 | 299 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.36+is%3Aclosed) 300 | 301 | ## 0.1.35 - 28 Sep 2018 302 | 303 | When printing sorted maps, the keys are presented in map order (not sorted). 304 | 305 | The new namespace, `io.aviso.component`, can be used to produce concise output 306 | for systems and components that are pretty printed as part of exception output. 307 | 308 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.35+is%3Aclosed) 309 | 310 | ## 0.1.34 - 28 Jun 2017 311 | 312 | Added possibility to disable default ANSI fonts by setting an environment variable `DISABLE_DEFAULT_PRETTY_FONTS` 313 | to any value. 314 | 315 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.34+is%3Aclosed) 316 | 317 | ## 0.1.33 - 28 Nov 2016 318 | 319 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.33+is%3Aclosed) 320 | 321 | ## 0.1.32 - 18 Nov 2016 322 | 323 | New functions in `io.aviso.repl` for copying text from the clipboard, 324 | and pretty printing it as EDN or as a formatted exception. 325 | 326 | ## 0.1.31 - 15 Nov 2016 327 | 328 | Switch to using macros instead of eval to play nicer with AOT 329 | 330 | Support all arities of `clojure.repl/pst` 331 | 332 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.31+is%3Aclosed) 333 | 334 | ## 0.1.30 - 16 Aug 2016 335 | 336 | Fix bad ns declaration and reflection warnings 337 | 338 | ## 0.1.29 - 19 Jul 2016 339 | 340 | Fix an issue where the code injected by the plugin could get damaged by other plugins, resulting in a 341 | ClassNotFoundException. 342 | 343 | ## 0.1.28 - 15 Jul 2016 344 | 345 | A warning is now produced when using pretty as a plugin, but it is not a 346 | project dependency. 347 | 348 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.28+is%3Aclosed) 349 | 350 | ## 0.1.27 - 1 Jul 2016 351 | 352 | Qualified keys in exception maps are now printed properly. 353 | 354 | ## 0.1.26 - 15 Apr 2016 355 | 356 | To get around Clojure 1.8 deep linking, `io.aviso.repl/install-pretty-exceptions` now reloads clojure.test 357 | after overriding other functions (such as `clojure.stacktrace/print-stack-trace`). 358 | 359 | [Closed Issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.26+is%3Aclosed) 360 | 361 | ## 0.1.25 - 5 Apr 2016 362 | 363 | The writer used in write-exception is now locked and flush on newline is disabled; 364 | this helps ensure that multiple threads do not write their output interspersed 365 | in an unreadable way. 366 | 367 | ## 0.1.24 - 26 Feb 2016 368 | 369 | Internal change to how exception properties are pretty-printed. 370 | 371 | ## 0.1.23 - 11 Feb 2016 372 | 373 | `parse-exception` can now handle method names containing `<` and `>` (used for instance and class 374 | constructor methods), as well as other cases from real-life stack traces. 375 | 376 | Stack traces were omitted when the root exception was via `ex-info`; this has been corrected. 377 | 378 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.23) 379 | 380 | ## 0.1.22 - 5 Feb 2016 381 | 382 | Fixed a bug where `parse-exception` would fail if the source was "Unknown Source" instead 383 | of a file name and line number. 384 | 385 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.22) 386 | 387 | ## 0.1.21 - 8 Jan 2016 388 | 389 | Improved docstrings for ANSI font constants and functions. 390 | 391 | Added support for invokePrim() stack frames. 392 | These are hidden as with Clojure 1.8 invokeStatic() frames. 393 | 394 | Stack frames that represent REPL input now appear as `REPL Input` in the file column, rather than 395 | something like `form-init9201216130440431126.clj`. 396 | 397 | Source files with extension `.cljc` (introduced in Clojure 1.7) are now recognized as Clojure code. 398 | 399 | It is now possible to parse a block of exception text (say, copied from an output log) 400 | so that it may be formatted. 401 | Because of the wide range in which different JDKs may output exceptions, this is considered 402 | experimental. 403 | 404 | **Incompatible change:** write-binary now expects an optional map (not a varargs of keys and values) 405 | for options such as :ascii and :line-bytes. 406 | 407 | ## 0.1.20 - 4 Dec 2015 408 | 409 | Pretty will identify repeating stack frames (for example, from an infinite loop) 410 | and only print the stack frame once, but append the number of times it repeats. 411 | 412 | Made an adjustment for Clojure 1.8's new direct linking feature. 413 | 414 | Improved the way Pretty acts as a Leiningen plugin. Pretty exceptions reports are now 415 | produced for both REPL sessions and test executions. 416 | 417 | The io.aviso.nrepl namespace has been removed. 418 | 419 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.20) 420 | 421 | ## 0.1.19 - 27 Aug 2015 422 | 423 | Print a blank line before the exception output, when reporting a clojure.test exception. 424 | Previously, the first line was was on the same line as the "actual:" label, which 425 | interfered with columnar output. 426 | 427 | The built in stack frame filtering rules are now better documented, and speclj.* is now included as :terminate. 428 | 429 | You may now add pretty to your Leiningen :plugins list; it will automatically add the Pretty nREPL 430 | middleware. 431 | 432 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.19+is%3Aclosed) 433 | 434 | 435 | ## 0.1.18 - 5 May 2015 436 | 437 | io.aviso.repl/install-pretty-logging now installs a default Thread uncaughtExceptionHandler. 438 | 439 | There's a new arity of io.aviso.columns/write-rows that streamlines the whole process (it can 440 | calculate column widths automatically). 441 | 442 | The Clojure ExceptionInfo exception is now treated specially. 443 | 444 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.18+is%3Aclosed) 445 | 446 | 447 | ## 0.1.17 - 18 Feb 2015 448 | 449 | Changed io.aviso.logging to always use the current value of \*default-logging-filter\* rather than capturing 450 | its value when install-pretty-logging is invoked. 451 | 452 | Sometimes, the file name of a stack trace element is a complete path (this occurs with some 453 | testing frameworks); in that case, Pretty will now strip off the prefix from the path, when 454 | it matches the current directory path. 455 | This keeps the file name column as narrow as possible. 456 | 457 | ## 0.1.16 - 4 Feb 2015 458 | 459 | io.aviso.exception/\*default-frame-filter\* has been added, and acts as the default frame filter for 460 | write-exception (previously there was no default). 461 | 462 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.16+is%3Aclosed) 463 | 464 | ## 0.1.15 - 2 Feb 2015 465 | 466 | Starting in this release, the exception report layout has changed significantly; however, the old 467 | behavior is still available via the io.aviso.exceptions/\*traditional\* dynamic var. 468 | 469 | A new namespace, io.aviso.logging, includes code to setup clojure.tools.logging to make use of pretty 470 | exception output. 471 | 472 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.15+is%3Aclosed) 473 | 474 | ## 0.1.14 - 9 Jan 2015 475 | 476 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.14+is%3Aclosed) 477 | 478 | ## 0.1.13 - 14 Nov 2014 479 | 480 | It is now possible to control how particular types are formatted when printing the 481 | properties of an exception. 482 | This can be very useful when using (for example) Stuart Sierra's [component](https://github.com/stuartsierra/component) 483 | library. 484 | 485 | ## 0.1.12 - 27 May 2014 486 | 487 | The default stack-frame filter now excludes frames from the `sun.reflect` package (and sub-packages). 488 | 489 | For exceptions added via `io.aviso.repl/install-pretty-exceptions`, the filtering omits frames from the `clojure.lang` 490 | package, and terminates output when the frames for the REPL are reached. 491 | 492 | ## 0.1.11 - 14 May 2014 493 | 494 | It is now possible to specify a _filter_ for stack frames in the exception output. 495 | Frames can be hidden (not displayed at all), or omitted (replaced with '...'). 496 | 497 | This can remove _significant_ clutter from the exception output, making it that much easier 498 | to identify the true cause of the exception. 499 | 500 | [Closed issues](https://github.com/clj-commons/pretty/issues?q=milestone%3A0.1.11) 501 | -------------------------------------------------------------------------------- /LICENSE-asl.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /ORIGINATOR: -------------------------------------------------------------------------------- 1 | @hlship 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Clojars](https://img.shields.io/clojars/v/org.clj-commons/pretty.svg)](http://clojars.org/org.clj-commons/pretty) 2 | [![CI](https://github.com/clj-commons/pretty/actions/workflows/clojure.yml/badge.svg)](https://github.com/clj-commons/pretty/actions/workflows/clojure.yml) 3 | [![cljdoc badge](https://cljdoc.org/badge/org.clj-commons/pretty)](https://cljdoc.org/d/org.clj-commons/pretty/) 4 | 5 | *Sometimes, neatness counts* 6 | 7 | If you are trying to puzzle out a stack trace, 8 | pick a critical line of text out of a long stream of console output, 9 | or compare two streams of binary data, a little bit of formatting can go a long way. 10 | 11 | That's what `org.clj-commons/pretty` is for. It adds support for pretty output where it counts: 12 | 13 | * Readable output for exceptions 14 | * General ANSI font and background color support 15 | * Readable output for binary sequences 16 | 17 | ![Example](docs/images/formatted-exception.png) 18 | 19 | 20 | Or, compare an example from 21 | [Pedestal](http://github.com/pedestal/pedestal)'s test suite: 22 | 23 | ![No Pretty](docs/images/pedestal-without-pretty.png) 24 | 25 | Or, same thing, but with Pretty enabled: 26 | 27 | ![With Pretty](docs/images/pedestal-with-pretty.png) 28 | 29 | The point is, you can scan down to see things in chronological order; the important parts are highlighted, the names are the same (or closer) to your source code, unnecessary details are omitted, and it's much easier to pick out the most important parts, such as file names and line numbers. 30 | 31 | ## Beyond Exceptions 32 | 33 | Pretty can print out a sequence of bytes; it includes color-coding inspired by 34 | [hexyl](https://github.com/sharkdp/hexyl): 35 | 36 | ![Binary Output](docs/images/binary-output.png) 37 | 38 | Pretty can also print out a delta of two byte sequences, using background color 39 | to indicate where the two sequences differ. 40 | 41 | ![Binary Delta](docs/images/binary-delta.png) 42 | 43 | Pretty can output pretty tabular data: 44 | 45 | ``` 46 | (print-table 47 | [:method 48 | :path 49 | {:key :route-name :title "Name"}] 50 | [{:method :get 51 | :path "/" 52 | :route-name :root-page} 53 | {:method :post 54 | :path "/reset" 55 | :route-name :reset} 56 | {:method :get 57 | :path "/status" 58 | :route-name :status}]) 59 | ┏━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━┓ 60 | ┃ Method ┃ Path ┃ Name ┃ 61 | ┣━━━━━━━━╋━━━━━━━━━╋━━━━━━━━━━━━┫ 62 | ┃ :get ┃ / ┃ :root-page ┃ 63 | ┃ :post ┃ /reset ┃ :reset ┃ 64 | ┃ :get ┃ /status ┃ :status ┃ 65 | ┗━━━━━━━━┻━━━━━━━━━┻━━━━━━━━━━━━┛ 66 | => nil 67 | ``` 68 | 69 | The `print-table` function has many options to easily adjust the output to your needs, including fonts, text alignment, and the table border. 70 | 71 | 72 | ## Compatibility 73 | 74 | Pretty is compatible with Clojure 1.10 and above. 75 | 76 | Parts of Pretty can be used with [Babashka](https://book.babashka.org/#introduction), such as the `clj-commons.ansi` 77 | namespace; however, Babashka runs in an interpreter and its approach to exceptions is 78 | incompatible with JVM exceptions. 79 | 80 | ## License 81 | 82 | The majority of this code is available under the terms of the Apache Software License 1.0; some portions 83 | are available under the terms of the Eclipse Public Licence 1.0. 84 | 85 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 3.3.2 2 | 3 | 4 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | ;; clj -T:build 2 | 3 | (ns build 4 | (:require [clojure.tools.build.api :as build] 5 | [net.lewisship.build :as b] 6 | [clojure.string :as str])) 7 | 8 | (def lib 'org.clj-commons/pretty) 9 | (def version (-> "VERSION.txt" slurp str/trim)) 10 | 11 | (def jar-params {:project-name lib 12 | :version version}) 13 | 14 | (defn clean 15 | [_params] 16 | (build/delete {:path "target"})) 17 | 18 | (defn jar 19 | [_params] 20 | (b/create-jar jar-params)) 21 | 22 | (defn deploy 23 | [_params] 24 | (clean nil) 25 | (b/deploy-jar (assoc (jar nil) :sign-artifacts? false))) 26 | 27 | (defn codox 28 | [_params] 29 | (b/generate-codox {:project-name lib 30 | :version version})) 31 | 32 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0"}} 3 | 4 | :aliases 5 | {:test 6 | ;; clj -X:test 7 | {:extra-paths ["test"] 8 | :extra-deps {criterium/criterium {:mvn/version "0.4.6"} 9 | org.clojure/core.async {:mvn/version "1.7.701"} 10 | nubank/matcher-combinators {:mvn/version "3.9.1"} 11 | io.github.tonsky/clj-reload {:mvn/version "0.9.4"} 12 | io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" 13 | :git/sha "dfb30dd"}} 14 | :jvm-opts ["-Dclj-commons.ansi.enabled=true"] 15 | :exec-fn cognitect.test-runner.api/test} 16 | 17 | ;; clj -T:build 18 | :build 19 | {:deps {io.github.hlship/build-tools 20 | {:git/tag "0.11.0" :git/sha "8c67d11"}} 21 | :ns-default build} 22 | 23 | :1.11 24 | {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}} 25 | 26 | :1.10 27 | {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}} 28 | 29 | ;; clj -M:test:demo 30 | :demo 31 | {:main-opts ["-m" "demo"]} 32 | 33 | :disable-colors 34 | {:jvm-opts ["-Dclj-commons.ansi.enabled=false"]} 35 | 36 | ;; clj -M:lint 37 | 38 | :lint 39 | {:deps {clj-kondo/clj-kondo {:mvn/version "2025.02.20"}} 40 | :main-opts ["-m" "clj-kondo.main" "--lint" "src"]} 41 | 42 | :nrepl 43 | {:extra-deps {nrepl/nrepl {:mvn/version "1.3.1"}} 44 | :main-opts ["-m" "nrepl.cmdline"]} 45 | 46 | :repl 47 | {:main-opts ["-m" "clj-commons.pretty.repl"]}} 48 | 49 | :net.lewisship.build/scm 50 | {:url "https://github.com/clj-commons/pretty" 51 | :license :asl} 52 | 53 | :codox/config 54 | {:description "Clojure library to help print things, prettily" 55 | :source-uri "https://github.com/clj-commons/pretty/blob/master/{filepath}#L{line}"}} 56 | -------------------------------------------------------------------------------- /docs/images/binary-delta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/pretty/5a36b44a88bbca91eaae3e771b4d9916840d0518/docs/images/binary-delta.png -------------------------------------------------------------------------------- /docs/images/binary-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/pretty/5a36b44a88bbca91eaae3e771b4d9916840d0518/docs/images/binary-output.png -------------------------------------------------------------------------------- /docs/images/formatted-exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/pretty/5a36b44a88bbca91eaae3e771b4d9916840d0518/docs/images/formatted-exception.png -------------------------------------------------------------------------------- /docs/images/pedestal-with-pretty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/pretty/5a36b44a88bbca91eaae3e771b4d9916840d0518/docs/images/pedestal-with-pretty.png -------------------------------------------------------------------------------- /docs/images/pedestal-without-pretty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/pretty/5a36b44a88bbca91eaae3e771b4d9916840d0518/docs/images/pedestal-without-pretty.png -------------------------------------------------------------------------------- /epl-v10.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Eclipse Public License - Version 1.0 8 | 25 | 26 | 27 | 28 | 29 | 30 |

Eclipse Public License - v 1.0

31 | 32 |

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 33 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR 34 | DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS 35 | AGREEMENT.

36 | 37 |

1. DEFINITIONS

38 | 39 |

"Contribution" means:

40 | 41 |

a) in the case of the initial Contributor, the initial 42 | code and documentation distributed under this Agreement, and

43 |

b) in the case of each subsequent Contributor:

44 |

i) changes to the Program, and

45 |

ii) additions to the Program;

46 |

where such changes and/or additions to the Program 47 | originate from and are distributed by that particular Contributor. A 48 | Contribution 'originates' from a Contributor if it was added to the 49 | Program by such Contributor itself or anyone acting on such 50 | Contributor's behalf. Contributions do not include additions to the 51 | Program which: (i) are separate modules of software distributed in 52 | conjunction with the Program under their own license agreement, and (ii) 53 | are not derivative works of the Program.

54 | 55 |

"Contributor" means any person or entity that distributes 56 | the Program.

57 | 58 |

"Licensed Patents" mean patent claims licensable by a 59 | Contributor which are necessarily infringed by the use or sale of its 60 | Contribution alone or when combined with the Program.

61 | 62 |

"Program" means the Contributions distributed in accordance 63 | with this Agreement.

64 | 65 |

"Recipient" means anyone who receives the Program under 66 | this Agreement, including all Contributors.

67 | 68 |

2. GRANT OF RIGHTS

69 | 70 |

a) Subject to the terms of this Agreement, each 71 | Contributor hereby grants Recipient a non-exclusive, worldwide, 72 | royalty-free copyright license to reproduce, prepare derivative works 73 | of, publicly display, publicly perform, distribute and sublicense the 74 | Contribution of such Contributor, if any, and such derivative works, in 75 | source code and object code form.

76 | 77 |

b) Subject to the terms of this Agreement, each 78 | Contributor hereby grants Recipient a non-exclusive, worldwide, 79 | royalty-free patent license under Licensed Patents to make, use, sell, 80 | offer to sell, import and otherwise transfer the Contribution of such 81 | Contributor, if any, in source code and object code form. This patent 82 | license shall apply to the combination of the Contribution and the 83 | Program if, at the time the Contribution is added by the Contributor, 84 | such addition of the Contribution causes such combination to be covered 85 | by the Licensed Patents. The patent license shall not apply to any other 86 | combinations which include the Contribution. No hardware per se is 87 | licensed hereunder.

88 | 89 |

c) Recipient understands that although each Contributor 90 | grants the licenses to its Contributions set forth herein, no assurances 91 | are provided by any Contributor that the Program does not infringe the 92 | patent or other intellectual property rights of any other entity. Each 93 | Contributor disclaims any liability to Recipient for claims brought by 94 | any other entity based on infringement of intellectual property rights 95 | or otherwise. As a condition to exercising the rights and licenses 96 | granted hereunder, each Recipient hereby assumes sole responsibility to 97 | secure any other intellectual property rights needed, if any. For 98 | example, if a third party patent license is required to allow Recipient 99 | to distribute the Program, it is Recipient's responsibility to acquire 100 | that license before distributing the Program.

101 | 102 |

d) Each Contributor represents that to its knowledge it 103 | has sufficient copyright rights in its Contribution, if any, to grant 104 | the copyright license set forth in this Agreement.

105 | 106 |

3. REQUIREMENTS

107 | 108 |

A Contributor may choose to distribute the Program in object code 109 | form under its own license agreement, provided that:

110 | 111 |

a) it complies with the terms and conditions of this 112 | Agreement; and

113 | 114 |

b) its license agreement:

115 | 116 |

i) effectively disclaims on behalf of all Contributors 117 | all warranties and conditions, express and implied, including warranties 118 | or conditions of title and non-infringement, and implied warranties or 119 | conditions of merchantability and fitness for a particular purpose;

120 | 121 |

ii) effectively excludes on behalf of all Contributors 122 | all liability for damages, including direct, indirect, special, 123 | incidental and consequential damages, such as lost profits;

124 | 125 |

iii) states that any provisions which differ from this 126 | Agreement are offered by that Contributor alone and not by any other 127 | party; and

128 | 129 |

iv) states that source code for the Program is available 130 | from such Contributor, and informs licensees how to obtain it in a 131 | reasonable manner on or through a medium customarily used for software 132 | exchange.

133 | 134 |

When the Program is made available in source code form:

135 | 136 |

a) it must be made available under this Agreement; and

137 | 138 |

b) a copy of this Agreement must be included with each 139 | copy of the Program.

140 | 141 |

Contributors may not remove or alter any copyright notices contained 142 | within the Program.

143 | 144 |

Each Contributor must identify itself as the originator of its 145 | Contribution, if any, in a manner that reasonably allows subsequent 146 | Recipients to identify the originator of the Contribution.

147 | 148 |

4. COMMERCIAL DISTRIBUTION

149 | 150 |

Commercial distributors of software may accept certain 151 | responsibilities with respect to end users, business partners and the 152 | like. While this license is intended to facilitate the commercial use of 153 | the Program, the Contributor who includes the Program in a commercial 154 | product offering should do so in a manner which does not create 155 | potential liability for other Contributors. Therefore, if a Contributor 156 | includes the Program in a commercial product offering, such Contributor 157 | ("Commercial Contributor") hereby agrees to defend and 158 | indemnify every other Contributor ("Indemnified Contributor") 159 | against any losses, damages and costs (collectively "Losses") 160 | arising from claims, lawsuits and other legal actions brought by a third 161 | party against the Indemnified Contributor to the extent caused by the 162 | acts or omissions of such Commercial Contributor in connection with its 163 | distribution of the Program in a commercial product offering. The 164 | obligations in this section do not apply to any claims or Losses 165 | relating to any actual or alleged intellectual property infringement. In 166 | order to qualify, an Indemnified Contributor must: a) promptly notify 167 | the Commercial Contributor in writing of such claim, and b) allow the 168 | Commercial Contributor to control, and cooperate with the Commercial 169 | Contributor in, the defense and any related settlement negotiations. The 170 | Indemnified Contributor may participate in any such claim at its own 171 | expense.

172 | 173 |

For example, a Contributor might include the Program in a commercial 174 | product offering, Product X. That Contributor is then a Commercial 175 | Contributor. If that Commercial Contributor then makes performance 176 | claims, or offers warranties related to Product X, those performance 177 | claims and warranties are such Commercial Contributor's responsibility 178 | alone. Under this section, the Commercial Contributor would have to 179 | defend claims against the other Contributors related to those 180 | performance claims and warranties, and if a court requires any other 181 | Contributor to pay any damages as a result, the Commercial Contributor 182 | must pay those damages.

183 | 184 |

5. NO WARRANTY

185 | 186 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 187 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 188 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, 189 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 190 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 191 | responsible for determining the appropriateness of using and 192 | distributing the Program and assumes all risks associated with its 193 | exercise of rights under this Agreement , including but not limited to 194 | the risks and costs of program errors, compliance with applicable laws, 195 | damage to or loss of data, programs or equipment, and unavailability or 196 | interruption of operations.

197 | 198 |

6. DISCLAIMER OF LIABILITY

199 | 200 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT 201 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 202 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 203 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 204 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 205 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 206 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 207 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

208 | 209 |

7. GENERAL

210 | 211 |

If any provision of this Agreement is invalid or unenforceable under 212 | applicable law, it shall not affect the validity or enforceability of 213 | the remainder of the terms of this Agreement, and without further action 214 | by the parties hereto, such provision shall be reformed to the minimum 215 | extent necessary to make such provision valid and enforceable.

216 | 217 |

If Recipient institutes patent litigation against any entity 218 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 219 | Program itself (excluding combinations of the Program with other 220 | software or hardware) infringes such Recipient's patent(s), then such 221 | Recipient's rights granted under Section 2(b) shall terminate as of the 222 | date such litigation is filed.

223 | 224 |

All Recipient's rights under this Agreement shall terminate if it 225 | fails to comply with any of the material terms or conditions of this 226 | Agreement and does not cure such failure in a reasonable period of time 227 | after becoming aware of such noncompliance. If all Recipient's rights 228 | under this Agreement terminate, Recipient agrees to cease use and 229 | distribution of the Program as soon as reasonably practicable. However, 230 | Recipient's obligations under this Agreement and any licenses granted by 231 | Recipient relating to the Program shall continue and survive.

232 | 233 |

Everyone is permitted to copy and distribute copies of this 234 | Agreement, but in order to avoid inconsistency the Agreement is 235 | copyrighted and may only be modified in the following manner. The 236 | Agreement Steward reserves the right to publish new versions (including 237 | revisions) of this Agreement from time to time. No one other than the 238 | Agreement Steward has the right to modify this Agreement. The Eclipse 239 | Foundation is the initial Agreement Steward. The Eclipse Foundation may 240 | assign the responsibility to serve as the Agreement Steward to a 241 | suitable separate entity. Each new version of the Agreement will be 242 | given a distinguishing version number. The Program (including 243 | Contributions) may always be distributed subject to the version of the 244 | Agreement under which it was received. In addition, after a new version 245 | of the Agreement is published, Contributor may elect to distribute the 246 | Program (including its Contributions) under the new version. Except as 247 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no 248 | rights or licenses to the intellectual property of any Contributor under 249 | this Agreement, whether expressly, by implication, estoppel or 250 | otherwise. All rights in the Program not expressly granted under this 251 | Agreement are reserved.

252 | 253 |

This Agreement is governed by the laws of the State of New York and 254 | the intellectual property laws of the United States of America. No party 255 | to this Agreement will bring a legal action under this Agreement more 256 | than one year after the cause of action arose. Each party waives its 257 | rights to a jury trial in any resulting litigation.

258 | 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /src/clj_commons/ansi.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.ansi 2 | "Help with generating textual output that includes ANSI escape codes for formatting. 3 | The [[compose]] function is the best starting point. 4 | 5 | Specs for types and functions are in the [[spec]] namespace. 6 | 7 | Reference: [ANSI Escape Codes @ Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR)." 8 | (:require [clojure.string :as str] 9 | [clj-commons.pretty-impl :refer [csi padding]])) 10 | 11 | (defn- is-ns-loaded? 12 | [sym] 13 | (some? (find-ns sym))) 14 | 15 | (defn- to-boolean 16 | [s] 17 | (-> s str/trim str/lower-case (= "true"))) 18 | 19 | (def ^:dynamic *color-enabled* 20 | "Determines if ANSI colors are enabled; color is a deliberate misnomer, as we lump 21 | other font characteristics (bold, underline, italic, etc.) along with colors. 22 | 23 | This will be false if the environment variable NO_COLOR is non-blank. 24 | 25 | Otherwise, the JVM system property `clj-commons.ansi.enabled` (if present) determines 26 | the value; \"true\" enables colors, any other value disables colors. 27 | 28 | If the property is null, then the default is a best guess based on the environment: 29 | if either the `nrepl.core` namespace is present, or the JVM has a console (via `(System/console)`), 30 | then color will be enabled. 31 | 32 | The nrepl.core check has been verified to work with Cursive, with `lein repl`, and with `clojure` (or `clj`)." 33 | (if (seq (System/getenv "NO_COLOR")) 34 | false 35 | (let [flag (System/getProperty "clj-commons.ansi.enabled")] 36 | (cond 37 | (some? flag) (to-boolean flag) 38 | 39 | (is-ns-loaded? 'nrepl.core) 40 | true 41 | 42 | :else 43 | (some? (System/console)))))) 44 | 45 | (defmacro when-color-enabled 46 | "Evaluates its body only when [[*color-enabled*]] is true." 47 | [& body] 48 | `(when *color-enabled* ~@body)) 49 | 50 | ;; select graphic rendition 51 | (def ^:const ^:private sgr 52 | "The Select Graphic Rendition suffix: m" 53 | "m") 54 | 55 | (def ^:const ^:private reset-font 56 | "ANSI escape code to resets all font characteristics." 57 | (str csi sgr)) 58 | 59 | (def ^:private font-terms 60 | ;; Map a keyword to a tuple of characteristic and SGR parameter value. 61 | ;; We track the current value for each characteristic. 62 | (reduce merge 63 | {:bold [:bold "1"] 64 | :plain [:bold "22"] 65 | :faint [:bold "2"] 66 | 67 | :italic [:italic "3"] 68 | :roman [:italic "23"] 69 | 70 | :inverse [:inverse "7"] 71 | :normal [:inverse "27"] 72 | 73 | :underlined [:underlined "4"] 74 | :not-underlined [:underlined "24"]} 75 | (map-indexed 76 | (fn [index color-name] 77 | {(keyword color-name) [:foreground (str (+ 30 index))] 78 | (keyword (str "bright-" color-name)) [:foreground (str (+ 90 index))] 79 | (keyword (str color-name "-bg")) [:background (str (+ 40 index))] 80 | (keyword (str "bright-" color-name "-bg")) [:background (str (+ 100 index))]}) 81 | ["black" "red" "green" "yellow" "blue" "magenta" "cyan" "white"]))) 82 | 83 | (defn- compose-font 84 | "Uses values in current to build a font string that will reset all fonts characteristics then, 85 | as necessary, add back needed font characteristics." 86 | ^String [current] 87 | (when-color-enabled 88 | (let [codes (keep #(get current %) [:foreground :background :bold :italic :inverse :underlined])] 89 | (if (seq codes) 90 | (str csi "0;" (str/join ";" codes) sgr) 91 | ;; there were active characteristics, but current has none, so just reset font characteristics 92 | reset-font)))) 93 | 94 | (defn- split-font-def* 95 | [font-def] 96 | (assert (simple-keyword? font-def) "expected a simple keyword to define the font characteristics") 97 | (mapv keyword (str/split (name font-def) #"\."))) 98 | 99 | (def ^:private memoized-split-font-def* (memoize split-font-def*)) 100 | 101 | (defn ^:private split-font-def 102 | [font-def] 103 | (if (vector? font-def) 104 | font-def 105 | (memoized-split-font-def* font-def))) 106 | 107 | (defn- update-font-data-from-font-def 108 | [font-data font-def] 109 | (if (some? font-def) 110 | (let [ks (split-font-def font-def) 111 | f (fn [font-data term] 112 | ;; nils are allowed in the vector form of a font def 113 | (if (nil? term) 114 | font-data 115 | (let [[font-k font-value] (or (get font-terms term) 116 | (throw (ex-info (str "unexpected font term: " term) 117 | {:font-term term 118 | :font-def font-def 119 | :available-terms (->> font-terms keys sort vec)})))] 120 | (assoc! font-data font-k font-value))))] 121 | (persistent! (reduce f (transient font-data) ks))) 122 | font-data)) 123 | 124 | (defn- extract-span-decl 125 | [value] 126 | (cond 127 | (nil? value) 128 | nil 129 | 130 | ;; It would be tempting to split the keyword here, but that would obscure an error if the keyword 131 | ;; contains a substring that isn't an expected font term. 132 | (or (keyword? value) 133 | (vector? value)) 134 | {:font value} 135 | 136 | (map? value) 137 | value 138 | 139 | :else 140 | (throw (ex-info "invalid span declaration" 141 | {:font-decl value})))) 142 | 143 | (defn- nil-or-empty-string? 144 | "True if an empty string, or nil; false otherwise, such as for numbers, etc." 145 | [value] 146 | (or (nil? value) 147 | (= "" value))) 148 | 149 | (declare ^:private normalize-markup) 150 | 151 | (defn- half-of 152 | [^long x round-up?] 153 | (let [base (Math/floorDiv x (long 2))] 154 | (+ base 155 | (if round-up? 156 | (mod x 2) 157 | 0)))) 158 | 159 | (defn- apply-padding 160 | [terms pad width actual-width] 161 | (let [padding-needed (- width actual-width) 162 | left-padding (case pad 163 | (:left nil) padding-needed 164 | :both (half-of padding-needed true) 165 | 0) 166 | right-padding (case pad 167 | :right padding-needed 168 | :both (half-of padding-needed false) 169 | 0) 170 | left-padded (if (pos? left-padding) 171 | (into [(first terms) 172 | (padding left-padding)] 173 | (next terms)) 174 | terms)] 175 | (cond-> left-padded 176 | (pos? right-padding) 177 | (conj (padding right-padding))))) 178 | 179 | (defn- normalize-and-pad-markup 180 | "Given a span broken into decl and inputs, returns a map of :width (actual width, which may exceed 181 | requested width) and :span (a replacement span vector with added spaces)." 182 | [span-decl remaining-inputs] 183 | (let [{:keys [width pad]} span-decl 184 | ;; Transform this span and everything below it into easily managed span vectors, starting 185 | ;; with a version of this span decl. 186 | span-decl' (dissoc span-decl :width :pad) 187 | *width (volatile! 0) 188 | inputs' (into [span-decl'] (normalize-markup remaining-inputs *width)) 189 | actual-width @*width] 190 | ;; If at or over desired width, don't need to pad 191 | (if (<= width actual-width) 192 | {:width actual-width 193 | :span inputs'} 194 | ;; Add the padding in the desired position(s); this ensures that the logic that generates 195 | ;; ANSI escape codes occurs correctly, with the added spaces getting the font for this span. 196 | {:width width 197 | :span (apply-padding inputs' pad width actual-width)}))) 198 | 199 | 200 | (defn- normalize-markup 201 | "Normalizes markup to span vectors, while keeping track of the total length of string values." 202 | [coll *width] 203 | (let [f (fn reducer [result input] 204 | (cond 205 | (nil-or-empty-string? input) 206 | result 207 | 208 | (vector? input) 209 | (let [decl (extract-span-decl (first input)) 210 | more-inputs (next input) 211 | span (if (:width decl) 212 | (let [{:keys [width span]} (normalize-and-pad-markup decl more-inputs)] 213 | (vswap! *width + width) 214 | span) 215 | ;; Normalize contents while also tracking the width 216 | (reduce reducer [decl] more-inputs))] 217 | (conj result span)) 218 | 219 | (sequential? input) 220 | ;; Convert to a span with a nil decl 221 | (let [sub-span (reduce reducer [nil] input)] 222 | (conj result sub-span)) 223 | 224 | :else 225 | (let [value-str ^String (str input)] 226 | (vswap! *width + (.length value-str)) 227 | (conj result value-str))))] 228 | (reduce f [] coll))) 229 | 230 | (defn- collect-markup 231 | [state input] 232 | (cond 233 | (nil-or-empty-string? input) 234 | state 235 | 236 | (vector? input) 237 | (let [[first-element & inputs] input 238 | {:keys [width font] :as span-decl} (extract-span-decl first-element)] 239 | (if width 240 | (recur state (:span (normalize-and-pad-markup span-decl inputs))) 241 | ;; Normal (no width tracking) 242 | (let [{:keys [current]} state] 243 | (-> (reduce collect-markup 244 | (update state :current update-font-data-from-font-def font) 245 | inputs) 246 | ;; At the end of the vector, return current (but not the active) 247 | ;; to what it was previously. We leave active alone until we're about 248 | ;; to output. 249 | (assoc :current current))))) 250 | 251 | ;; Lists, lazy-lists, etc: processed recursively 252 | (sequential? input) 253 | (reduce collect-markup state input) 254 | 255 | :else 256 | (let [{:keys [active current ^StringBuilder buffer]} state 257 | state' (if (= active current) 258 | state 259 | (let [font-str (compose-font current)] 260 | (when font-str 261 | (.append buffer font-str)) 262 | (cond-> (assoc state :active current) 263 | ;; Signal that a reset is needed at the very end 264 | font-str 265 | (assoc :dirty? (not= font-str reset-font)))))] 266 | (.append buffer (str input)) 267 | state'))) 268 | 269 | (defn- compose* 270 | [inputs] 271 | (let [buffer (StringBuilder. 100) 272 | {:keys [dirty?]} (collect-markup {:active {} 273 | :current {} 274 | :buffer buffer} 275 | inputs)] 276 | (when dirty? 277 | (.append buffer reset-font)) 278 | (.toString buffer))) 279 | 280 | (defn compose 281 | "Given a Hiccup-inspired data structure, composes and returns a string that includes ANSI formatting codes 282 | for font color and other characteristics. 283 | 284 | The data structure may consist of literal values (strings, numbers, etc.) that are formatted 285 | with `str` and concatenated. 286 | 287 | Nested sequences are composed recursively; this (for example) allows the output from 288 | `map` or `for` to be mixed into the composed string seamlessly. 289 | 290 | Nested vectors represent _spans_, a sequence of values with a specific visual representation. 291 | The first element in a span vector declares the visual properties of the span: the font color 292 | and other font characteristics, and the width and padding (described later). 293 | Spans may be nested. 294 | 295 | The declaration is usually a keyword, to define just the font. 296 | The font def contains one or more terms, separated by periods. 297 | 298 | The terms: 299 | 300 | Characteristic | Values 301 | --- |--- 302 | foreground color | `red` or `bright-red` (for each color) 303 | background color | same as foreground color, with a `-bg` suffix (e.g., `red-bg`) 304 | boldness | `bold`, `faint`, or `plain` 305 | italics | `italic` or `roman` 306 | inverse | `inverse` or `normal` 307 | underline | `underlined` or `not-underlined` 308 | 309 | e.g. 310 | 311 | ``` 312 | (compose [:yellow \"Warning: the \" [:bold.bright-white.bright-red-bg \"reactor\"] 313 | \" is about to \" 314 | [:italic.bold.red \"meltdown!\"]]) 315 | => ... 316 | ``` 317 | 318 | The order of the terms does not matter. Behavior for conflicting terms (e.g., `:blue.green.black`) 319 | is not defined. 320 | 321 | Font defs apply on top of the font def of the enclosing span, and the outer span's font def 322 | is restored at the end of the inner span, e.g. `[:red \" RED \" [:bold \"RED/BOLD\"] \" RED \"]`. 323 | 324 | Alternately, a font def may be a vector of individual keywords, e.g., `[[:bold :red] ...]` rather than 325 | `[:bold.red ...]`. This works better when the exact font characteristics are determined 326 | dynamically. 327 | 328 | A font def may also be nil, to indicate no change in font. 329 | 330 | `compose` presumes that on entry the current font is plain (default foreground and background, not bold, 331 | or inverse, or italic, or underlined) and appends a reset sequence to the end of the returned string to 332 | ensure that later output is also plain. 333 | 334 | The core colors are `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, and `white`. 335 | 336 | When [[*color-enabled*]] is false, then any font defs are validated, but otherwise ignored (no ANSI codes 337 | will be included in the composed string). 338 | 339 | The span's font declaration may also be a map with the following keys: 340 | 341 | Key | Type | Description 342 | --- |--- |--- 343 | :font | keyword or vector of keywords | The font declaration 344 | :width | number | The desired width of the span 345 | :pad | :left, :right, :both | Where to pad the span if :width specified, default is :left 346 | 347 | The map form of the font declaration is typically only used when a span width is specified. 348 | The span will be padded with spaces to ensure that it is the specified width. `compose` tracks the number 349 | of characters inside the span, excluding any ANSI code sequences injected by `compose`. 350 | 351 | Padding adds spaces; thus aligning the text on the left means padding on the right, and vice-versa. 352 | 353 | Setting the padding to :both will add spaces to both the left and the right; the content will be centered. 354 | If the necessary amount of padding is odd, the extra space will appear on the left. 355 | 356 | `compose` doesn't consider the characters when calculating widths; 357 | if the strings contain tabs, newlines, or ANSI code sequences not generated by `compose`, 358 | the calculation of the span width will be incorrect. 359 | 360 | Example: 361 | 362 | [{:font :red 363 | :width 20} message] 364 | 365 | This will output the value of `message` in red text, padded with spaces on the left to be 20 characters. 366 | 367 | `compose` does not truncate a span to a width, it only pads if the span in too short." 368 | {:added "1.4.0"} 369 | [& inputs] 370 | (compose* inputs)) 371 | 372 | (defn pout 373 | "Composes its inputs as with [[compose]] and then prints the results, with a newline." 374 | {:added "3.2"} 375 | [& inputs] 376 | (println (compose* inputs))) 377 | 378 | (defn pcompose 379 | "Composes its inputs as with [[compose]] and then prints the results, with a newline. 380 | 381 | Deprecated: use [[pout]] instead." 382 | {:added "2.2" 383 | :deprecated "3.2.0"} 384 | [& inputs] 385 | (println (compose* inputs))) 386 | 387 | (defn perr 388 | "Composes its inputs as with [[compose]] and then prints the result with a newline to `*err*`." 389 | {:added "2.3.0"} 390 | [& inputs] 391 | (binding [*out* *err*] 392 | (println (compose* inputs)))) 393 | 394 | 395 | -------------------------------------------------------------------------------- /src/clj_commons/format/binary.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.format.binary 2 | "Utilities for formatting binary data (byte arrays) or binary deltas." 3 | (:require [clj-commons.ansi :refer [compose]] 4 | [clj-commons.pretty-impl :refer [padding]]) 5 | (:import (java.nio ByteBuffer))) 6 | 7 | (def ^:dynamic *fonts* 8 | "Mapping from byte category to a font (color)." 9 | {:offset :bright-black 10 | :null :bright-black 11 | :printable :cyan 12 | :whitespace :green 13 | :other :faint.green 14 | :non-ascii :yellow}) 15 | 16 | (def ^:private placeholders 17 | {:null "•" 18 | :whitespace "_" 19 | :other "•" 20 | :non-ascii "×"}) 21 | 22 | (defprotocol BinaryData 23 | "Allows various data sources to be treated as a byte-array data type that 24 | supports a length and random access to individual bytes. 25 | 26 | BinaryData is extended onto byte arrays, java.nio.ByteBuffer, java.lang.String, java.lang.StringBuilder, and onto nil." 27 | 28 | (data-length [this] "The total number of bytes available.") 29 | ^byte (byte-at [this index] "The byte value at a specific offset.")) 30 | 31 | ;; This is problematic for clj-kondo, but valid. 32 | #_:clj-kondo/ignore 33 | (extend-type (Class/forName "[B") 34 | BinaryData 35 | (data-length [ary] (alength (bytes ary))) 36 | (byte-at [ary index] (aget (bytes ary) index))) 37 | 38 | (extend-type ByteBuffer 39 | BinaryData 40 | (data-length [b] (.remaining b)) 41 | (byte-at [b index] (.get ^ByteBuffer b (int index)))) 42 | 43 | ;;; Extends String as a convenience; assumes that the 44 | ;;; String is in utf-8. 45 | 46 | (extend-type String 47 | BinaryData 48 | (data-length [s] (.length s)) 49 | (byte-at [s index] (-> s (.charAt index) int byte))) 50 | 51 | (extend-type StringBuilder 52 | BinaryData 53 | (data-length [sb] (.length sb)) 54 | (byte-at [sb index] 55 | (-> sb (.charAt index) int byte))) 56 | 57 | (extend-type nil 58 | BinaryData 59 | (data-length [_] 0) 60 | (byte-at [_ _index] (throw (IndexOutOfBoundsException. "can not use byte-at with nil")))) 61 | 62 | (def ^:private ^:const bytes-per-diff-line 16) 63 | (def ^:private ^:const bytes-per-ascii-line 16) 64 | (def ^:private ^:const bytes-per-line (* 2 bytes-per-diff-line)) 65 | 66 | (def ^:private whitespace 67 | #{0x09 0x0a 0x0b 0x0c 0x0d 0x20}) 68 | 69 | (defn- category-for-byte 70 | [^long value] 71 | (cond 72 | (zero? value) 73 | :null 74 | 75 | (< 0x7f value) 76 | :non-ascii 77 | 78 | (contains? whitespace value) 79 | :whitespace 80 | 81 | (<= 0x21 value 0x7e) 82 | :printable 83 | 84 | :else 85 | :other)) 86 | 87 | (defn- font-for-byte 88 | [^long value] 89 | (get *fonts* (category-for-byte value))) 90 | 91 | (defn- to-ascii 92 | [^long b] 93 | (let [category (category-for-byte b)] 94 | [(get *fonts* category) 95 | (if (or (= :printable category) 96 | (= 0x20 b)) 97 | (char b) 98 | (get placeholders category))])) 99 | 100 | (defn- hex-digit-count 101 | [max-length] 102 | (loop [digits 4 103 | cutoff 0xffff] 104 | (if (<= max-length cutoff) 105 | digits 106 | (recur (+ 2 digits) 107 | (* cutoff 0xff))))) 108 | 109 | (defn- make-offset-format 110 | [max-length] 111 | (str "%0" (hex-digit-count max-length) "X:")) 112 | 113 | (defn- write-line 114 | [write-ascii? offset-format offset data line-count per-line] 115 | (let [line-bytes (for [i (range line-count)] 116 | (Byte/toUnsignedLong (byte-at data (+ offset i))))] 117 | (println 118 | (compose 119 | [(:offset *fonts*) 120 | (format offset-format offset)] 121 | (for [b line-bytes] 122 | (list " " 123 | [(font-for-byte b) 124 | (format "%02X" b)])) 125 | (when write-ascii? 126 | (list 127 | (padding (* 3 (- per-line line-count))) 128 | " |" 129 | (map to-ascii line-bytes) 130 | (padding (- per-line line-count)) 131 | "|")))))) 132 | 133 | (defn print-binary 134 | "Formats a BinaryData into a hex-dump string, consisting of multiple lines; each line formatted as: 135 | 136 | 0000: 43 68 6F 6F 73 65 20 69 6D 6D 75 74 61 62 69 6C 69 74 79 2C 20 61 6E 64 20 73 65 65 20 77 68 65 137 | 0020: 72 65 20 74 68 61 74 20 74 61 6B 65 73 20 79 6F 75 2E 138 | 139 | The full version specifies the [[BinaryData]] to write, and options: 140 | 141 | 142 | Key | Type | Description 143 | --- |--- |--- 144 | :ascii | boolean | If true, enable ASCII mode 145 | :line-bytes | number | Bytes printed per line 146 | 147 | :line-bytes defaults to 16 for ASCII, and 32 otherwise. 148 | 149 | In ASCII mode, the output is 16 bytes per line, but each line includes the ASCII printable characters: 150 | 151 | 0000: 43 68 6F 6F 73 65 20 69 6D 6D 75 74 61 62 69 6C |Choose immutabil| 152 | 0010: 69 74 79 2C 20 61 6E 64 20 73 65 65 20 77 68 65 |ity, and see whe| 153 | 0020: 72 65 20 74 68 61 74 20 74 61 6B 65 73 20 79 6F |re that takes yo| 154 | 0030: 75 2E |u. | 155 | 156 | When ANSI is enabled, the individual bytes and characters are color-coded as per the [[*fonts*]]." 157 | ([data] 158 | (print-binary data nil)) 159 | ([data options] 160 | (let [{show-ascii? :ascii 161 | per-line-option :line-bytes} options 162 | per-line (or per-line-option 163 | (if show-ascii? bytes-per-ascii-line bytes-per-line)) 164 | input-length (data-length data) 165 | offset-format (make-offset-format input-length)] 166 | (assert (pos? per-line) "must be at least one byte per line") 167 | (loop [offset 0] 168 | (let [remaining (- input-length offset)] 169 | (when (pos? remaining) 170 | (write-line show-ascii? offset-format offset data (min per-line remaining) per-line) 171 | (recur (long (+ per-line offset))))))))) 172 | 173 | (defn format-binary 174 | "Formats the data using [[write-binary]] and returns the result as a string." 175 | ([data] 176 | (format-binary data nil)) 177 | ([data options] 178 | (with-out-str 179 | (print-binary data options)))) 180 | 181 | (defn- match? 182 | [byte-offset data-length data alternate-length alternate] 183 | (and 184 | (< byte-offset data-length) 185 | (< byte-offset alternate-length) 186 | (== (byte-at data byte-offset) (byte-at alternate byte-offset)))) 187 | 188 | (defn- compose-deltas 189 | "Returns a composed value of one line (16 bytes) of data." 190 | [mismatch-font offset data-length data alternate-length alternate] 191 | (for [i (range bytes-per-diff-line)] 192 | (let [byte-offset (+ offset i) 193 | *value (delay 194 | (let [value (long (byte-at data byte-offset)) 195 | byte-font (font-for-byte value)] 196 | [byte-font (format "%02X" value)]))] 197 | (cond 198 | (match? byte-offset data-length data alternate-length alternate) 199 | (list " " @*value) 200 | 201 | ;; Some kind of mismatch, so decorate with this side's color 202 | (< byte-offset data-length) (list " " [mismatch-font @*value]) 203 | ;; Are we out of data on this side? Print a "--" decorated with the color. 204 | (< byte-offset alternate-length) (list " " [mismatch-font "--"]))))) 205 | 206 | (defn- print-delta-line 207 | [offset-format offset expected-length expected actual-length actual] 208 | (println 209 | (compose 210 | [(:offset *fonts*) 211 | (format offset-format offset)] 212 | [{:pad :right 213 | :width (* 3 bytes-per-diff-line)} 214 | (compose-deltas :bright-green-bg offset expected-length expected actual-length actual)] 215 | " |" 216 | (compose-deltas :bright-red-bg offset actual-length actual expected-length expected)))) 217 | 218 | (defn print-binary-delta 219 | "Formats a hex dump of the expected data (on the left) and actual data (on the right). Bytes 220 | that do not match are highlighted in green on the expected side, and red on the actual side. 221 | When one side is shorter than the other, it is padded with `--` placeholders to make this 222 | more clearly visible. 223 | 224 | expected and actual are [[BinaryData]]. 225 | 226 | Display 16 bytes (from each data set) per line." 227 | [expected actual] 228 | (let [expected-length (data-length expected) 229 | actual-length (data-length actual) 230 | target-length (max actual-length expected-length) 231 | offset-format (make-offset-format (max actual-length target-length))] 232 | (loop [offset 0] 233 | (when (pos? (- target-length offset)) 234 | (print-delta-line offset-format offset expected-length expected actual-length actual) 235 | (recur (long (+ bytes-per-diff-line offset))))))) 236 | 237 | (defn format-binary-delta 238 | "Formats the delta using [[print-binary-delta]] and returns the result as a string." 239 | [expected actual] 240 | (with-out-str 241 | (print-binary-delta expected actual))) 242 | -------------------------------------------------------------------------------- /src/clj_commons/format/exceptions.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.format.exceptions 2 | "Format and output exceptions in a pretty (structured, formatted) way." 3 | (:require [clojure.pprint :as pp] 4 | [clojure.set :as set] 5 | [clojure.string :as str] 6 | [clj-commons.ansi :refer [compose]] 7 | [clj-commons.pretty-impl :refer [padding]]) 8 | (:refer-clojure :exclude [*print-level* *print-length*]) 9 | (:import (java.lang StringBuilder StackTraceElement) 10 | (clojure.lang Compiler ExceptionInfo Named) 11 | (java.util.regex Pattern))) 12 | 13 | (def default-fonts 14 | "A default map of [[compose]] font defs for different elements in the formatted exception report." 15 | {:exception :bold.red 16 | :message :italic 17 | :property :bold 18 | :source :green 19 | :app-frame :bold.yellow 20 | :function-name :bold.yellow 21 | :clojure-frame :yellow 22 | :java-frame :bright-black 23 | :omitted-frame :faint.bright-black}) 24 | 25 | (def ^:dynamic *app-frame-names* 26 | "Set of strings or regular expressions defining the application's namespaces, which allows 27 | such namespaces to be highlighted in exception output." 28 | nil) 29 | 30 | (def ^:dynamic *fonts* 31 | "Current set of fonts used in exception formatting. This can be overridden to change colors, or bound to nil 32 | to disable fonts. Defaults are defined by [[default-fonts]]." 33 | default-fonts) 34 | 35 | (def ^{:dynamic true 36 | :added "0.1.15"} 37 | *traditional* 38 | "If bound to true, then exceptions will be formatted the traditional way - the same as Java exceptions 39 | with the deepest stack frame first. By default, the stack trace is inverted, so that the deepest 40 | stack frames come last, mimicking chronological order." 41 | false) 42 | 43 | (defn- length 44 | [^String s] 45 | (if s 46 | (.length s) 47 | 0)) 48 | 49 | (defn- strip-prefix 50 | [^String prefix ^String input] 51 | (let [prefix-len (.length prefix)] 52 | (if (and (str/starts-with? input prefix) 53 | (< prefix-len (.length input))) 54 | (subs input prefix-len) 55 | input))) 56 | 57 | (def ^:private current-dir-prefix 58 | "Convert the current directory (via property 'user.dir') into a prefix to be omitted from file names." 59 | (str (System/getProperty "user.dir") "/")) 60 | 61 | (defn- ?reverse 62 | [reverse? coll] 63 | (if reverse? 64 | (reverse coll) 65 | coll)) 66 | 67 | ;;; Obviously, this is making use of some internals of Clojure that 68 | ;;; could change at any time. 69 | 70 | (def ^:private clojure->java 71 | (->> Compiler/CHAR_MAP 72 | set/map-invert 73 | (sort-by #(-> % first length)) 74 | reverse)) 75 | 76 | (defn- match-mangled 77 | [^String s i] 78 | (->> clojure->java 79 | (filter (fn [[k _]] (.regionMatches s i k 0 (length k)))) 80 | ;; Return the matching sequence and its single character replacement 81 | first)) 82 | 83 | (defn demangle 84 | "De-mangle a Java name back to a Clojure name by converting mangled sequences, such as \"_QMARK_\" 85 | back into simple characters." 86 | [^String s] 87 | (let [in-length (.length s) 88 | result (StringBuilder. in-length)] 89 | (loop [i 0] 90 | (cond 91 | (>= i in-length) (.toString result) 92 | (= \_ (.charAt s i)) (let [[match replacement] (match-mangled s i)] 93 | (.append result replacement) 94 | (recur (long (+ i (length match))))) 95 | :else (do 96 | (.append result (.charAt s i)) 97 | (recur (inc i))))))) 98 | 99 | (defn- match-keys 100 | "Apply the function f to all values in the map; where the result is truthy, add the key to the result." 101 | [m f] 102 | ;; (seq m) is necessary because the source is via (bean), which returns an odd implementation of map 103 | (reduce (fn [result [k v]] (if (f v) (conj result k) result)) [] (seq m))) 104 | 105 | (def ^{:added "3.2.0"} default-frame-rules 106 | "The set of rules that forms the default for [[*default-frame-rules*]], and the 107 | basis for [[*default-frame-filter*]], as a vector of vectors. 108 | 109 | Each rule is a vector of three values: 110 | 111 | * A function that extracts the value from the stack frame map (typically, this is a keyword such 112 | as :package or :name). The value is converted to a string. 113 | * A string or regexp used for matching. Strings must match exactly. 114 | * A resulting frame visibility (:hide, :omit, :terminate, or :show). 115 | 116 | The default rules: 117 | 118 | * omit everything in `clojure.lang`, `java.lang.reflect`, and the function `clojure.core/apply` 119 | * hide everything in `sun.reflect` 120 | * terminate at `speclj.*`, `clojure.main/main*`, `clojure.main/repl/read-eval-print`, or `nrepl.middleware.interruptible-eval` 121 | " 122 | [[:package "clojure.lang" :omit] 123 | [:package #"sun\.reflect.*" :hide] 124 | [:package "java.lang.reflect" :omit] 125 | [:name #"speclj\..*" :terminate] 126 | [:name "clojure.core/apply" :omit] 127 | [:name #"nrepl\.middleware\.interruptible-eval/.*" :terminate] 128 | [:name #"clojure\.main/repl/read-eval-print.*" :terminate] 129 | [:name #"clojure\.main/main.*" :terminate]]) 130 | 131 | (def ^{:added "0.1.18" 132 | :dynamic true} 133 | *default-frame-rules* 134 | "The set of rules that forms the basis for [[*default-frame-filter*]], as a vector of vectors, 135 | initialized from [[default-frame-rules]]." 136 | default-frame-rules) 137 | 138 | (defn- apply-rule 139 | [frame [f match visibility :as rule]] 140 | (let [value (str (f frame))] 141 | (cond 142 | (string? match) 143 | (when (= match value) visibility) 144 | 145 | (instance? Pattern match) 146 | (when (re-matches match value) visibility) 147 | 148 | :else 149 | (throw (ex-info "unexpected match type in rule" 150 | {:rule rule}))))) 151 | 152 | (defn *default-frame-filter* 153 | "Default stack frame filter used when printing REPL exceptions; default value is derived from [[*default-frame-rules*]]." 154 | {:added "0.1.16" 155 | :dynamic true} 156 | [frame] 157 | (or 158 | (reduce (fn [_ rule] 159 | (when-let [result (apply-rule frame rule)] 160 | (reduced result))) 161 | nil 162 | *default-frame-rules*) 163 | :show)) 164 | 165 | (defn- convert-to-clojure 166 | [class-name method-name] 167 | (let [[namespace-name & raw-function-ids] (str/split class-name #"\$") 168 | ;; Clojure adds __1234 unique ids to the ends of things, remove those. 169 | function-ids (map #(str/replace % #"__\d+" "") raw-function-ids) 170 | ;; In a degenerate case, a protocol method could be called "invoke" or "doInvoke"; we're ignoring 171 | ;; that possibility here and assuming it's the IFn.invoke(), doInvoke() or 172 | ;; the invokeStatic method introduced with direct linking in Clojure 1.8. 173 | all-ids (if (#{"invoke" "doInvoke" "invokeStatic" "invokePrim"} method-name) 174 | function-ids 175 | (-> function-ids vec (conj method-name)))] 176 | ;; The assumption is that no real namespace or function name will contain underscores (the underscores 177 | ;; are name-mangled dashes). 178 | (->> 179 | (cons namespace-name all-ids) 180 | (mapv demangle)))) 181 | 182 | (defn- extension 183 | [^String file-name] 184 | (let [x (str/last-index-of file-name ".")] 185 | (when (and x (pos? x)) 186 | (subs file-name (inc x))))) 187 | 188 | (def ^:private clojure-extensions 189 | #{"clj" "cljc"}) 190 | 191 | (defn- is-repl-input? 192 | [file-name] 193 | (boolean 194 | (or 195 | (= "NO_SOURCE_FILE" file-name) 196 | ; This pattern comes from somewhere inside nREPL, I believe - may be dated 197 | (re-matches #"form-init\d+\.clj" file-name)))) 198 | 199 | (defn- transform-stack-trace-element 200 | [file-name-prefix *cache ^StackTraceElement element] 201 | (or (get @*cache element) 202 | (let [class-name (.getClassName element) 203 | method-name (.getMethodName element) 204 | dotx (str/last-index-of class-name ".") 205 | file-name (or (.getFileName element) "") 206 | repl-input (is-repl-input? file-name) 207 | [file line] (if repl-input 208 | ["REPL Input"] 209 | [(strip-prefix file-name-prefix file-name) 210 | (-> element .getLineNumber)]) 211 | is-clojure? (or repl-input 212 | (->> file-name extension (contains? clojure-extensions))) 213 | names (if is-clojure? (convert-to-clojure class-name method-name) []) 214 | name (str/join "/" names) 215 | id (cond-> (if is-clojure? 216 | name 217 | (str class-name "." method-name)) 218 | line (str ":" line)) 219 | expanded {:file file 220 | :line (when (and line 221 | (pos? line)) 222 | line) 223 | :class class-name 224 | :package (when dotx (subs class-name 0 dotx)) 225 | :is-clojure? is-clojure? 226 | :simple-class (if dotx 227 | (subs class-name (inc dotx)) 228 | class-name) 229 | :method method-name 230 | ;; Used to detect repeating frames 231 | :id id 232 | ;; Used to calculate column width 233 | :name name 234 | ;; Used to present compound Clojure name with last term highlighted 235 | :names names}] 236 | (vswap! *cache assoc element expanded) 237 | expanded))) 238 | 239 | (defn- apply-frame-filter 240 | [frame-filter frames] 241 | (if (nil? frame-filter) 242 | frames 243 | (let [*omitting? (volatile! false) 244 | result (reduce (fn [result frame] 245 | (case (frame-filter frame) 246 | :terminate 247 | (reduced result) 248 | 249 | :show 250 | (do 251 | (vreset! *omitting? false) 252 | (conj! result frame)) 253 | 254 | :hide 255 | result 256 | 257 | :omit 258 | (if @*omitting? 259 | result 260 | (do 261 | (vreset! *omitting? true) 262 | (conj! result (assoc frame :omitted true)))))) 263 | (transient []) 264 | frames)] 265 | (persistent! result)))) 266 | 267 | (defn- remove-direct-link-frames 268 | "With Clojure 1.8, in code (such as clojure.core) that is direct linked, 269 | you'll often see an invokeStatic() and/or invokePrim() frame invoked from an invoke() frame 270 | of the same class (the class being a compiled function). That ends up looking 271 | like a two-frame repeat, which is not accurate. 272 | 273 | This function filters out the .invoke frames so that a single Clojure 274 | function call is represented in the output as a single stack frame." 275 | [elements] 276 | (loop [filtered (transient []) 277 | prev-frame nil 278 | remaining elements] 279 | (if (empty? remaining) 280 | (persistent! filtered) 281 | (let [[this-frame & rest] remaining] 282 | (if (and prev-frame 283 | (:is-clojure? prev-frame) 284 | (:is-clojure? this-frame) 285 | (= (:class prev-frame) (:class this-frame)) 286 | (= "invokeStatic" (:method prev-frame)) 287 | (contains? #{"invoke" "invokePrim"} (:method this-frame))) 288 | (recur filtered this-frame rest) 289 | (recur (conj! filtered this-frame) 290 | this-frame 291 | rest)))))) 292 | 293 | (defn- is-repeat? 294 | [left-frame right-frame] 295 | (= (:id left-frame) 296 | (:id right-frame))) 297 | 298 | (defn- repeating-frame-reducer 299 | [output-frames frame] 300 | (let [output-count (count output-frames) 301 | last-output-index (dec output-count)] 302 | (cond 303 | (zero? output-count) 304 | (conj output-frames frame) 305 | 306 | (is-repeat? (output-frames last-output-index) frame) 307 | (update-in output-frames [last-output-index :repeats] 308 | (fnil inc 1)) 309 | 310 | :else 311 | (conj output-frames frame)))) 312 | 313 | (def ^:private stack-trace-warning 314 | (delay 315 | (binding [*out* *err*] 316 | (println (compose 317 | [:bright-yellow "WARNING: "] 318 | "Stack trace of root exception is empty; this is likely due to a JVM optimization that can be disabled with " 319 | [:bold "-XX:-OmitStackTraceInFastThrow"] ".")) 320 | (flush)))) 321 | 322 | (defn transform-stack-trace 323 | "Transforms a seq of StackTraceElement objects into a seq of stack frame maps: 324 | 325 | Key | Type | Description 326 | --- |--- |--- 327 | :file | String | Source file name, or nil if not known 328 | :line | Integer | Line number as integer, or nil 329 | :class | String | Fully qualified Java class name 330 | :package | String | Java package name, or nil for root package 331 | :simple-class | String | Simple name of Java class, without the package prefix 332 | :method | String | Java method name 333 | :is-clojure? | Boolean | If true, this represents a Clojure function call, rather than a Java method invocation 334 | :id | String | An id that can be used to identify repeating stack frames; consists of the fully qualified method name (for Java frames) or fully qualified Clojure name (for Clojure frames) appended with the line number. 335 | :name | String | Fully qualified Clojure name (demangled from the Java class name), or the empty string for non-Clojure stack frames 336 | :names | seq of String | Clojure name split at slashes (empty for non-Clojure stack frames)" 337 | {:added "3.0.0"} 338 | [elements] 339 | (let [*cache (volatile! {})] 340 | (map #(transform-stack-trace-element current-dir-prefix *cache %) elements))) 341 | 342 | (defn expand-stack-trace 343 | "Extracts the stack trace for an exception and returns a seq of stack frame maps; a wrapper around 344 | [[transform-stack-trace]]." 345 | [^Throwable exception] 346 | (let [elements (.getStackTrace exception)] 347 | (when (empty? elements) 348 | @stack-trace-warning) 349 | (transform-stack-trace elements))) 350 | 351 | (defn- clj-frame-font-key 352 | "Returns the font key to use for a Clojure stack frame. 353 | 354 | When provided a frame matching *app-frame-names*, returns :app-frame, otherwise :clojure-frame." 355 | [frame] 356 | (or 357 | (when *app-frame-names* 358 | (reduce (fn [_ app-frame-name] 359 | (when-let [match (apply-rule frame [:name app-frame-name :app-frame])] 360 | (reduced match))) 361 | nil 362 | *app-frame-names*)) 363 | :clojure-frame)) 364 | 365 | (defn- counted-terms 366 | [terms] 367 | (if-not (seq terms) 368 | [] 369 | (loop [acc-term (first terms) 370 | acc-count 1 371 | ts (next terms) 372 | result []] 373 | (if (nil? ts) 374 | (conj result [acc-term acc-count]) 375 | (let [t (first ts) 376 | ts' (next ts)] 377 | (if (= acc-term t) 378 | (recur acc-term (inc acc-count) ts' result) 379 | (recur t 1 ts' (conj result [acc-term acc-count])))))))) 380 | 381 | (defn- counted-frame-name 382 | [[name count]] 383 | (if (= count 1) 384 | name 385 | (str name "{x" count "}"))) 386 | 387 | (defn- format-clojure-frame-base 388 | [frame] 389 | (let [names' (->> frame 390 | :names 391 | counted-terms 392 | (map counted-frame-name)) 393 | width (->> names' 394 | (map length) 395 | (reduce + 0) 396 | (+ (count names')) ;; each name has a trailing slash 397 | dec)] ;; except the last 398 | {:name-width width 399 | :name [(get *fonts* (clj-frame-font-key frame)) 400 | (->> names' drop-last (str/join "/")) 401 | "/" 402 | [(:function-name *fonts*) (last names')]]})) 403 | 404 | (defn format-stack-frame 405 | "Transforms an expanded stack frame (see [[transform-stack-trace]]) 406 | into a formatted stack frame: 407 | 408 | Key | Type | Description 409 | --- |--- |--- 410 | :name | composed string | Formatted version of the stack frame :name (or :names) 411 | :name-width | Integer | Visual width of the name 412 | :file | String | Location of source (or nil) 413 | :line | String | Location of source (or nil) 414 | :repeats | Integer | Number of times the frame repeats (or nil) 415 | 416 | Formatting is based on whether the frame is omitted, and whether it is a Clojure or Java frame." 417 | {:added "0.3.0"} 418 | [{:keys [file line names repeats] :as frame}] 419 | (cond 420 | (:omitted frame) 421 | {:name [(:omitted-frame *fonts*) "..."] 422 | :name-width 3} 423 | 424 | ;; When :names is empty, it's a Java (not Clojure) frame 425 | (empty? names) 426 | (let [full-name (str (:class frame) "." (:method frame))] 427 | {:name [(:java-frame *fonts*) full-name] 428 | :name-width (length full-name) 429 | :file file 430 | :line (str line) 431 | :repeats repeats}) 432 | 433 | :else 434 | (assoc (format-clojure-frame-base frame) 435 | :file file 436 | :line (str line) 437 | :repeats repeats))) 438 | 439 | (defn filter-stack-trace-maps 440 | "Filters the stack trace maps (from [[transform-stack-trace]], removing unnecessary frames and 441 | applying a filter and optional frame-limit (:filter and :frame-limit options). 442 | 443 | The default frame filter is [[*default-frame-filter*]]. 444 | 445 | Returns the elements, filtered, and (in some cases) with an additional :omitted key 446 | (true for frames that should be omitted). This includes discarding elements that 447 | the filter indicates to :hide, and coalescing frames the filter indicates to :omit." 448 | {:added "0.3.0"} 449 | ([elements] 450 | (filter-stack-trace-maps elements nil)) 451 | ([elements options] 452 | (let [frame-filter (:filter options *default-frame-filter*) 453 | frame-limit (:frame-limit options) 454 | elements' (->> elements 455 | remove-direct-link-frames 456 | (apply-frame-filter frame-filter) 457 | (reduce repeating-frame-reducer []))] 458 | (if frame-limit 459 | (take frame-limit elements') 460 | elements')))) 461 | 462 | (defn- extract-stack-trace 463 | [exception options] 464 | (filter-stack-trace-maps (expand-stack-trace exception) options)) 465 | 466 | (defn- is-throwable? 467 | [v] 468 | (instance? Throwable v)) 469 | 470 | (defn- wrap-exception 471 | [^Throwable exception properties options] 472 | (let [throwable-property-keys (match-keys properties is-throwable?) 473 | nested-exception (or (->> (select-keys properties throwable-property-keys) 474 | vals 475 | (remove nil?) 476 | ;; Avoid infinite loop! 477 | (remove #(= % exception)) 478 | first) 479 | (.getCause exception)) 480 | stack-trace (when-not nested-exception 481 | (extract-stack-trace exception options))] 482 | [{:class-name (-> exception .getClass .getName) 483 | :message (.getMessage exception) 484 | ;; Don't ever want to include throwables since they will wreck the output format. 485 | ;; Would only expect a single throwable (either an explicit property, or as the cause) 486 | ;; per exception. 487 | :properties (apply dissoc properties throwable-property-keys) 488 | :stack-trace stack-trace} 489 | nested-exception])) 490 | 491 | (defn- expand-exception 492 | [^Throwable exception options] 493 | (if (instance? ExceptionInfo exception) 494 | (wrap-exception exception (ex-data exception) options) 495 | (let [properties (try (into {} (bean exception)) 496 | (catch Throwable _ nil)) 497 | ;; Ignore basic properties of Throwable, any nil properties, and any properties 498 | ;; that are themselves Throwables 499 | discarded-keys (concat [:suppressed :message :localizedMessage :class :stackTrace :cause] 500 | (match-keys properties nil?) 501 | (match-keys properties is-throwable?)) 502 | retained-properties (apply dissoc properties discarded-keys)] 503 | (wrap-exception exception retained-properties options)))) 504 | 505 | (defn analyze-exception 506 | "Converts an exception into a seq of maps representing nested exceptions. 507 | The order reflects exception nesting; first exception is the most recently 508 | thrown, last is the deepest, or root, exception ... the initial exception 509 | thrown in a chain of nested exceptions. 510 | 511 | The options map is as defined by [[format-exception]]. 512 | 513 | Each exception map contains: 514 | 515 | Key | Type | Description 516 | --- |--- |--- 517 | :class-name | String | Name of Java class for the exception 518 | :message | String | Value of the exception's message property (possibly nil) 519 | :properties | String | Map of properties to (optionally) present in the exception report 520 | :stack-trace | Vector | Stack trace element maps (as per [[expand-stack-trace]]), or nil; only present in the root exception 521 | 522 | The :properties map does not include any properties that are assignable to type Throwable. 523 | 524 | The first property that is assignable to type Throwable (not necessarily the rootCause property) 525 | will be used as the nested exception (for the next map in the sequence)." 526 | [^Throwable e options] 527 | (loop [result [] 528 | current e] 529 | (let [[expanded nested] (expand-exception current options) 530 | result' (conj result expanded)] 531 | (if nested 532 | (recur result' nested) 533 | result')))) 534 | 535 | ;; Shadow Clojure 1.11's version, while keeping operational in 1.10. 536 | (defn- -update-keys 537 | "Builds a map where f has been applied to each key in m." 538 | [m f] 539 | (reduce-kv (fn [m k v] 540 | (assoc m (f k) v)) 541 | {} 542 | m)) 543 | 544 | (defn- max-from 545 | [coll k] 546 | (reduce max 0 (keep k coll))) 547 | 548 | (defn- build-stack-trace-output 549 | [stack-trace modern?] 550 | (let [source-font (:source *fonts*) 551 | rows (map format-stack-frame (?reverse modern? stack-trace)) 552 | max-name-width (max-from rows :name-width) 553 | ;; Allow for the colon in frames w/ a line number (this assumes there's at least one) 554 | max-file-width (inc (max-from rows #(-> % :file length))) 555 | max-line-width (max-from rows #(-> % :line length)) 556 | f (fn [{:keys [name file line repeats]}] 557 | (list 558 | [{:width max-name-width} name] 559 | " " 560 | [{:width max-file-width 561 | :font source-font} file] 562 | (when line ":") 563 | " " 564 | [{:width max-line-width} line] 565 | (when repeats 566 | [(:source *fonts*) 567 | (format " (repeats %,d times)" repeats)])))] 568 | (interpose "\n" (map f rows)))) 569 | 570 | (defmulti exception-dispatch 571 | "The pretty print dispatch function used when formatting exception output (specifically, when 572 | printing the properties of an exception). Normally, this is the same as the simple-dispatch 573 | (in clojure.pprint) but can be extended for specific cases: 574 | 575 | (import com.stuartsierra.component.SystemMap) 576 | 577 | (defmethod exception-dispatch SystemMap [system-map] (print \"#\")) 578 | 579 | This ensures that the SystemMap record, wherever it appears in the exception output, 580 | is represented as the string `#`; normally it would print as a deeply nested 581 | tree of maps. 582 | 583 | This same approach can be adapted to any class or type whose structure is problematic 584 | for presenting in the exception output, whether for size and complexity reasons, or due to 585 | security concerns." 586 | class) 587 | 588 | (defmethod exception-dispatch Object 589 | [object] 590 | (pp/simple-dispatch object)) 591 | 592 | (defmethod exception-dispatch nil 593 | [_] 594 | (pp/simple-dispatch nil)) 595 | 596 | (defn- indented-value 597 | [indentation s] 598 | (let [lines (str/split-lines s) 599 | sep (str "\n" (padding indentation))] 600 | (interpose sep lines))) 601 | 602 | (def ^{:added "2.5.0" 603 | :dynamic true} 604 | *print-length* 605 | "The number of elements of collections to pretty-print; defaults to 10." 606 | 10) 607 | 608 | (def ^{:added "2.5.0" 609 | :dynamic true} 610 | *print-level* 611 | "The depth to which to pretty-printed nested collections; defaults to 5." 612 | 5) 613 | 614 | 615 | (defn- format-property-value 616 | [indentation value] 617 | (let [pretty-value (pp/write value 618 | :stream nil 619 | :length *print-length* 620 | :level *print-level* 621 | :dispatch exception-dispatch)] 622 | (indented-value indentation pretty-value))) 623 | 624 | (defn- qualified-name 625 | [x] 626 | (if (instance? Named x) 627 | (let [x-ns (namespace x) 628 | x-name (name x)] 629 | (if x-ns 630 | (str x-ns "/" x-name) 631 | x-name)) 632 | x)) 633 | 634 | (defn- replace-nil 635 | [x] 636 | (if (nil? x) 637 | "nil" 638 | x)) 639 | 640 | (defn- render-exception 641 | [exception-stack options] 642 | (let [{show-properties? :properties 643 | :or {show-properties? true}} options 644 | exception-font (:exception *fonts*) 645 | message-font (:message *fonts*) 646 | property-font (:property *fonts*) 647 | modern? (not *traditional*) 648 | max-class-name-width (max-from exception-stack #(-> % :class-name length)) 649 | message-indent (+ 2 max-class-name-width) 650 | exception-f (fn [{:keys [class-name message properties]}] 651 | (list 652 | [{:width max-class-name-width 653 | :font exception-font} class-name] 654 | ":" 655 | (when message 656 | (list 657 | " " 658 | [message-font (indented-value message-indent message)])) 659 | (when (and show-properties? (seq properties)) 660 | (let [properties' (-update-keys properties (comp replace-nil qualified-name)) 661 | sorted-keys (cond-> (keys properties') 662 | (not (sorted? properties')) sort) 663 | max-key-width (max-from sorted-keys length) 664 | value-indent (+ 2 max-key-width)] 665 | (map (fn [k] 666 | (list "\n " 667 | [{:width max-key-width 668 | :font property-font} k] 669 | ": " 670 | [property-font 671 | (format-property-value value-indent (get properties' k))])) 672 | sorted-keys))) 673 | "\n")) 674 | exceptions (list 675 | (map exception-f (?reverse modern? exception-stack)) 676 | "\n") 677 | root-stack-trace (-> exception-stack last :stack-trace)] 678 | (list 679 | (when *traditional* 680 | exceptions) 681 | 682 | (build-stack-trace-output root-stack-trace modern?) 683 | "\n" 684 | 685 | (when modern? 686 | exceptions)))) 687 | 688 | (defn format-exception* 689 | "Contains the main logic for [[format-exception]], which simply expands 690 | the exception (via [[analyze-exception]]) before invoking this function." 691 | {:added "0.1.21"} 692 | [exception-stack options] 693 | (compose 694 | (render-exception exception-stack options))) 695 | 696 | (defn format-exception 697 | "Formats an exception, returning a single large string. 698 | 699 | By default, includes the stack trace, with no frame limit. 700 | 701 | The options map may have the following keys: 702 | 703 | Key | Description 704 | --- |--- 705 | :filter | The stack frame filter, which defaults to [[*default-stack-frame-filter*]] 706 | :properties | If true (the default) then properties of exceptions will be output 707 | :frame-limit | If non-nil, the number of stack frames to keep when outputting the stack trace of the deepest exception 708 | 709 | Output may be traditional or modern, as controlled by [[*traditional*]]. 710 | Traditional is the typical output order for Java: the stack of exceptions comes first (outermost to 711 | innermost) followed by the stack trace of the innermost exception, with the frames 712 | in order from deepest to most shallow. 713 | 714 | Modern output is more readable; the stack trace comes first and is reversed: shallowest frame to most deep. 715 | Then the exception stack is output, from the root exception to the outermost exception. 716 | The modern output order is more readable, as it puts the most useful information together at the bottom, so that 717 | it is not necessary to scroll back to see, for example, where the exception occurred. 718 | 719 | The default is modern. 720 | 721 | The stack frame filter is passed the map detailing each stack frame 722 | in the stack trace, and must return one of the following values: 723 | 724 | Value | Description 725 | --- |--- 726 | :show | The normal state; display the stack frame 727 | :hide | Prevents the frame from being displayed, as if it never existed 728 | :omit | Replaces the frame with a \"...\" placeholder 729 | :terminate | Hides the frame AND all later frames 730 | 731 | Multiple consecutive :omits will be collapsed to a single line; use :omit for \"uninteresting\" stack frames. 732 | 733 | The default filter is [[*default-frame-filter*]]. An explicit filter of nil will display all stack frames. 734 | 735 | Repeating lines are collapsed to a single line, with a repeat count. Typically, this is the result of 736 | an endless loop that terminates with a StackOverflowException. 737 | 738 | When set, the frame limit is the number of stack frames to display; if non-nil, then some outermost 739 | stack frames may be omitted. It may be set to 0 to omit the stack trace entirely (but still display 740 | the exception stack). The frame limit is applied after the frame filter (which may hide or omit frames) and 741 | after repeating stack frames have been identified and coalesced ... :frame-limit is really the number 742 | of _output_ lines to present. 743 | 744 | Properties of exceptions will be output using Clojure's pretty-printer, but using 745 | this namespace's versions of [[*print-length*]] and [[*print-level*]], which default to 746 | 10 and 5, respectively. 747 | 748 | The `*fonts*` var contains a map from output element names (as :exception or :clojure-frame) to 749 | a font def used with [[compose]]; this allows easy customization of the output." 750 | (^String [exception] 751 | (format-exception exception nil)) 752 | (^String [exception options] 753 | (format-exception* (analyze-exception exception options) options))) 754 | 755 | (defn print-exception 756 | "Formats an exception via [[format-exception]], then prints it to `*out*`. Accepts the same options as `format-exception`." 757 | ([exception] 758 | (print-exception exception nil)) 759 | ([exception options] 760 | (print (format-exception exception options)) 761 | (flush))) 762 | 763 | (defn- assemble-final-stack 764 | [exceptions stack-trace stack-trace-batch options] 765 | (let [*cache (volatile! {}) 766 | stack-trace' (-> (map #(transform-stack-trace-element current-dir-prefix *cache %) 767 | (into stack-trace-batch stack-trace)) 768 | (filter-stack-trace-maps options)) 769 | x (-> exceptions count dec)] 770 | (assoc-in exceptions [x :stack-trace] stack-trace'))) 771 | 772 | (def ^:private re-exception-start 773 | "The start of an exception, possibly the outermost exception." 774 | #"(Caused by: )?(\w+(\.\w+)*): (.*)" 775 | ; Group 2 - exception name 776 | ; Group 4 - exception message 777 | ) 778 | 779 | (def ^:private re-stack-frame 780 | ;; Sometimes the file name and line number are replaced with "Unknown source" 781 | #"\s+at ([a-zA-Z_.$\d<>]+)\(((.+):(\d+))?.*\).*" 782 | ; Group 1 - class and method name 783 | ; Group 3 - file name (or nil) 784 | ; Group 4 - line number (or nil) 785 | ) 786 | 787 | (defn- add-message-text 788 | [exceptions line] 789 | (let [x (-> exceptions count dec)] 790 | (update-in exceptions [x :message] 791 | str \newline line))) 792 | 793 | (defn- add-to-batch 794 | [stack-trace-batch ^String class-and-method ^String file-name ^String line-number] 795 | (try 796 | (let [x (.lastIndexOf class-and-method ".") 797 | class-name (subs class-and-method 0 x) 798 | method-name (subs class-and-method (inc x)) 799 | element (StackTraceElement. class-name 800 | method-name 801 | file-name 802 | (if line-number 803 | (Integer/parseInt line-number) 804 | -1))] 805 | (conj stack-trace-batch element)) 806 | (catch Throwable t 807 | (throw (ex-info "Unable to create StackTraceElement." 808 | {:class-and-method class-and-method 809 | :file-name file-name 810 | :line-number line-number} 811 | t))))) 812 | 813 | (defn parse-exception 814 | "Given a chunk of text from an exception report (as with `.printStackTrace`), attempts to 815 | piece together the same information provided by [[analyze-exception]]. The result 816 | is ready to pass to [[write-exception*]]. 817 | 818 | This code does not attempt to recreate properties associated with the exceptions; in most 819 | exception's cases, this is not necessarily written to the output. For clojure.lang.ExceptionInfo, 820 | it is hard to distinguish the message text from the printed exception map. 821 | 822 | The options are used when processing the stack trace and may include the :filter and :frame-limit keys. 823 | 824 | Returns a sequence of exception maps; the final map will include the :stack-trace key (a vector 825 | of stack trace element maps). The exception maps are ordered outermost to innermost (that final map 826 | is the root exception). 827 | 828 | This should be considered experimental code; there are many cases where it may not work properly. 829 | 830 | It will work quite poorly with exceptions whose message incorporates a nested exception's 831 | .printStackTrace output. This happens too often with JDBC exceptions, for example." 832 | {:added "0.1.21"} 833 | [exception-text options] 834 | (loop [state :start 835 | lines (str/split-lines exception-text) 836 | exceptions [] 837 | stack-trace [] 838 | stack-trace-batch []] 839 | (if (empty? lines) 840 | (assemble-final-stack exceptions stack-trace stack-trace-batch options) 841 | (let [[line & more-lines] lines] 842 | (condp = state 843 | 844 | :start 845 | (let [[_ _ exception-class-name _ exception-message] (re-matches re-exception-start line)] 846 | (when-not exception-class-name 847 | (throw (ex-info "Unable to parse start of exception." 848 | {:line line 849 | :exception-text exception-text}))) 850 | 851 | ;; The exception message may span a couple of lines, so check for that before absorbing 852 | ;; more stack trace 853 | (recur :exception-message 854 | more-lines 855 | (conj exceptions {:class-name exception-class-name 856 | :message exception-message}) 857 | stack-trace 858 | stack-trace-batch)) 859 | 860 | :exception-message 861 | (if (re-matches re-stack-frame line) 862 | (recur :stack-frame lines exceptions stack-trace stack-trace-batch) 863 | (recur :exception-message 864 | more-lines 865 | (add-message-text exceptions line) 866 | stack-trace 867 | stack-trace-batch)) 868 | 869 | :stack-frame 870 | (let [[_ class-and-method _ file-name line-number] (re-matches re-stack-frame line)] 871 | (if class-and-method 872 | (recur :stack-frame 873 | more-lines 874 | exceptions 875 | stack-trace 876 | (add-to-batch stack-trace-batch class-and-method file-name line-number)) 877 | (recur :skip-more-line 878 | lines 879 | exceptions 880 | ;; With the weird ordering of the JDK, what we see is 881 | ;; a batch of entries that actually precede frames from earlier 882 | ;; in the output (because JDK tries to present the exceptions outside in). 883 | ;; This inner exception and its abbreviated stack trace represents 884 | ;; progress downward from the previously output exception. 885 | (into stack-trace-batch stack-trace) 886 | []))) 887 | 888 | :skip-more-line 889 | (if (re-matches #"\s+\.\.\. \d+ (more|common frames omitted)" line) 890 | (recur :start more-lines 891 | exceptions stack-trace stack-trace-batch) 892 | (recur :start lines 893 | exceptions stack-trace stack-trace-batch))))))) 894 | 895 | (defn format-stack-trace-element 896 | "Formats a stack trace element into a single string identifying the Java method or Clojure function being executed." 897 | {:added "3.2.0"} 898 | [^StackTraceElement e] 899 | (let [{:keys [class method names]} (transform-stack-trace-element current-dir-prefix (volatile! {}) e)] 900 | (if (empty? names) 901 | (str class "." method) 902 | (->> names counted-terms (map counted-frame-name) (str/join "/"))))) 903 | -------------------------------------------------------------------------------- /src/clj_commons/format/table.clj: -------------------------------------------------------------------------------- 1 | ; Copyright 2024-2025 Nubank NA 2 | ; 3 | ; The use and distribution terms for this software are covered by the 4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) 5 | ; which can be found in the file epl-v10.html at the root of this distribution. 6 | ; 7 | ; By using this software in any fashion, you are agreeing to be bound by 8 | ; the terms of this license. 9 | ; 10 | ; You must not remove this notice, or any other, from this software. 11 | 12 | ;; NOTE: This code briefly originated in io.pedestal/pedestal, which uses the EPL 13 | ;; license. 14 | 15 | (ns clj-commons.format.table 16 | "Formatted tabular output, similar to (but much prettier and more flexible than) 17 | clojure.pprint/print-table. 18 | 19 | Specs are in [[clj-commons.format.table.specs]]." 20 | {:added "2.3"} 21 | (:require [clojure.string :as string] 22 | [clj-commons.ansi :refer [pout]])) 23 | 24 | (defn- make-bar 25 | [width s] 26 | (let [b (StringBuilder. (int width))] 27 | (while (< (.length b) width) 28 | (.append b s)) 29 | (.toString b))) 30 | 31 | (defn- default-title 32 | [key] 33 | (-> key name (string/replace "-" " ") string/capitalize)) 34 | 35 | (defn- expand-column 36 | [column] 37 | (cond 38 | (keyword? column) 39 | {:key column 40 | :title (default-title column)} 41 | 42 | (-> column :title nil?) 43 | (assoc column 44 | :title (-> column :key default-title)) 45 | 46 | :else 47 | column)) 48 | 49 | (defn- set-width 50 | [column data] 51 | (let [{:keys [key ^String title width]} column 52 | title-width (.length title) 53 | width' (if width 54 | (max width title-width) 55 | (->> data 56 | (map key) 57 | (map str) 58 | (map #(.length %)) 59 | (reduce max title-width)))] 60 | (assoc column :width width'))) 61 | 62 | (def default-style 63 | "Default style, with thick borders (using character graphics) and a header and footer." 64 | {:hbar "━" 65 | :header? true 66 | :header-left "┏━" 67 | :header-sep "━┳━" 68 | :header-right "━┓" 69 | :divider-left "┣━" 70 | :divider-sep "━╋━" 71 | :divider-right "━┫" 72 | :row-left "┃ " 73 | :row-sep " ┃ " 74 | :row-right " ┃" 75 | :footer? true 76 | :footer-left "┗━" 77 | :footer-sep "━┻━" 78 | :footer-right "━┛"}) 79 | 80 | (def skinny-style 81 | "Removes most of the borders and uses simple characters for column separators." 82 | {:hbar "-" 83 | :header? false 84 | :divider-left nil 85 | :divider-sep "-+-" 86 | :divider-right nil 87 | :row-left nil 88 | :row-sep " | " 89 | :row-right nil 90 | :footer? false}) 91 | 92 | (defn print-table 93 | "Similar to clojure.pprint/print-table, but with fancier graphics and more control 94 | over column titles. 95 | 96 | The rows are a seq of associative values, usually maps. 97 | 98 | In simple mode, each column is just a keyword; the column title is derived 99 | from the keyword, and the column's width is set to the maximum 100 | of the title width and the width of the longest value in the rows. 101 | 102 | Alternately, a column can be a map: 103 | 104 | Key | Type | Description 105 | --- |--- |--- 106 | :key | keyword/function | Passed the row data and returns the value for the column (required) 107 | :title | String | The title for the column 108 | :title-pad | :left, :right, :both | How to pad the title column; default is :both to center the title 109 | :width | number | Width of the column 110 | :decorator | function | May return a font declaration for the cell 111 | :pad | :left, :right, :both | Defaults to :left except for last column, which pads on the right 112 | 113 | :key is typically a keyword but can be an arbitrary function 114 | (in which case, you must also provide :title). The return 115 | value is a composed string (passed to [[compose]]); if returning a composed string, 116 | you must also provide an explicit :width. 117 | 118 | The default for :title is deduced from :key; when omitted and :key is a keyword; 119 | the keyword is converted to a string, capitalized, and embedded dashes 120 | converted to spaces. 121 | 122 | :width will be determined as the maximum width of the title or of any 123 | value in the data. 124 | 125 | The decorator is a function; it will be 126 | passed the row index and the value for the column, 127 | and returns a font declaration (or nil). A font declaration can be a single keyword 128 | (.e.g, :red.bold) or a vector of keywords (e.g. [:red :bold]). 129 | 130 | opts can be a seq of columns, or it can be a map of options: 131 | 132 | Key | Type | Description 133 | --- |--- |--- 134 | :columns | seq of columns | Describes the columns to print 135 | :style | map | Overrides the default styling of the table 136 | :default-decorator | function | Used when a column doesn't define it own decorator 137 | :row-annotator | function | Can add text immediately after the end of the row 138 | 139 | :default-decorator is only used for columns that do not define their own 140 | decorator. This can be used, for example, to alternate the background color 141 | of cells. 142 | 143 | The :row-annotator is passed the row index and the row data, 144 | and returns a composed string that is appended immediately after 145 | the end of the row (but outside any border), which can be used to 146 | add a note to the right of a row." 147 | [opts rows] 148 | (let [opts' (if (sequential? opts) 149 | {:columns opts} 150 | opts) 151 | {:keys [columns style default-decorator row-annotator] 152 | :or {style default-style}} opts' 153 | {:keys [header? 154 | footer? 155 | header-left 156 | header-sep 157 | header-right 158 | divider-left 159 | divider-sep 160 | divider-right 161 | row-left 162 | row-sep 163 | row-right 164 | footer-left 165 | footer-sep 166 | footer-right 167 | hbar]} style 168 | last-column-index (dec (count columns)) 169 | columns' (->> columns 170 | (map expand-column) 171 | (map #(set-width % rows)) 172 | (map-indexed #(assoc %2 :index %1)) 173 | (map (fn [col] 174 | (assoc col 175 | :last? (= last-column-index (:index col)) 176 | :bar (make-bar (:width col) hbar)))))] 177 | (when header? 178 | (pout 179 | header-left 180 | (for [{:keys [last? bar]} columns'] 181 | (list bar 182 | (when-not last? 183 | header-sep))) 184 | header-right)) 185 | 186 | (pout 187 | row-left 188 | (for [{:keys [width title title-pad last?]} columns'] 189 | (list [{:width width 190 | :pad (or title-pad :both) 191 | :font :bold} title] 192 | (when-not last? 193 | row-sep))) 194 | row-right) 195 | 196 | (pout 197 | divider-left 198 | (for [{:keys [bar last?]} columns'] 199 | (list bar 200 | (when-not last? 201 | divider-sep))) 202 | divider-right) 203 | 204 | (when (seq rows) 205 | (loop [[row & more-rows] rows 206 | row-index 0] 207 | (pout 208 | row-left 209 | (for [{:keys [width key decorator last? pad]} columns' 210 | :let [value (key row) 211 | decorator' (or decorator default-decorator) 212 | font (when decorator' 213 | (decorator' row-index value))]] 214 | (list [{:font font 215 | :pad (or pad (if last? :right :left)) 216 | :width width} 217 | value] 218 | (when-not last? 219 | row-sep))) 220 | row-right 221 | (when row-annotator 222 | (row-annotator row-index row))) 223 | (when more-rows 224 | (recur more-rows (inc row-index))))) 225 | 226 | (when footer? 227 | (print footer-left) 228 | (doseq [{:keys [bar last?]} columns'] 229 | (print bar) 230 | (when-not last? 231 | (print footer-sep))) 232 | (println footer-right)))) 233 | 234 | -------------------------------------------------------------------------------- /src/clj_commons/format/table/specs.clj: -------------------------------------------------------------------------------- 1 | ; Copyright 2024 Nubank NA 2 | ; 3 | ; The use and distribution terms for this software are covered by the 4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0) 5 | ; which can be found in the file epl-v10.html at the root of this distribution. 6 | ; 7 | ; By using this software in any fashion, you are agreeing to be bound by 8 | ; the terms of this license. 9 | ; 10 | ; You must not remove this notice, or any other, from this software. 11 | 12 | ;; NOTE: This code briefly originated in io.pedestal/pedestal, which uses the EPL 13 | ;; license. 14 | 15 | (ns clj-commons.format.table.specs 16 | (:require [clojure.spec.alpha :as s] 17 | [clj-commons.format.table :refer [print-table]])) 18 | 19 | (s/fdef print-table 20 | :args (s/cat :columns 21 | (s/or :columns ::columns 22 | :opts ::options) 23 | :data (s/coll-of map?))) 24 | 25 | (s/def ::options (s/keys :req-un [::columns] 26 | :opt-un [::style 27 | ::default-decorator 28 | ::row-annotator])) 29 | 30 | (s/def ::row-annotator 31 | (s/fspec 32 | :args (s/cat 33 | :index int? 34 | :value any?) 35 | :ret any?)) 36 | 37 | ;; Not a lot of gain for breaking down what's in a style map. 38 | (s/def ::style map?) 39 | 40 | (s/def ::columns (s/coll-of ::column)) 41 | (s/def ::column 42 | (s/or :simple keyword? 43 | :full ::column-full)) 44 | 45 | (s/def ::column-full 46 | (s/keys :req-un [::key] 47 | :opt-un [::title 48 | ::width 49 | ::decorator 50 | ::pad])) 51 | 52 | (s/def ::pad #{:left :right}) 53 | 54 | (s/def ::key ifn?) 55 | (s/def ::title string?) 56 | (s/def ::width (s/and int? pos?)) 57 | (s/def ::font-declaration (s/or 58 | :keyword keyword? 59 | :vector (s/coll-of (s/nilable keyword?) 60 | :kind vector?))) 61 | (s/def ::decorator (s/fspec 62 | :args (s/cat 63 | :index int? 64 | :value any?) 65 | :ret (s/nilable ::font-declarationse))) 66 | (s/def ::default-decorator ::decorator) 67 | -------------------------------------------------------------------------------- /src/clj_commons/pretty/annotations.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.pretty.annotations 2 | "Tools to annotate a line of source code, in the form of callouts (lines and arrows) connected to a message. 3 | 4 | SELECT DATE, AMT FROM PAYMENTS WHEN AMT > 10000 5 | ▲▲▲ ▲▲▲▲ 6 | │ │ 7 | │ └╴ Unknown token 8 | │ 9 | └╴ Invalid column name 10 | 11 | This kind of output is common with various kinds of parsers or interpreters. 12 | 13 | Specs for types and functions are in the [[spec]] namespace." 14 | {:added "3.3.0"}) 15 | 16 | (def default-style 17 | "The default style used when generating callouts. 18 | 19 | Key | Default | Description 20 | --- |--- |--- 21 | :font | :yellow | Default font characteristics if not overrided by annotation 22 | :spacing | :tall | One of :tall, :compact, or :minimal 23 | :marker | \"▲\" | The marker character used to identify the offset/length of an annotation 24 | :bar | \"│\" | Character used as the vertical bar in the callout 25 | :nib | \"└╴ \" | String used just before the annotation's message 26 | 27 | When :spacing is :minimal, only the lines with markers or error messages appear 28 | (the lines with just vertical bars are omitted). :compact spacing is the same, but 29 | one line of bars appears between the markers and the first annotation message. 30 | 31 | Note: rendering of Unicode characters in HTML often uses incorrect fonts or adds unwanted 32 | character spacing; the annotations look proper in console output." 33 | {:font :yellow 34 | :spacing :tall 35 | :marker "▲" 36 | :bar "│" 37 | :nib "└╴ "}) 38 | 39 | (def ^:dynamic *default-style* 40 | "The default style used when no style is provided; some applications may bind or 41 | override this." 42 | default-style) 43 | 44 | (defn- nchars 45 | [n ch] 46 | (apply str (repeat n ch))) 47 | 48 | (defn- markers 49 | [style annotations] 50 | (let [{:keys [font marker]} style] 51 | (loop [output-offset 0 52 | annotations annotations 53 | result [font]] 54 | (if-not annotations 55 | result 56 | (let [{:keys [offset length font] 57 | :or {length 1}} (first annotations) 58 | spaces-needed (- offset output-offset) 59 | result' (conj result 60 | (nchars spaces-needed \space) 61 | [font (nchars length marker)])] 62 | (recur (+ offset length) 63 | (next annotations) 64 | result')))))) 65 | 66 | (defn- bars 67 | [style annotations] 68 | (let [{:keys [font bar]} style] 69 | (loop [output-offset 0 70 | annotations annotations 71 | result [font]] 72 | (if-not annotations 73 | result 74 | (let [{:keys [offset font]} (first annotations) 75 | spaces-needed (- offset output-offset) 76 | result' (conj result 77 | (nchars spaces-needed \space) 78 | [font bar])] 79 | (recur (+ offset 1) 80 | (next annotations) 81 | result')))))) 82 | 83 | (defn- bars+message 84 | [style annotations] 85 | (let [{:keys [font bar nib]} style] 86 | (loop [output-offset 0 87 | [annotation & more-annotations] annotations 88 | result [font]] 89 | (let [{:keys [offset font message]} annotation 90 | spaces-needed (- offset output-offset) 91 | last? (not (seq more-annotations)) 92 | result' (conj result 93 | (nchars spaces-needed \space) 94 | [font 95 | (if last? 96 | nib 97 | bar) 98 | (when last? 99 | message)])] 100 | (if last? 101 | result' 102 | (recur (+ offset 1) 103 | more-annotations 104 | result')))))) 105 | 106 | (defn callouts 107 | "Creates callouts (the marks, bars, and messages from the example) from annotations. 108 | 109 | Each annotation is a map: 110 | 111 | Key | Description 112 | --- |--- 113 | :message | Composed string of the message to present 114 | :offset | Integer position (from 0) to mark on the line 115 | :length | Number of characters in the marker (min 1, defaults to 1) 116 | :font | Override of the style's font; used for marker, bars, nib, and message 117 | 118 | The leftmost column has offset 0; some frameworks may report this as column 1 119 | and an adjustment is necessary before invoking callouts. 120 | 121 | At least one annotation is required; they will be sorted into an appropriate order. 122 | Annotation's ranges should not overlap. 123 | 124 | The messages should be relatively short, and not contain any line breaks. 125 | 126 | Returns a sequence of composed strings, one for each line of output. 127 | 128 | The calling code is responsible for any output; even the line being annotated; 129 | this might look something like: 130 | 131 | (ansi/perr source-line) 132 | (run! ansi/perr (annotations/annotate annotations)) 133 | 134 | Uses the style defined by [[*default-style*]] if no style is provided." 135 | ([annotations] 136 | (callouts *default-style* annotations)) 137 | ([style annotations] 138 | ;; TODO: Check for overlaps 139 | (let [expanded (sort-by :offset annotations) 140 | {:keys [spacing]} style 141 | marker-line (markers style expanded)] 142 | (loop [annotations expanded 143 | first? true 144 | result [marker-line]] 145 | (let [include-bars? (or (= spacing :tall) 146 | (and first? (= spacing :compact))) 147 | result' (conj result 148 | (when include-bars? 149 | (bars style annotations)) 150 | (bars+message style annotations)) 151 | annotations' (butlast annotations)] 152 | (if (seq annotations') 153 | (recur annotations' false result') 154 | (remove nil? result'))))))) 155 | 156 | (defn annotate-lines 157 | "Intersperses numbered lines with callouts to form a new sequence 158 | of composable strings where input lines are numbered, and 159 | callout lines are indented beneath the input lines. 160 | 161 | Example: 162 | 163 | ``` 164 | 1: SELECT DATE, AMT 165 | ▲▲▲ 166 | │ 167 | └╴ Invalid column name 168 | 2: FROM PAYMENTS WHEN AMT > 10000 169 | ▲▲▲▲ 170 | │ 171 | └╴ Unknown token 172 | ``` 173 | Each line is a map: 174 | 175 | Key | Value 176 | --- |--- 177 | :line | Composed string for a single line of input (usually, just a string) 178 | :annotations | Optional, a seq of annotation maps (used to create the callouts) 179 | 180 | Option keys are all optional: 181 | 182 | Key | Value 183 | --- |--- 184 | :style | style map (for callouts), defaults to [*default-style*] 185 | :start-line | Defaults to 1 186 | :line-number-width | Width for the line numbers column 187 | 188 | The :line-number-width option is usually computed from the maximum line number 189 | that will be output. 190 | 191 | Returns a seq of composed strings." 192 | ([lines] 193 | (annotate-lines nil lines)) 194 | ([opts lines] 195 | (let [{:keys [style start-line] 196 | :or {style *default-style* 197 | start-line 1}} opts 198 | max-line-number (+ start-line (count lines) -1) 199 | ;; inc by one to account for the ':' 200 | line-number-width (inc (or (:line-number-width opts) 201 | (-> max-line-number str count))) 202 | callout-indent (repeat (nchars (inc line-number-width) " "))] 203 | (loop [[line-data & more-lines] lines 204 | line-number start-line 205 | result []] 206 | (if-not line-data 207 | result 208 | (let [{:keys [line annotations]} line-data 209 | callout-lines (when (seq annotations) 210 | (callouts style annotations)) 211 | result' (cond-> (conj result 212 | (list 213 | [{:width line-number-width} 214 | line-number ":"] 215 | " " 216 | line)) 217 | callout-lines (into 218 | (map list callout-indent callout-lines)))] 219 | (recur more-lines (inc line-number) result'))))))) 220 | 221 | -------------------------------------------------------------------------------- /src/clj_commons/pretty/repl.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.pretty.repl 2 | "Utilities to assist with REPL-oriented development." 3 | (:require [clj-commons.format.exceptions :as e :refer [print-exception]] 4 | [clojure.main :as main] 5 | [clojure.repl :as repl] 6 | [clojure.stacktrace :as st]) 7 | (:import (clojure.lang RT) 8 | (java.io Writer))) 9 | 10 | (defn- reset-var! 11 | [v override] 12 | (alter-var-root v (constantly override))) 13 | 14 | (defn pretty-repl-caught 15 | "A replacement for `clojure.main/repl-caught` that prints the exception to `*err*`, without a stack trace or properties." 16 | [e] 17 | (binding [*out* *err*] 18 | (print-exception e {:frame-limit 0 :properties false}))) 19 | 20 | (defn uncaught-exception-handler 21 | "Returns a reified UncaughtExceptionHandler that prints the formatted exception to `*err*`." 22 | {:added "0.1.18"} 23 | [] 24 | (reify Thread$UncaughtExceptionHandler 25 | (uncaughtException [_ _ t] 26 | (binding [*out* *err*] 27 | (printf "Uncaught exception in thread %s:%n%s%n" 28 | (-> (Thread/currentThread) .getName) 29 | (e/format-exception t)) 30 | (flush))))) 31 | 32 | 33 | (defn pretty-pst 34 | "Used as an override of `clojure.repl/pst` but uses pretty formatting. Output is written to `*err*`." 35 | ([] (pretty-pst *e)) 36 | ([e-or-depth] 37 | (if (instance? Throwable e-or-depth) 38 | (pretty-pst e-or-depth nil) 39 | (pretty-pst *e e-or-depth))) 40 | ([e depth] 41 | (binding [*out* *err*] 42 | (print-exception e (when depth 43 | {:frame-limit depth}))))) 44 | 45 | (defn pretty-print-stack-trace 46 | "Replacement for `clojure.stacktrace/print-stack-trace` and `print-cause-trace`. These functions are used by `clojure.test`." 47 | ([tr] (pretty-print-stack-trace tr nil)) 48 | ([tr n] 49 | (println) 50 | (print-exception tr {:frame-limit n}))) 51 | 52 | (defn install-pretty-exceptions 53 | "Installs an override that outputs pretty exceptions when caught by the main REPL loop. Also, overrides 54 | `clojure.repl/pst`, `clojure.stacktrace/print-stack-trace`, `clojure.stacktrace/print-cause-trace`. 55 | 56 | Extends `clojure.core/print-method` for type Throwable to print a blank line followed by the 57 | formatted exception. This allows an expression that evaluates to an exception to be printed prettily, 58 | but more importantly, ensures that in `clojure.test/is` a failed `thrown-with-msg?` assertion 59 | prints a formatted exception. 60 | 61 | Finally, installs an [[uncaught-exception-handler]] so that uncaught exceptions in non-REPL threads 62 | will be printed reasonably. 63 | 64 | Caught exceptions do not print the stack trace; the pretty replacement for `pst` does." 65 | [] 66 | (reset-var! #'main/repl-caught pretty-repl-caught) 67 | (reset-var! #'repl/pst pretty-pst) 68 | (reset-var! #'st/print-stack-trace pretty-print-stack-trace) 69 | (reset-var! #'st/print-cause-trace pretty-print-stack-trace) 70 | 71 | (defmethod print-method Throwable 72 | [t ^Writer writer] 73 | (.write writer ^String (System/lineSeparator)) 74 | (.write writer (e/format-exception t))) 75 | 76 | ;; This is necessary due to direct linking (from clojure.test to clojure.stacktrace). 77 | (RT/loadResourceScript "clojure/test.clj") 78 | 79 | (Thread/setDefaultUncaughtExceptionHandler (uncaught-exception-handler)) 80 | nil) 81 | 82 | (defn -main 83 | "Installs pretty exceptions, then delegates to clojure.main/main." 84 | {:added "1.3.0"} 85 | [& args] 86 | (install-pretty-exceptions) 87 | (apply main/main args)) 88 | -------------------------------------------------------------------------------- /src/clj_commons/pretty/spec.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.pretty.spec 2 | (:require [clojure.spec.alpha :as s] 3 | [clj-commons.ansi :as ansi] 4 | [clj-commons.pretty.annotations :as ann])) 5 | 6 | (s/def ::nonneg-integer (s/and integer? #(<= 0 %))) 7 | 8 | (s/def ::positive-integer (s/and integer? pos?)) 9 | 10 | 11 | (s/def ::single-character (s/or 12 | :char char? 13 | :string (s/and string? 14 | #(= 1 (count %))))) 15 | 16 | ;; clj-commons.ansi: 17 | 18 | (s/def ::ansi/composed-string (s/or 19 | :string string? 20 | :nil nil? 21 | :span ::ansi/span 22 | :sequential ::ansi/composed-strings 23 | :other any?)) 24 | 25 | (s/def ::ansi/span (s/and vector? 26 | (s/cat 27 | :font ::ansi/span-font 28 | :span-body (s/* ::ansi/composed-string)))) 29 | 30 | (s/def ::ansi/span-font (s/or 31 | :nil nil? 32 | :font ::ansi/font 33 | :full ::ansi/span-font-full)) 34 | 35 | (s/def ::ansi/font (s/or 36 | :simple keyword? 37 | :list (s/coll-of keyword? :kind vector?))) 38 | 39 | (s/def ::ansi/span-font-full (s/keys 40 | :opt-un [::ansi/font ::ansi/width ::ansi/pad])) 41 | 42 | (s/def ::ansi/width ::positive-integer) 43 | 44 | (s/def ::ansi/pad #{:left :right :both}) 45 | 46 | (s/def ::ansi/composed-strings (s/and sequential? 47 | (s/* ::ansi/composed-string))) 48 | 49 | (s/fdef ansi/compose 50 | :args (s/* ::ansi/composed-string) 51 | :ret string?) 52 | 53 | (s/fdef ansi/pout 54 | :args (s/* ::ansi/composed-string) 55 | :ret nil?) 56 | 57 | (s/fdef ansi/pcompose ; old name of pout 58 | :args (s/* ::ansi/composed-string) 59 | :ret nil?) 60 | 61 | (s/fdef ansi/perr 62 | :args (s/* ::ansi/composed-string) 63 | :ret nil?) 64 | 65 | ;; clj-commons.pretty.annotations 66 | 67 | (s/fdef ann/callouts 68 | :args (s/cat 69 | :style (s/? ::ann/style) 70 | :annotations ::ann/annotations) 71 | :ret (s/coll-of ::ansi/composed-string)) 72 | 73 | (s/def ::ann/style (s/keys :req-un [::ansi/font 74 | ::ann/spacing 75 | ::ann/marker 76 | ::ann/bar 77 | ::ann/nib])) 78 | 79 | (s/def ::ann/spacing #{:tall :compact :minimal}) 80 | (s/def ::ann/marker ::single-character) 81 | (s/def ::ann/bar ::single-character) 82 | (s/def ::ann/nib ::ansi/composed-string) 83 | 84 | (s/def ::ann/annotations (s/coll-of ::ann/annotation)) 85 | 86 | (s/def ::ann/annotation (s/keys :req-un [::ann/message 87 | ::ann/offset] 88 | :opt-un [::ann/length 89 | ::ansi/font])) 90 | 91 | (s/def ::ann/message ::ansi/composed-string) 92 | (s/def ::ann/offset ::nonneg-integer) 93 | (s/def ::ann/length ::positive-integer) 94 | 95 | (s/def ::ann/annotate-lines-opts (s/keys :opt-un [::ann/style 96 | ::ann/start-line 97 | ::ann/line-number-width])) 98 | 99 | (s/def ::ann/line-number-width ::positive-integer) 100 | (s/def ::ann/start-line ::positive-integer) 101 | 102 | (s/def ::ann/lines (s/coll-of ::ann/line-data)) 103 | 104 | (s/def ::ann/line-data (s/keys :req-un [::ann/line] 105 | :opt-un [::ann/annotations])) 106 | 107 | (s/def ::ann/line ::ansi/composed-string) 108 | 109 | (s/fdef ann/annotate-lines 110 | :args (s/cat 111 | :opts (s/? (s/nilable ::ann/annotate-lines-opts)) 112 | :lines ::ann/lines) 113 | :ret (s/coll-of ::ansi/composed-string)) 114 | -------------------------------------------------------------------------------- /src/clj_commons/pretty_impl.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc clj-commons.pretty-impl 2 | "Private/internal - subject to change without notice.") 3 | 4 | (defn padding 5 | ^String [x] 6 | (when (pos? x) 7 | (let [sb (StringBuilder. (int x))] 8 | (dotimes [_ x] 9 | (.append sb " ")) 10 | (.toString sb)))) 11 | 12 | (def ^:const csi 13 | "The control sequence initiator: `ESC [`" 14 | "\u001b[") 15 | -------------------------------------------------------------------------------- /test/clj_commons/ansi_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.ansi-test 2 | (:require [clj-commons.ansi :as ansi] 3 | [clj-commons.test-common :as tc] 4 | [clojure.string :as str] 5 | [clojure.test :refer [deftest is are use-fixtures]] 6 | [clj-commons.ansi :refer [compose *color-enabled*]] 7 | [clj-commons.pretty-impl :refer [csi]])) 8 | 9 | (use-fixtures :once tc/spec-fixture) 10 | 11 | (deftest sanity-check 12 | (is (= true *color-enabled*))) 13 | 14 | (defn- safe-compose 15 | [input] 16 | (-> (apply compose input) 17 | (str/replace csi "[CSI]"))) 18 | 19 | (deftest nested-widths 20 | (is (= "xyz <( left)right >" 21 | ; .......... 22 | ; ....!....! [10] 23 | ; xxxxXxxxxXxxxxXxxxxXxxxxXxxxxX [30] 24 | (compose "xyz <" 25 | [{:width 30 26 | :pad :right} 27 | (list "(" 28 | [{:width 10} 29 | "left"] 30 | ")") 31 | "right"] 32 | ">")))) 33 | 34 | (deftest invalid-font-decl 35 | (when-let [e (is (thrown? Exception 36 | (compose "START" 37 | ["A" "B" "C"])))] 38 | (is (= "invalid span declaration" 39 | (ex-message e))) 40 | (is (= {:font-decl "A"} 41 | (ex-data e))))) 42 | 43 | (deftest compose-test 44 | (are [input expected] 45 | (= expected (safe-compose input)) 46 | 47 | ["Simple"] 48 | "Simple" 49 | 50 | 51 | ["String" \space :keyword \space 'symbol \space 123 \space 44.5] 52 | "String :keyword symbol 123 44.5" 53 | 54 | ;; Handles nested lists 55 | 56 | ["Prefix--" 57 | 58 | (for [i (range 3)] 59 | (str " " i)) 60 | 61 | " --Suffix"] 62 | "Prefix-- 0 1 2 --Suffix" 63 | 64 | ;; Check for skipping nils and blank strings, and not emitting the reset if no font 65 | ;; changes occurred. 66 | ["Prefix--" [:bold nil ""] "--Suffix"] 67 | "Prefix----Suffix" 68 | 69 | ;; A bug caused blank strings to be omitted, this checks for the fix: 70 | [" " 71 | "|" 72 | " " 73 | "|" 74 | " "] 75 | " | | " 76 | 77 | ["Notice: the " 78 | [:yellow "shields"] 79 | " are operating at " 80 | [:green "98.7%"] 81 | "."] 82 | "Notice: the [CSI]0;33mshields[CSI]m are operating at [CSI]0;32m98.7%[CSI]m." 83 | 84 | ;; nil is allowed (this is used when formatting is optional, such as the fonts in exceptions). 85 | 86 | ["NORMAL" [nil "-STILL NORMAL"]] 87 | "NORMAL-STILL NORMAL" 88 | 89 | 90 | ;; Demonstrate some optimizations (no change between first and second 91 | ;; INV/BOLD), and also specifying multiple font modifiers. 92 | 93 | ["NORMAL" 94 | [:red "-RED"] 95 | [:bright-red "-BR/RED"]] 96 | "NORMAL[CSI]0;31m-RED[CSI]0;91m-BR/RED[CSI]m" 97 | 98 | ["NORMAL-" 99 | [:inverse "-INVERSE" [:bold "-INV/BOLD"]] 100 | [:inverse.bold "-INV/BOLD"] 101 | "-NORMAL"] 102 | "NORMAL-[CSI]0;7m-INVERSE[CSI]0;1;7m-INV/BOLD-INV/BOLD[CSI]m-NORMAL" 103 | 104 | 105 | ;; Basic tests for width: 106 | 107 | '("START |" 108 | [{:width 10 109 | :pad :right} "AAA"] 110 | "|" 111 | [{:width 10} "BBB"] 112 | "|") 113 | "START |AAA | BBB|" 114 | ; 0123456789 0123456789 115 | 116 | '("START |" 117 | [{:width 10 118 | :pad :right 119 | :font :green} "A" "A" "A"] 120 | "|" 121 | [{:width 10 122 | :font :red} "BBB"] 123 | "|") 124 | "START |[CSI]0;32mAAA [CSI]m|[CSI]0;31m BBB[CSI]m|" 125 | ; 0123456789 0123456789 126 | 127 | '("START |" 128 | [{:width 10 129 | :pad :right 130 | :font :green} "A" [nil "B"] [:blue "C"]] 131 | "|" 132 | [{:width 10 133 | :font :red} "XYZ"] 134 | "|") 135 | "START |[CSI]0;32mAB[CSI]0;34mC[CSI]0;32m [CSI]m|[CSI]0;31m XYZ[CSI]m|" 136 | ; 01 2 3456789 0123456789 137 | 138 | ;; Only pads, never truncates 139 | 140 | '("START-" [{:width 5} "ABCDEFGH"] "-END") 141 | 142 | "START-ABCDEFGH-END")) 143 | 144 | (deftest ignores-fonts-when-color-disabled 145 | (binding [ansi/*color-enabled* false] 146 | (is (= "Warning: Reactor Leak!" 147 | (compose [:red "Warning:"] " " [:bold "Reactor Leak!"]))))) 148 | 149 | (deftest unrecognized-font-modifier 150 | (when-let [e (is (thrown? Throwable (compose [:what.is.this? "Fail!"])))] 151 | (is (= "unexpected font term: :what" (ex-message e))) 152 | (is (= {:font-term :what 153 | :font-def :what.is.this? 154 | :available-terms [:black 155 | :black-bg 156 | :blue 157 | :blue-bg 158 | :bold 159 | :bright-black 160 | :bright-black-bg 161 | :bright-blue 162 | :bright-blue-bg 163 | :bright-cyan 164 | :bright-cyan-bg 165 | :bright-green 166 | :bright-green-bg 167 | :bright-magenta 168 | :bright-magenta-bg 169 | :bright-red 170 | :bright-red-bg 171 | :bright-white 172 | :bright-white-bg 173 | :bright-yellow 174 | :bright-yellow-bg 175 | :cyan 176 | :cyan-bg 177 | :faint 178 | :green 179 | :green-bg 180 | :inverse 181 | :italic 182 | :magenta 183 | :magenta-bg 184 | :normal 185 | :not-underlined 186 | :plain 187 | :red 188 | :red-bg 189 | :roman 190 | :underlined 191 | :white 192 | :white-bg 193 | :yellow 194 | :yellow-bg]} 195 | (ex-data e))))) 196 | 197 | (deftest unrecognized-vector-font-modifier 198 | (when-let [e (is (thrown? Throwable (compose [[:what :is :this?] "Fail!"])))] 199 | (is (= "unexpected font term: :what" (ex-message e))) 200 | (is (match? {:font-term :what 201 | :font-def [:what :is :this?]} 202 | (ex-data e))))) 203 | 204 | (deftest pad-both-even-padding 205 | (is (= "| XX |" 206 | ; .... .... 207 | (compose "|" [{:width 10 208 | :pad :both} 209 | "XX"] "|")))) 210 | 211 | (deftest pad-both-odd-padding 212 | (is (= "| X |" 213 | ; ..... .... 214 | (compose "|" [{:width 10 215 | :pad :both} 216 | "X"] "|")))) 217 | 218 | (deftest use-of-vector-font-defs 219 | (doseq [[font-kw font-vector] [[:red.green-bg [:red :green-bg]] 220 | [:blue.italic [:italic :blue]] 221 | [:green.underlined [:green nil :underlined]]]] 222 | (is (= (compose [font-kw "some text"]) 223 | (compose [font-vector "some text"]))) 224 | 225 | 226 | (is (= (compose [{:font font-kw} "text"]) 227 | (compose [{:font font-vector} "text"]))))) 228 | -------------------------------------------------------------------------------- /test/clj_commons/binary_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.binary-test 2 | "Tests for the clj-commons.format.binary namespace." 3 | (:require [clj-commons.ansi :as ansi] 4 | [clj-commons.format.binary :as b] 5 | [clj-commons.test-common :as tc] 6 | [clojure.string :as string] 7 | [clojure.test :refer [deftest is are use-fixtures]]) 8 | (:import (java.nio ByteBuffer))) 9 | 10 | (use-fixtures :once tc/spec-fixture) 11 | 12 | (defn- format-binary-plain 13 | [input] 14 | (binding [ansi/*color-enabled* false] 15 | (b/format-binary input))) 16 | 17 | (defn- format-binary-delta-plain 18 | [expected actual] 19 | (binding [ansi/*color-enabled* false] 20 | (string/split-lines (b/format-binary-delta expected actual)))) 21 | 22 | (defn- fixup-sgr 23 | [s] 24 | (string/replace s #"\u001b\[(.*?)m" "{$1}")) 25 | 26 | (defn- format-binary-delta 27 | [expected actual] 28 | (-> (b/format-binary-delta expected actual) 29 | fixup-sgr 30 | string/split-lines)) 31 | 32 | (defn- format-binary-string-plain 33 | [^String str] 34 | (format-binary-plain (.getBytes str))) 35 | 36 | (deftest format-byte-array-test 37 | 38 | (are [input expected] 39 | (= expected (format-binary-string-plain input)) 40 | 41 | "Hello" "0000: 48 65 6C 6C 6F\n" 42 | 43 | "This is a longer text that spans to a second line." 44 | "0000: 54 68 69 73 20 69 73 20 61 20 6C 6F 6E 67 65 72 20 74 65 78 74 20 74 68 61 74 20 73 70 61 6E 73\n0020: 20 74 6F 20 61 20 73 65 63 6F 6E 64 20 6C 69 6E 65 2E\n")) 45 | 46 | (deftest binary-fonts 47 | (let [byte-data (byte-array [0x59 0x65 073 0x20 0x4e 0x00 0x00 0x09 0x80 0xff])] 48 | (is (= ["{0;90}0000:{} {0;36}59{} {0;36}65{} {0;36}3B{} {0;32}20{} {0;36}4E{} {0;90}00{} {0;90}00{} {0;32}09{} {0;33}80{} {0;33}FF{} |{0;36}Ye;{0;32} {0;36}N{0;90}••{0;32}_{0;33}××{} |"] 49 | (-> (b/format-binary byte-data {:ascii true}) 50 | fixup-sgr 51 | string/split-lines))))) 52 | 53 | (deftest format-string-as-byte-data 54 | (are [input expected] 55 | (= expected (format-binary-plain input)) 56 | "" "" 57 | 58 | "Hello" "0000: 48 65 6C 6C 6F\n" 59 | 60 | "This is a longer text that spans to a second line." 61 | "0000: 54 68 69 73 20 69 73 20 61 20 6C 6F 6E 67 65 72 20 74 65 78 74 20 74 68 61 74 20 73 70 61 6E 73\n0020: 20 74 6F 20 61 20 73 65 63 6F 6E 64 20 6C 69 6E 65 2E\n")) 62 | 63 | (deftest nil-is-an-empty-data 64 | (is (= (format-binary-plain nil) ""))) 65 | 66 | (deftest byte-buffer 67 | (let [bb (ByteBuffer/wrap (.getBytes "Duty Now For The Future" "UTF-8"))] 68 | (is (= "0000: 44 75 74 79 20 4E 6F 77 20 46 6F 72 20 54 68 65 20 46 75 74 75 72 65\n" 69 | (format-binary-plain bb))) 70 | 71 | (is (= "0000: 44 75 74 79\n" 72 | (-> bb 73 | (.position 5) 74 | (.limit 9) 75 | format-binary-plain))) 76 | 77 | (is (= "0000: 46 6F 72\n" 78 | (-> bb 79 | (.position 9) 80 | (.limit 12) 81 | .slice 82 | format-binary-plain))))) 83 | 84 | (deftest deltas 85 | (are [expected actual expected-output] 86 | (= expected-output 87 | (format-binary-delta-plain expected actual)) 88 | 89 | "123" "123" 90 | ["0000: 31 32 33 | 31 32 33"] 91 | 92 | "abcdefghijklmnopqrstuvwyz" "abCdefghijklmnopqrs" 93 | ["0000: 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 | 61 62 43 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70" 94 | "0010: 71 72 73 74 75 76 77 79 7A | 71 72 73 -- -- -- -- -- --"] 95 | 96 | "abc" "abcdef" 97 | ["0000: 61 62 63 -- -- -- | 61 62 63 64 65 66"])) 98 | 99 | 100 | (deftest deltas-with-fonts 101 | (are [expected actual expected-output] 102 | (match? expected-output 103 | (format-binary-delta expected actual)) 104 | 105 | "123\t" "123\n" 106 | ;; {} is reset font 107 | ;; 0 is reset font (as a prefix) 108 | ;; 90 is bright black for offset 109 | ;; 36 is cyan for printable ASCII 110 | ;; 32 is green for whitespace 111 | ;; 102 is bright green backround, 112 | ;; 101 is bright red background 113 | ["{0;90}0000:{} {0;36}31{} {0;36}32{} {0;36}33{} {0;32;102}09{} | {0;36}31{} {0;36}32{} {0;36}33{} {0;32;101}0A{}"] 114 | 115 | "1234" "12" 116 | ["{0;90}0000:{} {0;36}31{} {0;36}32{} {0;36;102}33{} {0;36;102}34{} | {0;36}31{} {0;36}32{} {0;101}--{} {0;101}--{}"] 117 | 118 | ;; 2 is faint for non-printable 119 | "\u001B" "\u001Cxyz" 120 | ["{0;90}0000:{} {0;32;102;2}1B{} {0;102}--{} {0;102}--{} {0;102}--{} | {0;32;101;2}1C{} {0;36;101}78{} {0;36;101}79{} {0;36;101}7A{}"] 121 | )) 122 | 123 | 124 | -------------------------------------------------------------------------------- /test/clj_commons/exception_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.exception-test 2 | (:require [clj-commons.test-common :as tc] 3 | [clojure.test :refer [deftest is use-fixtures testing]] 4 | [clojure.string :as str] 5 | [matcher-combinators.matchers :as m] 6 | [clj-commons.ansi :refer [*color-enabled*]] 7 | [clj-commons.pretty-impl :refer [csi]] 8 | [clj-commons.format.exceptions :as f :refer [*fonts* parse-exception format-exception]])) 9 | 10 | (use-fixtures :once tc/spec-fixture) 11 | 12 | (deftest write-exceptions 13 | (testing "exception properties printing" 14 | (testing "Does not fail with ex-info's map keys not implementing clojure.lang.Named" 15 | (is (re-find #"string-key.*string-val" 16 | (format-exception (ex-info "Error" {"string-key" "string-val"}))))))) 17 | 18 | (defn countdown 19 | [n] 20 | (if (zero? n) 21 | (throw (RuntimeException. "Boom!")) 22 | (countdown (dec n)))) 23 | 24 | (deftest captures-repeat-counts 25 | (binding [*fonts* nil] 26 | (let [formatted (try (countdown 20) 27 | (catch Throwable t 28 | (format-exception t)))] 29 | (is (re-find #"(?xmd) \Qclj-commons.exception-test/countdown\E (.*) \Q(repeats 20 times)\E" 30 | formatted))))) 31 | 32 | (deftest binding-fonts-to-nil-is-same-as-no-color 33 | (let [ex (ex-info "does not matter" {:gnip :gnop}) 34 | with-fonts-but-no-color (binding [*color-enabled* false] 35 | (format-exception ex)) 36 | with-color-but-no-fonts (binding [*fonts* nil] 37 | (format-exception ex))] 38 | (is (= with-fonts-but-no-color 39 | with-color-but-no-fonts)) 40 | 41 | (is (not (str/includes? with-fonts-but-no-color csi))))) 42 | 43 | (defn parse [& text-lines] 44 | (let [text (str/join \newline text-lines)] 45 | (parse-exception text nil))) 46 | 47 | (deftest parse-exceptions 48 | (is (= [{:class-name "java.lang.IllegalArgumentException" 49 | :message "No value supplied for key: {:host \"example.com\"}" 50 | :stack-trace [{:class "clojure.lang.PersistentHashMap" 51 | :file "PersistentHashMap.java" 52 | :id "clojure.lang.PersistentHashMap.create:77" 53 | :is-clojure? false 54 | :line 77 55 | :method "create" 56 | :name "" 57 | :names [] 58 | :omitted true 59 | :package "clojure.lang" 60 | :simple-class "PersistentHashMap"} 61 | {:class "riemann.client$tcp_client" 62 | :file "client.clj" 63 | :id "riemann.client/tcp-client:90" 64 | :is-clojure? true 65 | :line 90 66 | :method "doInvoke" 67 | :name "riemann.client/tcp-client" 68 | :names ["riemann.client" 69 | "tcp-client"] 70 | :package "riemann" 71 | :simple-class "client$tcp_client"} 72 | {:class "clojure.lang.RestFn" 73 | :file "RestFn.java" 74 | :id "clojure.lang.RestFn.invoke:408" 75 | :is-clojure? false 76 | :line 408 77 | :method "invoke" 78 | :name "" 79 | :names [] 80 | :omitted true 81 | :package "clojure.lang" 82 | :simple-class "RestFn"} 83 | {:class "com.example.error_monitor$make_connection" 84 | :file "error_monitor.clj" 85 | :id "com.example.error-monitor/make-connection:22" 86 | :is-clojure? true 87 | :line 22 88 | :method "invoke" 89 | :name "com.example.error-monitor/make-connection" 90 | :names ["com.example.error-monitor" 91 | "make-connection"] 92 | :package "com.example" 93 | :simple-class "error_monitor$make_connection"} 94 | {:class "com.example.error_monitor$make_client" 95 | :file "error_monitor.clj" 96 | :id "com.example.error-monitor/make-client:26" 97 | :is-clojure? true 98 | :line 26 99 | :method "invoke" 100 | :name "com.example.error-monitor/make-client" 101 | :names ["com.example.error-monitor" 102 | "make-client"] 103 | :package "com.example" 104 | :simple-class "error_monitor$make_client"} 105 | {:class "clojure.core$map$fn__4553" 106 | :file "core.clj" 107 | :id "clojure.core/map/fn:2624" 108 | :is-clojure? true 109 | :line 2624 110 | :method "invoke" 111 | :name "clojure.core/map/fn" 112 | :names ["clojure.core" 113 | "map" 114 | "fn"] 115 | :package "clojure" 116 | :simple-class "core$map$fn__4553"} 117 | {:class "clojure.lang.LazySeq" 118 | :file "LazySeq.java" 119 | :id "clojure.lang.LazySeq.sval:40" 120 | :is-clojure? false 121 | :line 40 122 | :method "sval" 123 | :name "" 124 | :names [] 125 | :omitted true 126 | :package "clojure.lang" 127 | :simple-class "LazySeq"} 128 | {:class "clojure.core$seq__4128" 129 | :file "core.clj" 130 | :id "clojure.core/seq:137" 131 | :is-clojure? true 132 | :line 137 133 | :method "invoke" 134 | :name "clojure.core/seq" 135 | :names ["clojure.core" 136 | "seq"] 137 | :package "clojure" 138 | :simple-class "core$seq__4128"} 139 | {:class "clojure.core$sort" 140 | :file "core.clj" 141 | :id "clojure.core/sort:2981" 142 | :is-clojure? true 143 | :line 2981 144 | :method "invoke" 145 | :name "clojure.core/sort" 146 | :names ["clojure.core" 147 | "sort"] 148 | :package "clojure" 149 | :simple-class "core$sort"} 150 | {:class "clojure.core$sort_by" 151 | :file "core.clj" 152 | :id "clojure.core/sort-by:2998" 153 | :is-clojure? true 154 | :line 2998 155 | :method "invoke" 156 | :name "clojure.core/sort-by" 157 | :names ["clojure.core" 158 | "sort-by"] 159 | :package "clojure" 160 | :simple-class "core$sort_by"} 161 | {:class "clojure.core$sort_by" 162 | :file "core.clj" 163 | :id "clojure.core/sort-by:2996" 164 | :is-clojure? true 165 | :line 2996 166 | :method "invoke" 167 | :name "clojure.core/sort-by" 168 | :names ["clojure.core" 169 | "sort-by"] 170 | :package "clojure" 171 | :simple-class "core$sort_by"} 172 | {:class "com.example.error_monitor$make_clients" 173 | :file "error_monitor.clj" 174 | :id "com.example.error-monitor/make-clients:31" 175 | :is-clojure? true 176 | :line 31 177 | :method "invoke" 178 | :name "com.example.error-monitor/make-clients" 179 | :names ["com.example.error-monitor" 180 | "make-clients"] 181 | :package "com.example" 182 | :simple-class "error_monitor$make_clients"} 183 | {:class "com.example.error_monitor$report_and_reset" 184 | :file "error_monitor.clj" 185 | :id "com.example.error-monitor/report-and-reset:185" 186 | :is-clojure? true 187 | :line 185 188 | :method "invoke" 189 | :name "com.example.error-monitor/report-and-reset" 190 | :names ["com.example.error-monitor" 191 | "report-and-reset"] 192 | :package "com.example" 193 | :simple-class "error_monitor$report_and_reset"} 194 | {:class "com.example.error_monitor.main$_main$fn__705" 195 | :file "main.clj" 196 | :id "com.example.error-monitor.main/-main/fn:19" 197 | :is-clojure? true 198 | :line 19 199 | :method "invoke" 200 | :name "com.example.error-monitor.main/-main/fn" 201 | :names ["com.example.error-monitor.main" 202 | "-main" 203 | "fn"] 204 | :package "com.example.error_monitor" 205 | :simple-class "main$_main$fn__705"} 206 | {:class "com.example.error_monitor.main$_main" 207 | :file "main.clj" 208 | :id "com.example.error-monitor.main/-main:16" 209 | :is-clojure? true 210 | :line 16 211 | :method "doInvoke" 212 | :name "com.example.error-monitor.main/-main" 213 | :names ["com.example.error-monitor.main" 214 | "-main"] 215 | :package "com.example.error_monitor" 216 | :simple-class "main$_main"} 217 | {:class "clojure.lang.RestFn" 218 | :file "RestFn.java" 219 | :id "clojure.lang.RestFn.applyTo:137" 220 | :is-clojure? false 221 | :line 137 222 | :method "applyTo" 223 | :name "" 224 | :names [] 225 | :omitted true 226 | :package "clojure.lang" 227 | :simple-class "RestFn"} 228 | {:class "com.example.error_monitor.main" 229 | :file "" 230 | :id "com.example.error_monitor.main.main:-1" 231 | :is-clojure? false 232 | :line nil 233 | :method "main" 234 | :name "" 235 | :names [] 236 | :package "com.example.error_monitor" 237 | :simple-class "main"}]}] 238 | (parse "java.lang.IllegalArgumentException: No value supplied for key: {:host \"example.com\"}" 239 | "\tat clojure.lang.PersistentHashMap.create(PersistentHashMap.java:77)" 240 | "\tat riemann.client$tcp_client.doInvoke(client.clj:90)" 241 | "\tat clojure.lang.RestFn.invoke(RestFn.java:408)" 242 | "\tat com.example.error_monitor$make_connection.invoke(error_monitor.clj:22)" 243 | "\tat com.example.error_monitor$make_client.invoke(error_monitor.clj:26)" 244 | "\tat clojure.core$map$fn__4553.invoke(core.clj:2624)" 245 | "\tat clojure.lang.LazySeq.sval(LazySeq.java:40)" 246 | "\tat clojure.lang.LazySeq.seq(LazySeq.java:49)" 247 | "\tat clojure.lang.RT.seq(RT.java:507)" 248 | "\tat clojure.core$seq__4128.invoke(core.clj:137)" 249 | "\tat clojure.core$sort.invoke(core.clj:2981)" 250 | "\tat clojure.core$sort_by.invoke(core.clj:2998)" 251 | "\tat clojure.core$sort_by.invoke(core.clj:2996)" 252 | "\tat com.example.error_monitor$make_clients.invoke(error_monitor.clj:31)" 253 | "\tat com.example.error_monitor$report_and_reset.invoke(error_monitor.clj:185)" 254 | "\tat com.example.error_monitor.main$_main$fn__705.invoke(main.clj:19)" 255 | "\tat com.example.error_monitor.main$_main.doInvoke(main.clj:16)" 256 | "\tat clojure.lang.RestFn.applyTo(RestFn.java:137)" 257 | "\tat com.example.error_monitor.main.main(Unknown Source)"))) 258 | 259 | (is (= [{:class-name "java.lang.RuntimeException" 260 | :message "Request handling exception"} 261 | {:class-name "java.lang.RuntimeException" 262 | :message "Failure updating row"} 263 | {:class-name "java.sql.SQLException" 264 | :message "Database failure 265 | SELECT FOO, BAR, BAZ 266 | FROM GNIP 267 | failed with ABC123" 268 | :stack-trace [{:class "user$jdbc_update" 269 | :file "user.clj" 270 | :id "user/jdbc-update:7" 271 | :is-clojure? true 272 | :line 7 273 | :method "invoke" 274 | :name "user/jdbc-update" 275 | :names ["user" 276 | "jdbc-update"] 277 | :package nil 278 | :simple-class "user$jdbc_update"} 279 | {:class "user$make_jdbc_update_worker$reify__497" 280 | :file "user.clj" 281 | :id "user/make-jdbc-update-worker/reify/do-work:18" 282 | :is-clojure? true 283 | :line 18 284 | :method "do_work" 285 | :name "user/make-jdbc-update-worker/reify/do-work" 286 | :names ["user" 287 | "make-jdbc-update-worker" 288 | "reify" 289 | "do-work"] 290 | :package nil 291 | :simple-class "user$make_jdbc_update_worker$reify__497"} 292 | {:class "user$update_row" 293 | :file "user.clj" 294 | :id "user/update-row:23" 295 | :is-clojure? true 296 | :line 23 297 | :method "invoke" 298 | :name "user/update-row" 299 | :names ["user" 300 | "update-row"] 301 | :package nil 302 | :simple-class "user$update_row"} 303 | {:class "user$make_exception" 304 | :file "user.clj" 305 | :id "user/make-exception:31" 306 | :is-clojure? true 307 | :line 31 308 | :method "invoke" 309 | :name "user/make-exception" 310 | :names ["user" 311 | "make-exception"] 312 | :package nil 313 | :simple-class "user$make_exception"} 314 | {:class "user$eval2018" 315 | :file "REPL Input" 316 | :id "user/eval2018" 317 | :is-clojure? true 318 | :line nil 319 | :method "invoke" 320 | :name "user/eval2018" 321 | :names ["user" 322 | "eval2018"] 323 | :package nil 324 | :simple-class "user$eval2018"} 325 | {:class "clojure.lang.Compiler" 326 | :file "Compiler.java" 327 | :id "clojure.lang.Compiler.eval:6619" 328 | :is-clojure? false 329 | :line 6619 330 | :method "eval" 331 | :name "" 332 | :names [] 333 | :omitted true 334 | :package "clojure.lang" 335 | :simple-class "Compiler"} 336 | {:class "clojure.core$eval" 337 | :file "core.clj" 338 | :id "clojure.core/eval:2852" 339 | :is-clojure? true 340 | :line 2852 341 | :method "invoke" 342 | :name "clojure.core/eval" 343 | :names ["clojure.core" 344 | "eval"] 345 | :package "clojure" 346 | :simple-class "core$eval"}]}] 347 | (parse "java.lang.RuntimeException: Request handling exception" 348 | "\tat user$make_exception.invoke(user.clj:31)" 349 | "\tat user$eval2018.invoke(form-init1482095333541107022.clj:1)" 350 | "\tat clojure.lang.Compiler.eval(Compiler.java:6619)" 351 | "\tat clojure.lang.Compiler.eval(Compiler.java:6582)" 352 | "\tat clojure.core$eval.invoke(core.clj:2852)" 353 | "\tat clojure.main$repl$read_eval_print__6602$fn__6605.invoke(main.clj:259)" 354 | "\tat clojure.main$repl$read_eval_print__6602.invoke(main.clj:259)" 355 | "\tat clojure.main$repl$fn__6611$fn__6612.invoke(main.clj:277)" 356 | "\tat clojure.main$repl$fn__6611.invoke(main.clj:277)" 357 | "\tat clojure.main$repl.doInvoke(main.clj:275)" 358 | "\tat clojure.lang.RestFn.invoke(RestFn.java:1523)" 359 | "\tat clojure.tools.nrepl.middleware.interruptible_eval$evaluate$fn__1419.invoke(interruptible_eval.clj:72)" 360 | "\tat clojure.lang.AFn.applyToHelper(AFn.java:159)" 361 | "\tat clojure.lang.AFn.applyTo(AFn.java:151)" 362 | "\tat clojure.core$apply.invoke(core.clj:617)" 363 | "\tat clojure.core$with_bindings_STAR_.doInvoke(core.clj:1788)" 364 | "\tat clojure.lang.RestFn.invoke(RestFn.java:425)" 365 | "\tat clojure.tools.nrepl.middleware.interruptible_eval$evaluate.invoke(interruptible_eval.clj:56)" 366 | "\tat clojure.tools.nrepl.middleware.interruptible_eval$interruptible_eval$fn__1461$fn__1464.invoke(interruptible_eval.clj:191)" 367 | "\tat clojure.tools.nrepl.middleware.interruptible_eval$run_next$fn__1456.invoke(interruptible_eval.clj:159)" 368 | "\tat clojure.lang.AFn.run(AFn.java:24)" 369 | "\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)" 370 | "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)" 371 | "\tat java.lang.Thread.run(Thread.java:745)" 372 | "Caused by: java.lang.RuntimeException: Failure updating row" 373 | "\tat user$update_row.invoke(user.clj:23)" 374 | "\t... 24 more" 375 | "Caused by: java.sql.SQLException: Database failure" 376 | "SELECT FOO, BAR, BAZ" 377 | "FROM GNIP" 378 | "failed with ABC123" 379 | "\tat user$jdbc_update.invoke(user.clj:7)" 380 | "\tat user$make_jdbc_update_worker$reify__497.do_work(user.clj:18)" 381 | "\t... 25 more"))) 382 | 383 | (is (= [{:class-name "com.datastax.driver.core.TransportException" 384 | :message "/17.76.3.14:9042 Cannot connect"} 385 | {:class-name "java.net.ConnectException" 386 | :message "Connection refused: /17.76.3.14:9042" 387 | :stack-trace [{:class "sun.nio.ch.SocketChannelImpl" 388 | :file "" 389 | :id "sun.nio.ch.SocketChannelImpl.checkConnect:-1" 390 | :is-clojure? false 391 | :line nil 392 | :method "checkConnect" 393 | :name "" 394 | :names [] 395 | :package "sun.nio.ch" 396 | :simple-class "SocketChannelImpl"} 397 | {:class "sun.nio.ch.SocketChannelImpl" 398 | :file "SocketChannelImpl.java" 399 | :id "sun.nio.ch.SocketChannelImpl.finishConnect:717" 400 | :is-clojure? false 401 | :line 717 402 | :method "finishConnect" 403 | :name "" 404 | :names [] 405 | :package "sun.nio.ch" 406 | :simple-class "SocketChannelImpl"} 407 | {:class "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss" 408 | :file "NioClientBoss.java" 409 | :id "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.connect:150" 410 | :is-clojure? false 411 | :line 150 412 | :method "connect" 413 | :name "" 414 | :names [] 415 | :package "com.datastax.shaded.netty.channel.socket.nio" 416 | :simple-class "NioClientBoss"} 417 | {:class "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss" 418 | :file "NioClientBoss.java" 419 | :id "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.processSelectedKeys:105" 420 | :is-clojure? false 421 | :line 105 422 | :method "processSelectedKeys" 423 | :name "" 424 | :names [] 425 | :package "com.datastax.shaded.netty.channel.socket.nio" 426 | :simple-class "NioClientBoss"} 427 | {:class "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss" 428 | :file "NioClientBoss.java" 429 | :id "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.process:79" 430 | :is-clojure? false 431 | :line 79 432 | :method "process" 433 | :name "" 434 | :names [] 435 | :package "com.datastax.shaded.netty.channel.socket.nio" 436 | :simple-class "NioClientBoss"} 437 | {:class "com.datastax.shaded.netty.channel.socket.nio.AbstractNioSelector" 438 | :file "AbstractNioSelector.java" 439 | :id "com.datastax.shaded.netty.channel.socket.nio.AbstractNioSelector.run:318" 440 | :is-clojure? false 441 | :line 318 442 | :method "run" 443 | :name "" 444 | :names [] 445 | :package "com.datastax.shaded.netty.channel.socket.nio" 446 | :simple-class "AbstractNioSelector"} 447 | {:class "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss" 448 | :file "NioClientBoss.java" 449 | :id "com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.run:42" 450 | :is-clojure? false 451 | :line 42 452 | :method "run" 453 | :name "" 454 | :names [] 455 | :package "com.datastax.shaded.netty.channel.socket.nio" 456 | :simple-class "NioClientBoss"} 457 | {:class "com.datastax.shaded.netty.util.ThreadRenamingRunnable" 458 | :file "ThreadRenamingRunnable.java" 459 | :id "com.datastax.shaded.netty.util.ThreadRenamingRunnable.run:108" 460 | :is-clojure? false 461 | :line 108 462 | :method "run" 463 | :name "" 464 | :names [] 465 | :package "com.datastax.shaded.netty.util" 466 | :simple-class "ThreadRenamingRunnable"} 467 | {:class "com.datastax.shaded.netty.util.internal.DeadLockProofWorker$1" 468 | :file "DeadLockProofWorker.java" 469 | :id "com.datastax.shaded.netty.util.internal.DeadLockProofWorker$1.run:42" 470 | :is-clojure? false 471 | :line 42 472 | :method "run" 473 | :name "" 474 | :names [] 475 | :package "com.datastax.shaded.netty.util.internal" 476 | :simple-class "DeadLockProofWorker$1"} 477 | {:class "com.datastax.driver.core.Connection" 478 | :file "Connection.java" 479 | :id "com.datastax.driver.core.Connection.:104" 480 | :is-clojure? false 481 | :line 104 482 | :method "" 483 | :name "" 484 | :names [] 485 | :package "com.datastax.driver.core" 486 | :simple-class "Connection"} 487 | {:class "com.datastax.driver.core.PooledConnection" 488 | :file "PooledConnection.java" 489 | :id "com.datastax.driver.core.PooledConnection.:32" 490 | :is-clojure? false 491 | :line 32 492 | :method "" 493 | :name "" 494 | :names [] 495 | :package "com.datastax.driver.core" 496 | :simple-class "PooledConnection"} 497 | {:class "com.datastax.driver.core.Connection$Factory" 498 | :file "Connection.java" 499 | :id "com.datastax.driver.core.Connection$Factory.open:557" 500 | :is-clojure? false 501 | :line 557 502 | :method "open" 503 | :name "" 504 | :names [] 505 | :package "com.datastax.driver.core" 506 | :simple-class "Connection$Factory"} 507 | {:class "com.datastax.driver.core.DynamicConnectionPool" 508 | :file "DynamicConnectionPool.java" 509 | :id "com.datastax.driver.core.DynamicConnectionPool.:74" 510 | :is-clojure? false 511 | :line 74 512 | :method "" 513 | :name "" 514 | :names [] 515 | :package "com.datastax.driver.core" 516 | :simple-class "DynamicConnectionPool"} 517 | {:class "com.datastax.driver.core.HostConnectionPool" 518 | :file "HostConnectionPool.java" 519 | :id "com.datastax.driver.core.HostConnectionPool.newInstance:33" 520 | :is-clojure? false 521 | :line 33 522 | :method "newInstance" 523 | :name "" 524 | :names [] 525 | :package "com.datastax.driver.core" 526 | :simple-class "HostConnectionPool"} 527 | {:class "com.datastax.driver.core.SessionManager$2" 528 | :file "SessionManager.java" 529 | :id "com.datastax.driver.core.SessionManager$2.call:231" 530 | :is-clojure? false 531 | :line 231 532 | :method "call" 533 | :name "" 534 | :names [] 535 | :package "com.datastax.driver.core" 536 | :simple-class "SessionManager$2"} 537 | {:class "com.datastax.driver.core.SessionManager$2" 538 | :file "SessionManager.java" 539 | :id "com.datastax.driver.core.SessionManager$2.call:224" 540 | :is-clojure? false 541 | :line 224 542 | :method "call" 543 | :name "" 544 | :names [] 545 | :package "com.datastax.driver.core" 546 | :simple-class "SessionManager$2"} 547 | {:class "java.util.concurrent.FutureTask" 548 | :file "FutureTask.java" 549 | :id "java.util.concurrent.FutureTask.run:266" 550 | :is-clojure? false 551 | :line 266 552 | :method "run" 553 | :name "" 554 | :names [] 555 | :package "java.util.concurrent" 556 | :simple-class "FutureTask"} 557 | {:class "java.util.concurrent.ThreadPoolExecutor" 558 | :file "ThreadPoolExecutor.java" 559 | :id "java.util.concurrent.ThreadPoolExecutor.runWorker:1142" 560 | :is-clojure? false 561 | :line 1142 562 | :method "runWorker" 563 | :name "" 564 | :names [] 565 | :package "java.util.concurrent" 566 | :simple-class "ThreadPoolExecutor"} 567 | {:class "java.util.concurrent.ThreadPoolExecutor$Worker" 568 | :file "ThreadPoolExecutor.java" 569 | :id "java.util.concurrent.ThreadPoolExecutor$Worker.run:617" 570 | :is-clojure? false 571 | :line 617 572 | :method "run" 573 | :name "" 574 | :names [] 575 | :package "java.util.concurrent" 576 | :simple-class "ThreadPoolExecutor$Worker"} 577 | {:class "java.lang.Thread" 578 | :file "Thread.java" 579 | :id "java.lang.Thread.run:745" 580 | :is-clojure? false 581 | :line 745 582 | :method "run" 583 | :name "" 584 | :names [] 585 | :package "java.lang" 586 | :simple-class "Thread"}]}] 587 | (parse "com.datastax.driver.core.TransportException: /17.76.3.14:9042 Cannot connect" 588 | "\tat com.datastax.driver.core.Connection.(Connection.java:104) ~store-service.jar:na" 589 | "\tat com.datastax.driver.core.PooledConnection.(PooledConnection.java:32) ~store-service.jar:na" 590 | "\tat com.datastax.driver.core.Connection$Factory.open(Connection.java:557) ~store-service.jar:na" 591 | "\tat com.datastax.driver.core.DynamicConnectionPool.(DynamicConnectionPool.java:74) ~store-service.jar:na" 592 | "\tat com.datastax.driver.core.HostConnectionPool.newInstance(HostConnectionPool.java:33) ~store-service.jar:na" 593 | "\tat com.datastax.driver.core.SessionManager$2.call(SessionManager.java:231) store-service.jar:na" 594 | "\tat com.datastax.driver.core.SessionManager$2.call(SessionManager.java:224) store-service.jar:na" 595 | "\tat java.util.concurrent.FutureTask.run(FutureTask.java:266) na:1.8.0_66" 596 | "\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) na:1.8.0_66" 597 | "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) na:1.8.0_66" 598 | "\tat java.lang.Thread.run(Thread.java:745) na:1.8.0_66" 599 | "Caused by: java.net.ConnectException: Connection refused: /17.76.3.14:9042" 600 | "\tat sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) ~na:1.8.0_66" 601 | "\tat sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717) ~na:1.8.0_66" 602 | "\tat com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.connect(NioClientBoss.java:150) ~store-service.jar:na" 603 | "\tat com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.processSelectedKeys(NioClientBoss.java:105) ~store-service.jar:na" 604 | "\tat com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.process(NioClientBoss.java:79) ~store-service.jar:na" 605 | "\tat com.datastax.shaded.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:318) ~store-service.jar:na" 606 | "\tat com.datastax.shaded.netty.channel.socket.nio.NioClientBoss.run(NioClientBoss.java:42) ~store-service.jar:na" 607 | "\tat com.datastax.shaded.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108) ~store-service.jar:na" 608 | "\tat com.datastax.shaded.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42) ~store-service.jar:na" 609 | "\t... 3 common frames omitted")))) 610 | 611 | 612 | (deftest write-exceptions-with-nil-data 613 | (testing "Does not fail with a nil ex-info map key" 614 | (is (re-find #"nil.*nil" 615 | (format-exception (ex-info "Error" {nil nil})))))) 616 | 617 | (deftest format-stack-trace-element 618 | (let [frame-names (->> (Thread/currentThread) 619 | .getStackTrace 620 | seq 621 | (mapv f/format-stack-trace-element))] 622 | (is (match? 623 | ;; A few sample Java and Clojure frame names 624 | (m/embeds #{"java.lang.Thread.getStackTrace" 625 | "clojure.core/apply" 626 | "clojure.test/run-tests"}) 627 | (set frame-names))))) 628 | -------------------------------------------------------------------------------- /test/clj_commons/pretty/annotations_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.pretty.annotations-test 2 | (:require [clojure.test :refer [deftest is use-fixtures]] 3 | [clj-commons.ansi :as ansi] 4 | [clj-commons.test-common :as tc] 5 | [clj-commons.pretty.annotations :refer [callouts default-style annotate-lines]] 6 | [matcher-combinators.matchers :as m])) 7 | 8 | (use-fixtures :once tc/spec-fixture) 9 | 10 | (defn- compose-each 11 | [coll] 12 | (mapv ansi/compose coll)) 13 | 14 | (defn compose-all 15 | [& strings] 16 | (compose-each strings)) 17 | 18 | (deftest with-default-style 19 | ;; Ultimately, comparing the strings with ANSI characters (the result of compose). 20 | (is (match? (m/via compose-each 21 | (compose-all 22 | [:yellow " ▲ ▲"] 23 | [:yellow " │ │"] 24 | [:yellow " │ └╴ Second"] 25 | [:yellow " │"] 26 | [:yellow " └╴ First"])) 27 | (callouts [{:offset 3 28 | :message "First"} 29 | {:offset 6 30 | :message "Second"}])))) 31 | 32 | (deftest sorts-by-offset 33 | (is (match? (m/via compose-each 34 | (compose-all 35 | [:yellow " ▲ ▲"] 36 | [:yellow " │ │"] 37 | [:yellow " │ └╴ First"] 38 | [:yellow " │"] 39 | [:yellow " └╴ Second"])) 40 | (callouts [{:offset 6 41 | :message "First"} 42 | {:offset 3 43 | :message "Second"}])))) 44 | 45 | (deftest with-annotation-length 46 | (is (match? (m/via compose-each 47 | (compose-all 48 | [:yellow " ▲▲ ▲▲▲▲"] 49 | [:yellow " │ │"] 50 | [:yellow " │ └╴ First"] 51 | [:yellow " │"] 52 | [:yellow " └╴ Second"])) 53 | (callouts [{:offset 6 54 | :length 4 55 | :message "First"} 56 | {:offset 3 57 | :length 2 58 | :message "Second"}])))) 59 | 60 | (deftest with-annotation-font 61 | (is (match? (m/via compose-each 62 | (compose-all 63 | [:yellow " ▲▲ " [:red "▲▲▲▲"]] 64 | [:yellow " │ " [:red "│"]] 65 | [:yellow " │ " [:red "└╴ First"]] 66 | [:yellow " │"] 67 | [:yellow " └╴ Second"])) 68 | (callouts [{:offset 6 69 | :length 4 70 | :font :red 71 | :message "First"} 72 | {:offset 3 73 | :length 2 74 | :message "Second"}])))) 75 | 76 | (deftest spacing-minimal 77 | (is (match? (m/via compose-each 78 | (compose-all 79 | [:yellow " ▲ ▲"] 80 | [:yellow " │ └╴ Second"] 81 | [:yellow " └╴ First"])) 82 | (callouts (assoc default-style :spacing :minimal) 83 | [{:offset 3 84 | :message "First"} 85 | {:offset 6 86 | :message "Second"}])))) 87 | 88 | (deftest spacing-compact 89 | (is (match? (m/via compose-each 90 | (compose-all 91 | [:yellow " ▲ ▲"] 92 | [:yellow " │ │"] 93 | [:yellow " │ └╴ Second"] 94 | [:yellow " └╴ First"])) 95 | (callouts (assoc default-style :spacing :compact) 96 | [{:offset 3 97 | :message "First"} 98 | {:offset 6 99 | :message "Second"}])))) 100 | 101 | (deftest custom-style 102 | (is (match? (m/via compose-each 103 | (compose-all 104 | [:blue " ~ ~~"] 105 | [:blue " ! !"] 106 | [:blue " ! +> Second"] 107 | [:blue " +> First"])) 108 | (callouts {:font :blue 109 | :marker "~" 110 | :bar "!" 111 | :nib "+> " 112 | :spacing :compact} 113 | [{:offset 3 114 | :message "First"} 115 | {:offset 6 116 | :length 2 117 | :message "Second"}])))) 118 | 119 | (deftest annotate-lines-defaults-to-line-one 120 | (is (match? (m/via compose-each 121 | (compose-all 122 | "1: barney" 123 | "2: fred")) 124 | (annotate-lines [{:line "barney"} 125 | {:line "fred"}])))) 126 | 127 | (deftest sets-line-number-column-width-from-max 128 | (is (match? (m/via compose-each 129 | (compose-all 130 | " 99: barney" 131 | "100: fred" 132 | "101: wilma")) 133 | (annotate-lines {:start-line 99} 134 | [{:line "barney"} 135 | {:line "fred"} 136 | {:line "wilma"}])))) 137 | 138 | (deftest intersperses-with-indented-annotation-lines 139 | (is (match? (m/via compose-each 140 | (compose-all 141 | [nil " 99: barney"] 142 | [nil " " [:yellow " ▲"]] 143 | [nil " " [:yellow " │"]] 144 | [nil " " [:yellow " └╴ r not allowed"]] 145 | "100: fred" 146 | [nil " " [:yellow " ▲"]] 147 | [nil " " [:yellow " │"]] 148 | [nil " " [:yellow " └╴ d not allowed"]] 149 | "101: wilma")) 150 | (annotate-lines {:start-line 99} 151 | [{:line "barney" 152 | :annotations [{:offset 2 :message "r not allowed"}]} 153 | {:line "fred" 154 | :annotations [{:offset 3 :message "d not allowed"}]} 155 | {:line "wilma"}])))) 156 | 157 | (deftest can-override-style 158 | (is (match? (m/via compose-each 159 | (compose-all 160 | [nil " 99: barney"] 161 | [nil " " [:blue " ▲"]] 162 | [nil " " [:blue " │"]] 163 | [nil " " [:blue " └╴ r not allowed"]] 164 | "100: fred")) 165 | (annotate-lines {:start-line 99 166 | :style (assoc default-style :font :blue)} 167 | [{:line "barney" 168 | :annotations [{:offset 2 :message "r not allowed"}]} 169 | {:line "fred"}])))) 170 | 171 | (deftest can-override-line-number-width 172 | (is (match? (m/via compose-each 173 | (compose-all 174 | [nil " 99: barney"] 175 | [nil " " [:blue " ▲"]] 176 | [nil " " [:blue " │"]] 177 | [nil " " [:blue " └╴ r not allowed"]] 178 | " 100: fred")) 179 | (annotate-lines {:start-line 99 180 | :line-number-width 5 181 | :style (assoc default-style :font :blue)} 182 | [{:line "barney" 183 | :annotations [{:offset 2 :message "r not allowed"}]} 184 | {:line "fred"}])))) 185 | 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /test/clj_commons/test_common.clj: -------------------------------------------------------------------------------- 1 | (ns clj-commons.test-common 2 | (:require clj-commons.pretty.spec 3 | [clojure.spec.test.alpha :as stest])) 4 | 5 | (defn spec-fixture 6 | [f] 7 | (try 8 | (stest/instrument) 9 | (f) 10 | (finally 11 | (stest/unstrument)))) 12 | -------------------------------------------------------------------------------- /test/demo.clj: -------------------------------------------------------------------------------- 1 | (ns demo 2 | (:require 3 | [clj-commons.pretty.repl :as repl] 4 | [clj-commons.format.exceptions :as e] 5 | [clj-commons.ansi :refer [compose pcompose]] 6 | [clj-commons.format.binary :as b] 7 | [clojure.java.io :as io] 8 | [clojure.repl :refer [pst]] 9 | [criterium.core :as c] 10 | playground 11 | [clojure.test :refer [report deftest is]]) 12 | (:import 13 | (java.nio.file Files) 14 | (java.sql SQLException))) 15 | 16 | (defn- jdbc-update 17 | [] 18 | (throw (SQLException. "Database failure\nSELECT FOO, BAR, BAZ\nFROM GNIP\nfailed with ABC123" "ABC" 123))) 19 | 20 | (defprotocol Worker 21 | (do-work [this])) 22 | 23 | (defn make-jdbc-update-worker 24 | [] 25 | (reify Worker 26 | (do-work [_this] (jdbc-update)))) 27 | 28 | (defn- update-row 29 | [] 30 | (try 31 | (-> (make-jdbc-update-worker) do-work) 32 | (catch Throwable e 33 | (throw (RuntimeException. "Failure updating row" e))))) 34 | 35 | (defn make-exception 36 | "Creates a sample exception used to test the exception formatting logic." 37 | [] 38 | (try 39 | (update-row) 40 | (catch Throwable e 41 | ;; Return it, not rethrow it. 42 | (RuntimeException. "Request handling exception" e)))) 43 | 44 | (defn make-ex-info 45 | "" 46 | [] 47 | (try 48 | (throw (make-exception)) 49 | (catch Throwable t 50 | ;; Return it, not rethrow it. 51 | (ex-info "Exception in make-ex-info." 52 | {:function 'make-exception} 53 | t)))) 54 | 55 | (defn infinite-loop 56 | [] 57 | (infinite-loop)) 58 | 59 | (defn countdown 60 | [n] 61 | (if (zero? n) 62 | (throw (RuntimeException. "Boom!")) 63 | (countdown (dec n)))) 64 | 65 | (defn test-failure 66 | [] 67 | (report {:type :error :expected nil :actual (make-ex-info)})) 68 | 69 | (defn -main [& args] 70 | (prn `-main :args args) 71 | (println "Clojure version: " *clojure-version*) 72 | (println "Installing pretty exceptions ...") 73 | (repl/install-pretty-exceptions) 74 | (pcompose [:bold.green "ok"]) 75 | (pst (make-exception)) 76 | (println "\nTesting reporting of repeats:") 77 | (try (countdown 20) 78 | (catch Throwable t (e/print-exception t))) 79 | (println "\nBinary output:\n") 80 | (-> (io/file "test/tiny-clojure.gif") 81 | .toPath 82 | Files/readAllBytes 83 | (b/print-binary {:ascii true})) 84 | 85 | (println "\nBinary delta:\n") 86 | (b/print-binary-delta "Welcome, Friend" 87 | "We1come, Fiend") 88 | (println)) 89 | 90 | (deftest fail-wrong-exception 91 | (is (thrown? IllegalArgumentException 92 | (jdbc-update)))) 93 | 94 | (deftest error-thrown-exception 95 | (jdbc-update)) 96 | 97 | (deftest fail-wrong-message 98 | (is (thrown-with-msg? SQLException #"validation failure" 99 | (jdbc-update)))) 100 | 101 | (comment 102 | 103 | (require '[clojure.core.async :refer [chan x :stack-trace doall)))) 165 | 166 | (playground/caller) 167 | ) 168 | -------------------------------------------------------------------------------- /test/demo_app_frames.clj: -------------------------------------------------------------------------------- 1 | (ns demo-app-frames 2 | "This namespace demonstrates how customizing clj-commons.format.exceptions/*app-frames* 3 | helps highlight application logic in stacktraces 4 | 5 | The `comment` block at end of file demonstrates this feature. 6 | " 7 | (:require [clj-commons.pretty.repl :as repl])) 8 | 9 | (repl/install-pretty-exceptions) 10 | 11 | ;; -- provided.* namespaces are libraries we're consuming --------------------- 12 | (ns provided.db 13 | (:import (java.sql SQLException))) 14 | 15 | (defn jdbc-update 16 | [] 17 | (throw (SQLException. "Database failure" "ABC" 123))) 18 | 19 | (ns provided.worker) 20 | 21 | (defprotocol Worker 22 | (do-work [this])) 23 | 24 | (ns provided.db-worker) 25 | 26 | (defn make-jdbc-update-worker 27 | [] 28 | (reify 29 | provided.worker/Worker 30 | (provided.worker/do-work [this] (provided.db/jdbc-update)))) 31 | 32 | ;; -- my-app are namespaces belonging to our application ---------------------- 33 | (ns my-app.db) 34 | 35 | (defn update-row 36 | [] 37 | (try 38 | (-> (provided.db-worker/make-jdbc-update-worker) provided.worker/do-work) 39 | (catch Throwable e 40 | (throw (RuntimeException. "Failure updating row" e))))) 41 | 42 | 43 | (ns my-app.handler) 44 | 45 | (defn make-exception 46 | "Creates a sample exception used to test the exception formatting logic." 47 | [] 48 | (try 49 | (my-app.db/update-row) 50 | (catch Throwable e 51 | ;; Return it, not rethrow it. 52 | (RuntimeException. "Request handling exception" e)))) 53 | 54 | (defn make-ex-info 55 | "" 56 | [] 57 | (try 58 | (throw (make-exception)) 59 | (catch Throwable t 60 | ;; Return it, not rethrow it. 61 | (ex-info "Exception in make-ex-info." 62 | {:function 'make-exception} 63 | t)))) 64 | 65 | 66 | (ns my-app.handler-test 67 | (:require [clojure.test :refer [report]])) 68 | 69 | (defn test-failure 70 | [] 71 | (report {:type :error :expected nil :actual (my-app.handler/make-ex-info)})) 72 | 73 | (comment 74 | ;; Run these commands in a REPL 75 | (require '[demo-appframes] :reload) 76 | 77 | ;; Should show no app-frames highlighted 78 | (alter-var-root #'clj-commons.format.exceptions/*app-frame-names* (constantly [])) 79 | (my-app.handler-test/test-failure) 80 | 81 | ;; Should show app-frames (beginning with my-app) highlighted 82 | (alter-var-root #'clj-commons.format.exceptions/*app-frame-names* (constantly [#"my-app.*"])) 83 | (my-app.handler-test/test-failure) 84 | 85 | ) 86 | -------------------------------------------------------------------------------- /test/playground.clj: -------------------------------------------------------------------------------- 1 | (ns playground 2 | (:require [clj-commons.ansi :as ansi] 3 | [clj-commons.format.exceptions :as e])) 4 | 5 | (defn deprecation-warning 6 | [id] 7 | (let [names (->> (Thread/currentThread) 8 | .getStackTrace 9 | (drop 1) ; call to .getStackTrace() 10 | (e/transform-stack-trace) 11 | (e/filter-stack-trace-maps) 12 | (drop-while #(= "playground/deprecation-warning" (:name %))) 13 | (remove :omitted) 14 | (map e/format-stack-frame) 15 | (map :name) 16 | reverse 17 | (interpose " -> "))] 18 | (ansi/perr [:yellow 19 | [:bold "WARNING:"] 20 | " " id " is deprecated ...\n"] 21 | "Call trace: " 22 | names))) 23 | 24 | (defmacro deprecated 25 | [id & body] 26 | `(do 27 | (deprecation-warning ~id) 28 | ~@body)) 29 | 30 | (defn my-deprecated-fn 31 | [] 32 | (deprecated `my-deprecated-fn)) 33 | 34 | (defn caller 35 | [] 36 | (my-deprecated-fn)) 37 | 38 | (defn deep 39 | [x] 40 | ((fn [x1] 41 | ((fn [x2] 42 | ((fn [x3] 43 | ((fn inner [x4] 44 | (/ x4 0)) x3)) 45 | x2)) 46 | x1)) 47 | x)) 48 | 49 | (comment 50 | (caller) 51 | 52 | (deep 10) 53 | 54 | (clojure.repl/pst) 55 | 56 | ) 57 | -------------------------------------------------------------------------------- /test/table_demo.clj: -------------------------------------------------------------------------------- 1 | (ns table-demo 2 | (:require [clj-commons.format.table :refer [print-table] :as t])) 3 | 4 | (def row [{:first "Arthur" :middle "C" :last "Clark"} 5 | {:first "Alan" :last "Turing"} 6 | {:first "Larry" :last "Niven"} 7 | {:first "Fred" :last "Flintstone"}]) 8 | (def columns [:first 9 | {:key :middle 10 | :width 15 11 | :align :right} 12 | {:key :last 13 | :title "Family Name" 14 | :decorator (fn [i v] 15 | (when (odd? i) 16 | :bold))}]) 17 | (comment 18 | (print-table columns row) 19 | (print-table {:style t/skinny-style 20 | :row-annotator (fn [i row] 21 | (when (= i 2) 22 | [:italic " (prescient)"])) 23 | :default-decorator 24 | (fn [i _] 25 | (when (odd? i) 26 | :blue)) 27 | :columns columns} row) 28 | 29 | (print-table 30 | [:method 31 | :path 32 | {:key :route-name :title "Name" :title-pad :right}] 33 | [{:method :get 34 | :path "/" 35 | :route-name :root-page} 36 | {:method :post 37 | :path "/reset" 38 | :route-name :reset} 39 | {:method :get 40 | :path "/status" 41 | :route-name :status}]) 42 | 43 | ) -------------------------------------------------------------------------------- /test/tiny-clojure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/pretty/5a36b44a88bbca91eaae3e771b4d9916840d0518/test/tiny-clojure.gif -------------------------------------------------------------------------------- /test/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require matcher-combinators.clj-test)) 3 | 4 | (set! *warn-on-reflection* true) 5 | --------------------------------------------------------------------------------