├── .github └── workflows │ ├── clippy.yml │ ├── rustfmt.yml │ └── tests.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── Cargo.lock.msrv ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets └── screenshot.png ├── examples ├── basic.rs ├── custom.rs ├── extended.rs ├── long.rs ├── newlines-matter.rs ├── no-debug.rs ├── serde.rs ├── str.rs └── string.rs ├── src ├── lib.rs ├── print.rs └── serde_impl.rs └── tests └── test_unsized.rs /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Clippy 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | profile: minimal 15 | components: clippy, rustfmt 16 | override: true 17 | - name: Run clippy 18 | run: make lint 19 | -------------------------------------------------------------------------------- /.github/workflows/rustfmt.yml: -------------------------------------------------------------------------------- 1 | name: Rustfmt 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | profile: minimal 15 | components: clippy, rustfmt 16 | override: true 17 | - name: Run rustfmt 18 | run: make format-check 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-latest: 7 | name: Test on Latest 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: stable 15 | profile: minimal 16 | override: true 17 | - name: Test 18 | run: make test 19 | 20 | build-stable: 21 | name: Test on 1.56.0 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v1 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: 1.56.0 29 | profile: minimal 30 | override: true 31 | - name: Use Cargo.lock.msrv 32 | run: cp Cargo.lock.msrv Cargo.lock 33 | - name: Test 34 | run: make test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug", 11 | "program": "${workspaceFolder}/", 12 | "args": [], 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `similar-asserts` are documented here. 4 | 5 | ## 1.7.0 6 | 7 | - Added support for `SIMILAR_ASSERTS_CONTEXT_SIZE`. #13 8 | 9 | ## 1.6.1 10 | 11 | - Maintenance release with some clippy fixes. 12 | 13 | ## 1.6.0 14 | 15 | - Loosen static lifetime bounds for labels. #9 16 | 17 | ## 1.5.0 18 | 19 | - Added automatic truncation of assertions. This behavior can be overridden with the 20 | new `SIMILAR_ASSERTS_MAX_STRING_LENGTH` environment variable. It defaults to `200` 21 | characters. 22 | -------------------------------------------------------------------------------- /Cargo.lock.msrv: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bstr" 7 | version = "0.2.17" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 10 | dependencies = [ 11 | "lazy_static", 12 | "memchr", 13 | "regex-automata", 14 | ] 15 | 16 | [[package]] 17 | name = "console" 18 | version = "0.15.2" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" 21 | dependencies = [ 22 | "encode_unicode", 23 | "lazy_static", 24 | "libc", 25 | "terminal_size", 26 | "winapi", 27 | ] 28 | 29 | [[package]] 30 | name = "encode_unicode" 31 | version = "0.3.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 34 | 35 | [[package]] 36 | name = "lazy_static" 37 | version = "1.4.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 40 | 41 | [[package]] 42 | name = "libc" 43 | version = "0.2.112" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" 46 | 47 | [[package]] 48 | name = "memchr" 49 | version = "2.4.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 52 | 53 | [[package]] 54 | name = "proc-macro2" 55 | version = "1.0.36" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 58 | dependencies = [ 59 | "unicode-xid", 60 | ] 61 | 62 | [[package]] 63 | name = "quote" 64 | version = "1.0.14" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" 67 | dependencies = [ 68 | "proc-macro2", 69 | ] 70 | 71 | [[package]] 72 | name = "regex-automata" 73 | version = "0.1.10" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 76 | 77 | [[package]] 78 | name = "serde" 79 | version = "1.0.133" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" 82 | dependencies = [ 83 | "serde_derive", 84 | ] 85 | 86 | [[package]] 87 | name = "serde_derive" 88 | version = "1.0.133" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" 91 | dependencies = [ 92 | "proc-macro2", 93 | "quote", 94 | "syn", 95 | ] 96 | 97 | [[package]] 98 | name = "similar" 99 | version = "2.2.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803" 102 | dependencies = [ 103 | "bstr", 104 | "unicode-segmentation", 105 | ] 106 | 107 | [[package]] 108 | name = "similar-asserts" 109 | version = "1.6.0" 110 | dependencies = [ 111 | "console", 112 | "serde", 113 | "similar", 114 | ] 115 | 116 | [[package]] 117 | name = "syn" 118 | version = "1.0.84" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b" 121 | dependencies = [ 122 | "proc-macro2", 123 | "quote", 124 | "unicode-xid", 125 | ] 126 | 127 | [[package]] 128 | name = "terminal_size" 129 | version = "0.1.17" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 132 | dependencies = [ 133 | "libc", 134 | "winapi", 135 | ] 136 | 137 | [[package]] 138 | name = "unicode-segmentation" 139 | version = "1.8.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" 142 | 143 | [[package]] 144 | name = "unicode-xid" 145 | version = "0.2.2" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 148 | 149 | [[package]] 150 | name = "winapi" 151 | version = "0.3.9" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 154 | dependencies = [ 155 | "winapi-i686-pc-windows-gnu", 156 | "winapi-x86_64-pc-windows-gnu", 157 | ] 158 | 159 | [[package]] 160 | name = "winapi-i686-pc-windows-gnu" 161 | version = "0.4.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 164 | 165 | [[package]] 166 | name = "winapi-x86_64-pc-windows-gnu" 167 | version = "0.4.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 170 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "similar-asserts" 3 | version = "1.7.0" 4 | authors = ["Armin Ronacher "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "provides assert_eq! like macros with colorized diff output" 8 | repository = "https://github.com/mitsuhiko/similar-asserts" 9 | keywords = ["assert", "diff", "color"] 10 | readme = "README.md" 11 | exclude = ["assets/*"] 12 | rust-version = "1.46.0" 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | 17 | [features] 18 | default = ["unicode"] 19 | unicode = ["similar/unicode"] 20 | 21 | [dependencies] 22 | similar = { version = "2.2.0", features = ["inline"] } 23 | console = { version = "0.15.0", default-features = false } 24 | serde = { version = "1.0.123", optional = true } 25 | 26 | [dev-dependencies] 27 | serde = { version = "1.0.123", features = ["derive"] } 28 | 29 | [[example]] 30 | name = "serde" 31 | required-features = ["serde"] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | build: 4 | @cargo build --all-features 5 | 6 | check: 7 | @cargo check --all-features 8 | @cargo check --all-features --examples 9 | 10 | doc: 11 | @cargo doc --all-features 12 | 13 | test: check 14 | @cargo test 15 | @cargo test --all-features 16 | @cargo test --no-default-features 17 | 18 | format: 19 | @rustup component add rustfmt 2> /dev/null 20 | @cargo fmt --all 21 | 22 | format-check: 23 | @rustup component add rustfmt 2> /dev/null 24 | @cargo fmt --all -- --check 25 | 26 | lint: 27 | @rustup component add clippy 2> /dev/null 28 | @cargo clippy 29 | 30 | .PHONY: all doc test format format-check lint 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # similar-asserts 2 | 3 | [![Crates.io](https://img.shields.io/crates/d/similar-asserts.svg)](https://crates.io/crates/similar-asserts) 4 | [![License](https://img.shields.io/github/license/mitsuhiko/similar-asserts)](https://github.com/mitsuhiko/similar-asserts/blob/main/LICENSE) 5 | [![Documentation](https://docs.rs/similar-asserts/badge.svg)](https://docs.rs/similar-asserts) 6 | 7 | `similar-asserts` is a crate that enhances the default assertion experience 8 | by using [similar](https://crates.io/crates/similar) for diffing. It supports 9 | comparing either `Debug` or `Serialize` representations of values. On failed 10 | assertions it renders out a colorized diff to the terminal. 11 | 12 | ```rust 13 | fn main() { 14 | let reference = vec![1, 2, 3, 4]; 15 | similar_asserts::assert_eq!(reference, (0..4).collect::>()); 16 | } 17 | ``` 18 | 19 | ![](https://raw.githubusercontent.com/mitsuhiko/similar-asserts/main/assets/screenshot.png) 20 | 21 | ## Related Projects 22 | 23 | * [insta](https://insta.rs) snapshot testing library 24 | * [similar](https://insta.rs/similar) diffing library 25 | 26 | ## License and Links 27 | 28 | - [Documentation](https://docs.rs/similar-asserts/) 29 | - [Issue Tracker](https://github.com/mitsuhiko/similar-asserts/issues) 30 | - [Examples](https://github.com/mitsuhiko/similar-asserts/tree/main/examples) 31 | - License: [Apache-2.0](https://github.com/mitsuhiko/similar-asserts/blob/main/LICENSE) 32 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitsuhiko/similar-asserts/30a6c8a78e7572fdb7b2dadca765236de793ca89/assets/screenshot.png -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let reference = vec![1, 2, 3, 4]; 3 | similar_asserts::assert_eq!(reference, (0..4).collect::>()); 4 | } 5 | -------------------------------------------------------------------------------- /examples/custom.rs: -------------------------------------------------------------------------------- 1 | use similar_asserts::SimpleDiff; 2 | 3 | fn main() { 4 | panic!( 5 | "Not equal\n\n{}", 6 | SimpleDiff::from_str("a\nb\n", "b\nb\n", "left", "right") 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /examples/extended.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | enum Tag { 3 | Major, 4 | Minor, 5 | Value, 6 | } 7 | 8 | fn main() { 9 | let reference = vec![(Tag::Major, 2), (Tag::Minor, 20), (Tag::Value, 0)]; 10 | 11 | similar_asserts::assert_eq!( 12 | expected: reference, 13 | actual: 14 | vec![ 15 | (Tag::Major, 2), 16 | (Tag::Minor, 0), 17 | (Tag::Value, 0), 18 | (Tag::Value, 1) 19 | ], 20 | "some stuff here {}", 21 | 42, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/long.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let reference = (1..100).collect::>(); 3 | similar_asserts::assert_eq!(reference, (0..98).collect::>()); 4 | } 5 | -------------------------------------------------------------------------------- /examples/newlines-matter.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let reference = "foo\r\nbar"; 3 | similar_asserts::assert_eq!(reference, "foo\nbar"); 4 | } 5 | -------------------------------------------------------------------------------- /examples/no-debug.rs: -------------------------------------------------------------------------------- 1 | struct TypeWithoutDebug; 2 | 3 | impl PartialEq for TypeWithoutDebug { 4 | fn eq(&self, _other: &Self) -> bool { 5 | false 6 | } 7 | } 8 | 9 | fn main() { 10 | similar_asserts::assert_eq!(TypeWithoutDebug, TypeWithoutDebug); 11 | } 12 | -------------------------------------------------------------------------------- /examples/serde.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize, PartialEq)] 4 | pub enum MyEnum { 5 | One, 6 | Two, 7 | } 8 | 9 | #[derive(Serialize, PartialEq)] 10 | pub struct Foo { 11 | a: Vec, 12 | b: MyEnum, 13 | } 14 | 15 | fn main() { 16 | let reference = Foo { 17 | a: vec![1, 2, 3, 4], 18 | b: MyEnum::One, 19 | }; 20 | let actual = Foo { 21 | a: vec![1, 2, 4, 5], 22 | b: MyEnum::Two, 23 | }; 24 | 25 | similar_asserts::assert_serde_eq!(&reference, &actual); 26 | } 27 | -------------------------------------------------------------------------------- /examples/str.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let reference = "Hello\nWorld"; 3 | similar_asserts::assert_eq!(reference, "Goodbye\nWorld"); 4 | } 5 | -------------------------------------------------------------------------------- /examples/string.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let reference = "Hello\nWorld".to_string(); 3 | similar_asserts::assert_eq!(reference, "Goodbye\nWorld"); 4 | } 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `similar-asserts` is a crate that enhances the default assertion 2 | //! experience by using [similar](https://crates.io/crates/similar) for diffing. 3 | //! On failed assertions it renders out a colorized diff to the terminal. 4 | //! 5 | //! It comes with a handful of macros to replace [`std::assert_eq!`] with: 6 | //! 7 | //! - [`assert_eq!`]: diffs `Debug` on assertion failure. 8 | #![cfg_attr( 9 | feature = "serde", 10 | doc = r#" 11 | - [`assert_serde_eq!`]: diffs `Serialize` on assertion failure. 12 | "# 13 | )] 14 | //! 15 | //! ![](https://raw.githubusercontent.com/mitsuhiko/similar-asserts/main/assets/screenshot.png) 16 | //! 17 | //! # Usage 18 | //! 19 | //! ```rust 20 | //! use similar_asserts::assert_eq; 21 | //! assert_eq!((1..3).collect::>(), vec![1, 2]); 22 | //! ``` 23 | //! 24 | //! Optionally the assertion macros also let you "name" the left and right 25 | //! side which will produce slightly more explicit output: 26 | //! 27 | //! ```rust 28 | //! use similar_asserts::assert_eq; 29 | //! assert_eq!(expected: vec![1, 2], actual: (1..3).collect::>()); 30 | //! ``` 31 | //! 32 | //! # Feature Flags 33 | //! 34 | //! * `unicode` enable improved character matching (enabled by default) 35 | //! * `serde` turns on support for serde. 36 | //! 37 | //! # Faster Builds 38 | //! 39 | //! This crate works best if you add it as `dev-dependency` only. To make your code 40 | //! still compile you can use conditional uses that override the default uses for the 41 | //! `assert_eq!` macro from the stdlib: 42 | //! 43 | //! ``` 44 | //! #[cfg(test)] 45 | //! use similar_asserts::assert_eq; 46 | //! ``` 47 | //! 48 | //! Since `similar_asserts` uses the `similar` library for diffing you can also 49 | //! enable optimziation for them in all build types for quicker diffing. Add 50 | //! this to your `Cargo.toml`: 51 | //! 52 | //! ```toml 53 | //! [profile.dev.package.similar] 54 | //! opt-level = 3 55 | //! ``` 56 | //! 57 | //! # String Truncation 58 | //! 59 | //! By default the assertion only shows 200 characters. This can be changed with the 60 | //! `SIMILAR_ASSERTS_MAX_STRING_LENGTH` environment variable. Setting it to `0` disables 61 | //! all truncation, otherwise it sets the maximum number of characters before truncation 62 | //! kicks in. 63 | //! 64 | //! # Context Size 65 | //! 66 | //! Diffs displayed by assertions have a default context size of 4 (show up to 4 lines above and 67 | //! below changes). This can be changed with the `SIMILAR_ASSERTS_CONTEXT_SIZE` environment 68 | //! variable. 69 | //! 70 | //! # Manual Diff Printing 71 | //! 72 | //! If you want to build your own comparison macros and you need a quick and simple 73 | //! way to render diffs, you can use the [`SimpleDiff`] type and display it: 74 | //! 75 | //! ```should_panic 76 | //! use similar_asserts::SimpleDiff; 77 | //! panic!("Not equal\n\n{}", SimpleDiff::from_str("a\nb\n", "b\nb\n", "left", "right")); 78 | //! ``` 79 | use std::borrow::Cow; 80 | use std::fmt::{self, Display}; 81 | use std::sync::atomic::{AtomicUsize, Ordering}; 82 | use std::time::Duration; 83 | 84 | use console::{style, Style}; 85 | use similar::{Algorithm, ChangeTag, TextDiff}; 86 | 87 | #[cfg(feature = "serde")] 88 | #[doc(hidden)] 89 | pub mod serde_impl; 90 | 91 | // This needs to be public as we are using it internally in a macro. 92 | #[doc(hidden)] 93 | pub mod print; 94 | 95 | /// The maximum number of characters a string can be long before truncating. 96 | fn get_max_string_length() -> usize { 97 | static TRUNCATE: AtomicUsize = AtomicUsize::new(!0); 98 | get_usize_from_env(&TRUNCATE, "SIMILAR_ASSERTS_MAX_STRING_LENGTH", 200) 99 | } 100 | 101 | /// The context size for diff groups. 102 | fn get_context_size() -> usize { 103 | static CONTEXT_SIZE: AtomicUsize = AtomicUsize::new(!0); 104 | get_usize_from_env(&CONTEXT_SIZE, "SIMILAR_ASSERTS_CONTEXT_SIZE", 4) 105 | } 106 | 107 | /// Parse a `usize` value from an environment variable, cached in a static atomic. 108 | fn get_usize_from_env(value: &'static AtomicUsize, var: &str, default: usize) -> usize { 109 | let rv = value.load(Ordering::Relaxed); 110 | if rv != !0 { 111 | return rv; 112 | } 113 | let rv: usize = std::env::var(var) 114 | .ok() 115 | .and_then(|x| x.parse().ok()) 116 | .unwrap_or(default); 117 | value.store(rv, Ordering::Relaxed); 118 | rv 119 | } 120 | 121 | /// A console printable diff. 122 | /// 123 | /// The [`Display`](std::fmt::Display) implementation of this type renders out a 124 | /// diff with ANSI markers so it creates a nice colored diff. This can be used to 125 | /// build your own custom assertions in addition to the ones from this crate. 126 | /// 127 | /// It does not provide much customization beyond what's possible done by default. 128 | pub struct SimpleDiff<'a> { 129 | pub(crate) left_short: Cow<'a, str>, 130 | pub(crate) right_short: Cow<'a, str>, 131 | pub(crate) left_expanded: Option>, 132 | pub(crate) right_expanded: Option>, 133 | pub(crate) left_label: &'a str, 134 | pub(crate) right_label: &'a str, 135 | } 136 | 137 | impl<'a> SimpleDiff<'a> { 138 | /// Creates a diff from two strings. 139 | /// 140 | /// `left_label` and `right_label` are the labels used for the two sides. 141 | /// `"left"` and `"right"` are sensible defaults if you don't know what 142 | /// to pick. 143 | pub fn from_str( 144 | left: &'a str, 145 | right: &'a str, 146 | left_label: &'a str, 147 | right_label: &'a str, 148 | ) -> SimpleDiff<'a> { 149 | SimpleDiff { 150 | left_short: left.into(), 151 | right_short: right.into(), 152 | left_expanded: None, 153 | right_expanded: None, 154 | left_label, 155 | right_label, 156 | } 157 | } 158 | 159 | #[doc(hidden)] 160 | pub fn __from_macro( 161 | left_short: Option>, 162 | right_short: Option>, 163 | left_expanded: Option>, 164 | right_expanded: Option>, 165 | left_label: &'a str, 166 | right_label: &'a str, 167 | ) -> SimpleDiff<'a> { 168 | SimpleDiff { 169 | left_short: left_short.unwrap_or_else(|| "".into()), 170 | right_short: right_short.unwrap_or_else(|| "".into()), 171 | left_expanded, 172 | right_expanded, 173 | left_label, 174 | right_label, 175 | } 176 | } 177 | 178 | /// Returns the left side as string. 179 | fn left(&self) -> &str { 180 | self.left_expanded.as_deref().unwrap_or(&self.left_short) 181 | } 182 | 183 | /// Returns the right side as string. 184 | fn right(&self) -> &str { 185 | self.right_expanded.as_deref().unwrap_or(&self.right_short) 186 | } 187 | 188 | /// Returns the label padding 189 | fn label_padding(&self) -> usize { 190 | self.left_label 191 | .chars() 192 | .count() 193 | .max(self.right_label.chars().count()) 194 | } 195 | 196 | #[doc(hidden)] 197 | #[track_caller] 198 | pub fn fail_assertion(&self, hint: &dyn Display) { 199 | // prefer the shortened version here. 200 | let len = get_max_string_length(); 201 | let (left, left_truncated) = truncate_str(&self.left_short, len); 202 | let (right, right_truncated) = truncate_str(&self.right_short, len); 203 | 204 | panic!( 205 | "assertion failed: `({} == {})`{}'\ 206 | \n {:>label_padding$}: `{:?}`{}\ 207 | \n {:>label_padding$}: `{:?}`{}\ 208 | \n\n{}\n", 209 | self.left_label, 210 | self.right_label, 211 | hint, 212 | self.left_label, 213 | DebugStrTruncated(left, left_truncated), 214 | if left_truncated { " (truncated)" } else { "" }, 215 | self.right_label, 216 | DebugStrTruncated(right, right_truncated), 217 | if right_truncated { " (truncated)" } else { "" }, 218 | &self, 219 | label_padding = self.label_padding(), 220 | ); 221 | } 222 | } 223 | 224 | fn truncate_str(s: &str, chars: usize) -> (&str, bool) { 225 | if chars == 0 { 226 | return (s, false); 227 | } 228 | s.char_indices() 229 | .enumerate() 230 | .find_map(|(idx, (offset, _))| { 231 | if idx == chars { 232 | Some((&s[..offset], true)) 233 | } else { 234 | None 235 | } 236 | }) 237 | .unwrap_or((s, false)) 238 | } 239 | 240 | struct DebugStrTruncated<'s>(&'s str, bool); 241 | 242 | impl fmt::Debug for DebugStrTruncated<'_> { 243 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 244 | if self.1 { 245 | let s = format!("{}...", self.0); 246 | fmt::Debug::fmt(&s, f) 247 | } else { 248 | fmt::Debug::fmt(&self.0, f) 249 | } 250 | } 251 | } 252 | 253 | fn trailing_newline(s: &str) -> &str { 254 | if s.ends_with("\r\n") { 255 | "\r\n" 256 | } else if s.ends_with("\r") { 257 | "\r" 258 | } else if s.ends_with("\n") { 259 | "\n" 260 | } else { 261 | "" 262 | } 263 | } 264 | 265 | fn detect_newlines(s: &str) -> (bool, bool, bool) { 266 | let mut last_char = None; 267 | let mut detected_crlf = false; 268 | let mut detected_cr = false; 269 | let mut detected_lf = false; 270 | 271 | for c in s.chars() { 272 | if c == '\n' { 273 | if last_char.take() == Some('\r') { 274 | detected_crlf = true; 275 | } else { 276 | detected_lf = true; 277 | } 278 | } 279 | if last_char == Some('\r') { 280 | detected_cr = true; 281 | } 282 | last_char = Some(c); 283 | } 284 | if last_char == Some('\r') { 285 | detected_cr = true; 286 | } 287 | 288 | (detected_cr, detected_crlf, detected_lf) 289 | } 290 | 291 | #[allow(clippy::match_like_matches_macro)] 292 | fn newlines_matter(left: &str, right: &str) -> bool { 293 | if trailing_newline(left) != trailing_newline(right) { 294 | return true; 295 | } 296 | 297 | let (cr1, crlf1, lf1) = detect_newlines(left); 298 | let (cr2, crlf2, lf2) = detect_newlines(right); 299 | 300 | match (cr1 || cr2, crlf1 || crlf2, lf1 || lf2) { 301 | (false, false, false) => false, 302 | (true, false, false) => false, 303 | (false, true, false) => false, 304 | (false, false, true) => false, 305 | _ => true, 306 | } 307 | } 308 | 309 | impl fmt::Display for SimpleDiff<'_> { 310 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 311 | let left = self.left(); 312 | let right = self.right(); 313 | let newlines_matter = newlines_matter(left, right); 314 | 315 | if left == right { 316 | writeln!( 317 | f, 318 | "{}: the two values are the same in string form.", 319 | style("Invisible differences").bold(), 320 | )?; 321 | return Ok(()); 322 | } 323 | 324 | let diff = TextDiff::configure() 325 | .timeout(Duration::from_millis(200)) 326 | .algorithm(Algorithm::Patience) 327 | .diff_lines(left, right); 328 | 329 | writeln!( 330 | f, 331 | "{} ({}{}|{}{}):", 332 | style("Differences").bold(), 333 | style("-").red().dim(), 334 | style(self.left_label).red(), 335 | style("+").green().dim(), 336 | style(self.right_label).green(), 337 | )?; 338 | for (idx, group) in diff.grouped_ops(get_context_size()).into_iter().enumerate() { 339 | if idx > 0 { 340 | writeln!(f, "@ {}", style("~~~").dim())?; 341 | } 342 | for op in group { 343 | for change in diff.iter_inline_changes(&op) { 344 | let (marker, style) = match change.tag() { 345 | ChangeTag::Delete => ('-', Style::new().red()), 346 | ChangeTag::Insert => ('+', Style::new().green()), 347 | ChangeTag::Equal => (' ', Style::new().dim()), 348 | }; 349 | write!(f, "{}", style.apply_to(marker).dim().bold())?; 350 | for &(emphasized, value) in change.values() { 351 | let value = if newlines_matter { 352 | Cow::Owned( 353 | value 354 | .replace("\r", "␍\r") 355 | .replace("\n", "␊\n") 356 | .replace("␍\r␊\n", "␍␊\r\n"), 357 | ) 358 | } else { 359 | Cow::Borrowed(value) 360 | }; 361 | if emphasized { 362 | write!(f, "{}", style.clone().underlined().bold().apply_to(value))?; 363 | } else { 364 | write!(f, "{}", style.apply_to(value))?; 365 | } 366 | } 367 | if change.missing_newline() { 368 | writeln!(f)?; 369 | } 370 | } 371 | } 372 | } 373 | 374 | Ok(()) 375 | } 376 | } 377 | 378 | #[doc(hidden)] 379 | #[macro_export] 380 | macro_rules! __assert_eq { 381 | ( 382 | $method:ident, 383 | $left_label:ident, 384 | $left:expr, 385 | $right_label:ident, 386 | $right:expr, 387 | $hint_suffix:expr 388 | ) => {{ 389 | match (&($left), &($right)) { 390 | (left_val, right_val) => 391 | { 392 | #[allow(unused_mut)] 393 | if !(*left_val == *right_val) { 394 | use $crate::print::{PrintMode, PrintObject}; 395 | let left_label = stringify!($left_label); 396 | let right_label = stringify!($right_label); 397 | let mut left_val_tup1 = (&left_val,); 398 | let mut right_val_tup1 = (&right_val,); 399 | let mut left_val_tup2 = (&left_val,); 400 | let mut right_val_tup2 = (&right_val,); 401 | let left_short = left_val_tup1.print_object(PrintMode::Default); 402 | let right_short = right_val_tup1.print_object(PrintMode::Default); 403 | let left_expanded = left_val_tup2.print_object(PrintMode::Expanded); 404 | let right_expanded = right_val_tup2.print_object(PrintMode::Expanded); 405 | let diff = $crate::SimpleDiff::__from_macro( 406 | left_short, 407 | right_short, 408 | left_expanded, 409 | right_expanded, 410 | left_label, 411 | right_label, 412 | ); 413 | diff.fail_assertion(&$hint_suffix); 414 | } 415 | } 416 | } 417 | }}; 418 | } 419 | 420 | /// Asserts that two expressions are equal to each other (using [`PartialEq`]). 421 | /// 422 | /// On panic, this macro will print the values of the expressions with their 423 | /// [`Debug`] or [`ToString`] representations with a colorized diff of the 424 | /// changes in the debug output. It picks [`Debug`] for all types that are 425 | /// not strings themselves and [`ToString`] for [`str`] and [`String`]. 426 | /// 427 | /// Like [`assert!`], this macro has a second form, where a custom panic 428 | /// message can be provided. 429 | /// 430 | /// ```rust 431 | /// use similar_asserts::assert_eq; 432 | /// assert_eq!((1..3).collect::>(), vec![1, 2]); 433 | /// ``` 434 | #[macro_export] 435 | macro_rules! assert_eq { 436 | ($left_label:ident: $left:expr, $right_label:ident: $right:expr $(,)?) => ({ 437 | $crate::__assert_eq!(make_diff, $left_label, $left, $right_label, $right, ""); 438 | }); 439 | ($left_label:ident: $left:expr, $right_label:ident: $right:expr, $($arg:tt)*) => ({ 440 | $crate::__assert_eq!(make_diff, $left_label, $left, $right_label, $right, format_args!(": {}", format_args!($($arg)*))); 441 | }); 442 | ($left:expr, $right:expr $(,)?) => ({ 443 | $crate::assert_eq!(left: $left, right: $right); 444 | }); 445 | ($left:expr, $right:expr, $($arg:tt)*) => ({ 446 | $crate::assert_eq!(left: $left, right: $right, $($arg)*); 447 | }); 448 | } 449 | 450 | /// Deprecated macro. Use [`assert_eq!`] instead. 451 | #[macro_export] 452 | #[doc(hidden)] 453 | #[deprecated(since = "1.4.0", note = "use assert_eq! instead")] 454 | macro_rules! assert_str_eq { 455 | ($left_label:ident: $left:expr, $right_label:ident: $right:expr $(,)?) => ({ 456 | $crate::assert_eq!($left_label: $left, $right_label: $right); 457 | }); 458 | ($left_label:ident: $left:expr, $right_label:ident: $right:expr, $($arg:tt)*) => ({ 459 | $crate::assert_eq!($left_label: $left, $right_label: $right, $($arg)*); 460 | }); 461 | ($left:expr, $right:expr $(,)?) => ({ 462 | $crate::assert_eq!($left, $right); 463 | }); 464 | ($left:expr, $right:expr, $($arg:tt)*) => ({ 465 | $crate::assert_eq!($left, $right, $($arg)*); 466 | }); 467 | } 468 | 469 | #[test] 470 | fn test_newlines_matter() { 471 | assert!(newlines_matter("\r\n", "\n")); 472 | assert!(newlines_matter("foo\n", "foo")); 473 | assert!(newlines_matter("foo\r\nbar", "foo\rbar")); 474 | assert!(newlines_matter("foo\r\nbar", "foo\nbar")); 475 | assert!(newlines_matter("foo\r\nbar\n", "foobar")); 476 | assert!(newlines_matter("foo\nbar\r\n", "foo\nbar\r\n")); 477 | assert!(newlines_matter("foo\nbar\n", "foo\nbar")); 478 | 479 | assert!(!newlines_matter("foo\nbar", "foo\nbar")); 480 | assert!(!newlines_matter("foo\nbar\n", "foo\nbar\n")); 481 | assert!(!newlines_matter("foo\r\nbar", "foo\r\nbar")); 482 | assert!(!newlines_matter("foo\r\nbar\r\n", "foo\r\nbar\r\n")); 483 | assert!(!newlines_matter("foo\r\nbar", "foo\r\nbar")); 484 | } 485 | 486 | #[test] 487 | fn test_truncate_str() { 488 | assert_eq!(truncate_str("foobar", 20), ("foobar", false)); 489 | assert_eq!(truncate_str("foobar", 2), ("fo", true)); 490 | assert_eq!(truncate_str("🔥🔥🔥🔥🔥", 2), ("🔥🔥", true)); 491 | } 492 | -------------------------------------------------------------------------------- /src/print.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::Debug; 3 | 4 | pub trait StringRepr: AsRef {} 5 | 6 | impl StringRepr for str {} 7 | impl StringRepr for String {} 8 | impl StringRepr for Cow<'_, str> {} 9 | impl StringRepr for &T {} 10 | 11 | /// Defines how the object is printed. 12 | pub enum PrintMode { 13 | /// The regular print mode. If an object does not return 14 | /// something for this print mode it's not formattable. 15 | Default, 16 | /// Some objects have an extra expanded print mode with pretty newlines. 17 | Expanded, 18 | } 19 | 20 | pub trait PrintObject<'a> { 21 | fn print_object(self, mode: PrintMode) -> Option>; 22 | } 23 | 24 | impl<'a, 'b: 'a, T: StringRepr + ?Sized + 'a> PrintObject<'a> for (&'b T,) { 25 | fn print_object(self, mode: PrintMode) -> Option> { 26 | match mode { 27 | PrintMode::Default => Some(Cow::Borrowed(self.0.as_ref())), 28 | PrintMode::Expanded => None, 29 | } 30 | } 31 | } 32 | 33 | impl<'a, 'b: 'a, T: Debug + 'a> PrintObject<'a> for &'b (T,) { 34 | fn print_object(self, mode: PrintMode) -> Option> { 35 | Some( 36 | match mode { 37 | PrintMode::Default => format!("{:?}", self.0), 38 | PrintMode::Expanded => format!("{:#?}", self.0), 39 | } 40 | .into(), 41 | ) 42 | } 43 | } 44 | 45 | impl<'a, 'b: 'a, T: 'a> PrintObject<'a> for &'b mut (T,) { 46 | fn print_object(self, _mode: PrintMode) -> Option> { 47 | fn type_name_of_val(_: T) -> &'static str { 48 | std::any::type_name::() 49 | } 50 | let s = type_name_of_val(&self.0).trim_start_matches('&'); 51 | if s.is_empty() { 52 | None 53 | } else { 54 | Some(Cow::Borrowed(s)) 55 | } 56 | } 57 | } 58 | 59 | #[test] 60 | fn test_object() { 61 | macro_rules! print_object { 62 | ($expr:expr, $mode:ident) => {{ 63 | use $crate::print::PrintObject; 64 | #[allow(unused_mut)] 65 | let mut _tmp = ($expr,); 66 | _tmp.print_object($crate::print::PrintMode::$mode) 67 | .map(|x| x.to_string()) 68 | }}; 69 | } 70 | 71 | struct NoDebugNoString; 72 | 73 | struct DoNotCallMe; 74 | 75 | impl DoNotCallMe { 76 | #[allow(unused)] 77 | fn print_object(&self, mode: PrintMode) { 78 | panic!("never call me"); 79 | } 80 | } 81 | 82 | assert_eq!( 83 | print_object!(&DoNotCallMe, Default).as_deref(), 84 | Some("similar_asserts::print::test_object::DoNotCallMe") 85 | ); 86 | assert_eq!( 87 | print_object!(&NoDebugNoString, Default).as_deref(), 88 | Some("similar_asserts::print::test_object::NoDebugNoString") 89 | ); 90 | assert_eq!( 91 | print_object!(vec![1, 2, 3], Default).as_deref(), 92 | Some("[1, 2, 3]") 93 | ); 94 | assert_eq!( 95 | print_object!(vec![1, 2, 3], Expanded).as_deref(), 96 | Some("[\n 1,\n 2,\n 3,\n]") 97 | ); 98 | assert_eq!(print_object!(&"Hello", Default).as_deref(), Some("Hello")); 99 | assert_eq!(print_object!(&"Hello", Expanded).as_deref(), None); 100 | } 101 | -------------------------------------------------------------------------------- /src/serde_impl.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::ser::{ 4 | SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, 5 | SerializeTupleStruct, SerializeTupleVariant, 6 | }; 7 | use serde::{Serialize, Serializer}; 8 | 9 | pub struct Debug<'a, T: Serialize + ?Sized>(pub &'a T); 10 | 11 | impl<'a, T: Serialize + ?Sized> fmt::Debug for Debug<'a, T> { 12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | self.0.serialize(DebugSerializer(f))?; 14 | Ok(()) 15 | } 16 | } 17 | 18 | macro_rules! simple_serialize { 19 | ($name:ident, $type:ty) => { 20 | fn $name(self, v: $type) -> Result { 21 | fmt::Debug::fmt(&v, self.0) 22 | } 23 | }; 24 | } 25 | 26 | pub struct DebugSerializer<'a, 'b: 'a>(pub &'a mut fmt::Formatter<'b>); 27 | 28 | impl<'a, 'b: 'a> Serializer for DebugSerializer<'a, 'b> { 29 | type Ok = (); 30 | type Error = fmt::Error; 31 | 32 | type SerializeSeq = SeqSerializer<'a, 'b>; 33 | type SerializeTuple = TupleSerializer<'a, 'b>; 34 | type SerializeTupleStruct = TupleSerializer<'a, 'b>; 35 | type SerializeTupleVariant = TupleSerializer<'a, 'b>; 36 | type SerializeMap = MapSerializer<'a, 'b>; 37 | type SerializeStruct = StructSerializer<'a, 'b>; 38 | type SerializeStructVariant = StructSerializer<'a, 'b>; 39 | 40 | simple_serialize!(serialize_bool, bool); 41 | simple_serialize!(serialize_i8, i8); 42 | simple_serialize!(serialize_i16, i16); 43 | simple_serialize!(serialize_i32, i32); 44 | simple_serialize!(serialize_i64, i64); 45 | simple_serialize!(serialize_i128, i128); 46 | simple_serialize!(serialize_u8, u8); 47 | simple_serialize!(serialize_u16, u16); 48 | simple_serialize!(serialize_u32, u32); 49 | simple_serialize!(serialize_u64, u64); 50 | simple_serialize!(serialize_u128, u128); 51 | simple_serialize!(serialize_f32, f32); 52 | simple_serialize!(serialize_f64, f64); 53 | simple_serialize!(serialize_char, char); 54 | simple_serialize!(serialize_str, &str); 55 | simple_serialize!(serialize_bytes, &[u8]); 56 | 57 | fn serialize_none(self) -> Result { 58 | self.serialize_unit_struct("None") 59 | } 60 | 61 | fn serialize_some(self, value: &T) -> Result { 62 | self.serialize_newtype_struct("Some", value) 63 | } 64 | 65 | fn serialize_unit(self) -> Result { 66 | write!(self.0, "()") 67 | } 68 | 69 | fn serialize_unit_struct(self, name: &'static str) -> Result { 70 | SerializeTupleStruct::end(self.serialize_tuple_struct(name, 0)?) 71 | } 72 | 73 | fn serialize_unit_variant( 74 | self, 75 | _name: &'static str, 76 | _variant_index: u32, 77 | variant: &'static str, 78 | ) -> Result { 79 | self.serialize_unit_struct(variant) 80 | } 81 | 82 | fn serialize_newtype_struct( 83 | self, 84 | name: &'static str, 85 | value: &T, 86 | ) -> Result { 87 | let mut tuple = self.serialize_tuple_struct(name, 1)?; 88 | SerializeTupleStruct::serialize_field(&mut tuple, value)?; 89 | SerializeTupleStruct::end(tuple) 90 | } 91 | 92 | fn serialize_newtype_variant( 93 | self, 94 | _name: &'static str, 95 | _variant_index: u32, 96 | variant: &'static str, 97 | value: &T, 98 | ) -> Result { 99 | self.serialize_newtype_struct(variant, value) 100 | } 101 | 102 | fn serialize_seq(self, _len: Option) -> Result { 103 | Ok(SeqSerializer(self.0.debug_list())) 104 | } 105 | 106 | fn serialize_tuple_struct( 107 | self, 108 | name: &'static str, 109 | _len: usize, 110 | ) -> Result { 111 | Ok(TupleSerializer(self.0.debug_tuple(name))) 112 | } 113 | 114 | fn serialize_tuple(self, len: usize) -> Result { 115 | self.serialize_tuple_struct("", len) 116 | } 117 | 118 | fn serialize_tuple_variant( 119 | self, 120 | _name: &'static str, 121 | _variant_index: u32, 122 | variant: &'static str, 123 | len: usize, 124 | ) -> Result { 125 | self.serialize_tuple_struct(variant, len) 126 | } 127 | 128 | fn serialize_map(self, _len: Option) -> Result { 129 | Ok(MapSerializer(self.0.debug_map())) 130 | } 131 | 132 | fn serialize_struct( 133 | self, 134 | name: &'static str, 135 | _len: usize, 136 | ) -> Result { 137 | Ok(StructSerializer(self.0.debug_struct(name))) 138 | } 139 | 140 | fn serialize_struct_variant( 141 | self, 142 | _name: &'static str, 143 | _variant_index: u32, 144 | variant: &'static str, 145 | len: usize, 146 | ) -> Result { 147 | self.serialize_struct(variant, len) 148 | } 149 | } 150 | 151 | pub struct SeqSerializer<'a, 'b: 'a>(fmt::DebugList<'a, 'b>); 152 | 153 | impl<'a, 'b: 'a> SerializeSeq for SeqSerializer<'a, 'b> { 154 | type Ok = (); 155 | type Error = fmt::Error; 156 | 157 | fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> { 158 | self.0.entry(&Debug(value)); 159 | Ok(()) 160 | } 161 | 162 | fn end(mut self) -> Result<(), Self::Error> { 163 | self.0.finish() 164 | } 165 | } 166 | 167 | pub struct TupleSerializer<'a, 'b: 'a>(fmt::DebugTuple<'a, 'b>); 168 | 169 | impl<'a, 'b: 'a> SerializeTuple for TupleSerializer<'a, 'b> { 170 | type Ok = (); 171 | type Error = fmt::Error; 172 | 173 | fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> { 174 | self.0.field(&Debug(value)); 175 | Ok(()) 176 | } 177 | 178 | fn end(mut self) -> Result<(), Self::Error> { 179 | self.0.finish() 180 | } 181 | } 182 | 183 | impl<'a, 'b: 'a> SerializeTupleStruct for TupleSerializer<'a, 'b> { 184 | type Ok = (); 185 | type Error = fmt::Error; 186 | 187 | fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> { 188 | SerializeTuple::serialize_element(self, value) 189 | } 190 | 191 | fn end(self) -> Result<(), Self::Error> { 192 | SerializeTuple::end(self) 193 | } 194 | } 195 | 196 | impl<'a, 'b: 'a> SerializeTupleVariant for TupleSerializer<'a, 'b> { 197 | type Ok = (); 198 | type Error = fmt::Error; 199 | 200 | fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> { 201 | SerializeTuple::serialize_element(self, value) 202 | } 203 | 204 | fn end(self) -> Result<(), Self::Error> { 205 | SerializeTuple::end(self) 206 | } 207 | } 208 | 209 | pub struct MapSerializer<'a, 'b: 'a>(fmt::DebugMap<'a, 'b>); 210 | 211 | impl<'a, 'b: 'a> SerializeMap for MapSerializer<'a, 'b> { 212 | type Ok = (); 213 | type Error = fmt::Error; 214 | 215 | fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> { 216 | self.0.key(&Debug(key)); 217 | Ok(()) 218 | } 219 | 220 | fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> { 221 | self.0.value(&Debug(value)); 222 | Ok(()) 223 | } 224 | 225 | fn serialize_entry( 226 | &mut self, 227 | key: &K, 228 | value: &V, 229 | ) -> Result<(), Self::Error> { 230 | self.0.entry(&Debug(key), &Debug(value)); 231 | Ok(()) 232 | } 233 | 234 | fn end(mut self) -> Result<(), Self::Error> { 235 | self.0.finish() 236 | } 237 | } 238 | 239 | pub struct StructSerializer<'a, 'b: 'a>(fmt::DebugStruct<'a, 'b>); 240 | 241 | impl<'a, 'b: 'a> SerializeStruct for StructSerializer<'a, 'b> { 242 | type Ok = (); 243 | type Error = fmt::Error; 244 | 245 | fn serialize_field( 246 | &mut self, 247 | key: &'static str, 248 | value: &T, 249 | ) -> Result<(), Self::Error> { 250 | self.0.field(key, &Debug(value)); 251 | Ok(()) 252 | } 253 | 254 | fn end(mut self) -> Result<(), Self::Error> { 255 | self.0.finish() 256 | } 257 | } 258 | 259 | impl<'a, 'b: 'a> SerializeStructVariant for StructSerializer<'a, 'b> { 260 | type Ok = (); 261 | type Error = fmt::Error; 262 | 263 | fn serialize_field( 264 | &mut self, 265 | key: &'static str, 266 | value: &T, 267 | ) -> Result<(), Self::Error> { 268 | SerializeStruct::serialize_field(self, key, value) 269 | } 270 | 271 | fn end(self) -> Result<(), Self::Error> { 272 | SerializeStruct::end(self) 273 | } 274 | } 275 | 276 | #[doc(hidden)] 277 | #[macro_export] 278 | macro_rules! __assert_serde_eq { 279 | ( 280 | $method:ident, 281 | $left_label:ident, 282 | $left:expr, 283 | $right_label:ident, 284 | $right:expr, 285 | $hint_suffix:expr 286 | ) => {{ 287 | match (&($left), &($right)) { 288 | (left_val, right_val) => { 289 | if !(*left_val == *right_val) { 290 | use std::borrow::Cow; 291 | use $crate::serde_impl::Debug; 292 | let left_label = stringify!($left_label); 293 | let right_label = stringify!($right_label); 294 | let left_short = Some(Cow::Owned(format!("{:?}", Debug(left_val)))); 295 | let right_short = Some(Cow::Owned(format!("{:?}", Debug(right_val)))); 296 | let left_expanded = Some(Cow::Owned(format!("{:#?}", Debug(left_val)))); 297 | let right_expanded = Some(Cow::Owned(format!("{:#?}", Debug(right_val)))); 298 | let diff = $crate::SimpleDiff::__from_macro( 299 | left_short, 300 | right_short, 301 | left_expanded, 302 | right_expanded, 303 | left_label, 304 | right_label, 305 | ); 306 | diff.fail_assertion(&$hint_suffix); 307 | } 308 | } 309 | } 310 | }}; 311 | } 312 | 313 | /// Asserts that two expressions are equal to each other (using [`PartialEq`]) using [`Serialize`](serde::Serialize) for comparision. 314 | /// 315 | /// On panic, this macro will print the values of the expressions with their 316 | /// serde [`Serialize`](serde::Serialize) representations rendered in the same 317 | /// format that [`std::fmt::Debug`] would with a colorized diff of the changes in 318 | /// the debug output. 319 | /// 320 | /// Like [`assert!`], this macro has a second form, where a custom panic 321 | /// message can be provided. 322 | /// 323 | /// ```rust 324 | /// use similar_asserts::assert_serde_eq; 325 | /// assert_serde_eq!((1..3).collect::>(), vec![1, 2]); 326 | /// ``` 327 | /// 328 | /// This requires the `serde` feature. 329 | #[macro_export] 330 | #[cfg(feature = "serde")] 331 | macro_rules! assert_serde_eq { 332 | ($left_label:ident: $left:expr, $right_label:ident: $right:expr $(,)?) => ({ 333 | $crate::__assert_serde_eq!(make_serde_diff, $left_label, $left, $right_label, $right, ""); 334 | }); 335 | ($left_label:ident: $left:expr, $right_label:ident: $right:expr, $($arg:tt)*) => ({ 336 | $crate::__assert_serde_eq!(make_serde_diff, $left_label, $left, $right_label, $right, format_args!(": {}", format_args!($($arg)*))); 337 | }); 338 | ($left:expr, $right:expr $(,)?) => ({ 339 | $crate::assert_serde_eq!(left: $left, right: $right); 340 | }); 341 | ($left:expr, $right:expr, $($arg:tt)*) => ({ 342 | $crate::assert_serde_eq!(left: $left, right: $right, $($arg)*); 343 | }); 344 | } 345 | -------------------------------------------------------------------------------- /tests/test_unsized.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_unsized() { 3 | similar_asserts::assert_eq!("foo".to_string(), "bfoo"[1..]); 4 | } 5 | --------------------------------------------------------------------------------