├── .github └── workflows │ └── rust-ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── bin └── release ├── src ├── core_ext.rs ├── diff.rs └── lib.rs └── tests ├── integration_test.rs └── version-numbers.rs /.github/workflows/rust-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags: 6 | - "*" 7 | pull_request: 8 | 9 | name: CI 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | override: true 20 | 21 | # make sure all code has been formatted with rustfmt 22 | - run: rustup component add rustfmt 23 | - name: check rustfmt 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: fmt 27 | args: -- --check --color always 28 | 29 | # run clippy to verify we have no warnings 30 | - run: rustup component add clippy 31 | - name: cargo fetch 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: fetch 35 | - name: cargo clippy 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: clippy 39 | args: --lib --tests -- -D warnings 40 | 41 | test: 42 | name: Test 43 | strategy: 44 | matrix: 45 | os: [ubuntu-latest, windows-latest, macOS-latest] 46 | runs-on: ${{ matrix.os }} 47 | steps: 48 | - uses: actions/checkout@v2 49 | - uses: actions-rs/toolchain@v1 50 | with: 51 | toolchain: stable 52 | override: true 53 | - name: cargo fetch 54 | uses: actions-rs/cargo@v1 55 | with: 56 | command: fetch 57 | - name: cargo test 58 | uses: actions-rs/cargo@v1 59 | with: 60 | command: test 61 | args: --release 62 | 63 | publish-check: 64 | name: Publish Check 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v2 68 | - uses: actions-rs/toolchain@v1 69 | with: 70 | toolchain: stable 71 | override: true 72 | - name: cargo fetch 73 | uses: actions-rs/cargo@v1 74 | with: 75 | command: fetch 76 | - name: cargo publish check 77 | uses: actions-rs/cargo@v1 78 | with: 79 | command: publish 80 | args: --dry-run 81 | 82 | # Remove this job if you don't publish the crate(s) from this repo 83 | # You must add a crates.io API token to your GH secrets called CRATES_IO_TOKEN 84 | publish: 85 | name: Publish 86 | needs: [test, publish-check] 87 | runs-on: ubuntu-latest 88 | if: startsWith(github.ref, 'refs/tags/') 89 | steps: 90 | - uses: actions/checkout@v1 91 | - uses: actions-rs/toolchain@v1 92 | with: 93 | toolchain: stable 94 | override: true 95 | - name: cargo fetch 96 | uses: actions-rs/cargo@v1 97 | with: 98 | command: fetch 99 | - name: cargo publish 100 | uses: actions-rs/cargo@v1 101 | env: 102 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 103 | with: 104 | command: publish 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All user visible changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/), as described 5 | for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/text/1105-api-evolution.md) 6 | 7 | ## Unreleased 8 | 9 | - None. 10 | 11 | ### Breaking changes 12 | 13 | None. 14 | 15 | ## 2.0.2 - 2022-06-29 16 | 17 | - Don't move the `Value`s being compared in `assert_json_matches` 18 | 19 | ## 2.0.1 - 2021-02-14 20 | 21 | - Add maintenance status to readme and `Cargo.toml`. 22 | 23 | ## 2.0.0 - 2021-01-23 24 | 25 | ## Unreleased 26 | 27 | - A less strict numeric mode for comparisons is now supported. The `AssumeFloat` mode will make `1 == 1.0`. This mode can be set via `Config::numeric_mode`. 28 | - A panicking `assert_json_matches` macro has been added which takes a `Config`. 29 | - Remove dependency on "extend". 30 | 31 | ### Breaking changes 32 | 33 | - Some breaking changes have been made to support customizing how the JSON values are compared: 34 | - `assert_json_eq_no_panic` and `assert_json_include_no_panic` have been replaced by `assert_json_matches_no_panic` which takes a `Config` that describes how the comparison should work. 35 | - This setup will support adding further customizations without more breaking changes. 36 | 37 | ## 1.1.0 - 2020-07-12 38 | 39 | - All methods now accept any `T: Serialize` rather than just `serde_json::Value`. 40 | 41 | ## 1.0.3 - 2020-02-21 42 | 43 | - Introduce non-panicking functions with `assert_json_include_no_panic` and `assert_json_eq_no_panic`. 44 | 45 | ## 1.0.2 - 2020-02-19 46 | 47 | - Internal diffing algorithm simplified. There should be no external changes. Some error messages might have changed, but everything that passed/failed before should still do the same. 48 | 49 | ## 1.0.1 - 2019-10-24 50 | 51 | - Update to 2018 edition 52 | 53 | ## 1.0.0 - 2019-02-15 54 | 55 | ### Fixed 56 | 57 | - Make macros work with trailing comma 58 | 59 | ## 0.2.1 - 2018-11-15 60 | 61 | ### Fixed 62 | 63 | - Fix wrong error message when a JSON atom was missing from actual. 64 | 65 | ## 0.2.0 - 2018-11-16 66 | 67 | ### Added 68 | 69 | - Add `assert_json_include`. It does partial matching the same way the old `assert_json_eq` did. 70 | 71 | ### Changed 72 | 73 | - Change `assert_json_eq` do exact matching. If the two values are not exactly the same, it'll panic. 74 | 75 | ## 0.1.0 - 2018-10-17 76 | 77 | Initial release. 78 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | version = "2.0.2" 3 | authors = ["David Pedersen "] 4 | categories = ["development-tools"] 5 | description = "Easily compare two JSON values and get great output" 6 | homepage = "https://github.com/davidpdrsn/assert-json-diff" 7 | keywords = ["serde_json", "json", "testing"] 8 | license = "MIT" 9 | name = "assert-json-diff" 10 | readme = "README.md" 11 | repository = "https://github.com/davidpdrsn/assert-json-diff.git" 12 | documentation = "https://docs.rs/assert-json-diff" 13 | edition = "2018" 14 | 15 | [dependencies] 16 | serde_json = "1" 17 | serde = "1" 18 | 19 | [dev-dependencies] 20 | version-sync = "0.8" 21 | serde = { version = "1", features = ["derive"] } 22 | 23 | [badges] 24 | maintenance = { status = "passively-maintained" } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 David Pedersen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/assert-json-diff.svg)](https://crates.io/crates/assert-json-diff) 2 | [![Docs](https://docs.rs/assert-json-diff/badge.svg)](https://docs.rs/assert-json-diff) 3 | [![dependency status](https://deps.rs/repo/github/davidpdrsn/assert-json-diff/status.svg)](https://deps.rs/repo/github/davidpdrsn/assert-json-diff) 4 | [![Build status](https://github.com/davidpdrsn/assert-json-diff/workflows/CI/badge.svg)](https://github.com/davidpdrsn/assert-json-diff/actions) 5 | ![maintenance-status](https://img.shields.io/badge/maintenance-passively--maintained-yellowgreen.svg) 6 | 7 | # assert-json-diff 8 | 9 | This crate includes macros for comparing two serializable values by diffing their JSON 10 | representations. It is designed to give much more helpful error messages than the standard 11 | [`assert_eq!`]. It basically does a diff of the two objects and tells you the exact 12 | differences. This is useful when asserting that two large JSON objects are the same. 13 | 14 | It uses the [serde] and [serde_json] to perform the serialization. 15 | 16 | [serde]: https://crates.io/crates/serde 17 | [serde_json]: https://crates.io/crates/serde_json 18 | [`assert_eq!`]: https://doc.rust-lang.org/std/macro.assert_eq.html 19 | 20 | ### Partial matching 21 | 22 | If you want to assert that one JSON value is "included" in another use 23 | [`assert_json_include`](macro.assert_json_include.html): 24 | 25 | ```rust 26 | use assert_json_diff::assert_json_include; 27 | use serde_json::json; 28 | 29 | let a = json!({ 30 | "data": { 31 | "users": [ 32 | { 33 | "id": 1, 34 | "country": { 35 | "name": "Denmark" 36 | } 37 | }, 38 | { 39 | "id": 24, 40 | "country": { 41 | "name": "Denmark" 42 | } 43 | } 44 | ] 45 | } 46 | }); 47 | 48 | let b = json!({ 49 | "data": { 50 | "users": [ 51 | { 52 | "id": 1, 53 | "country": { 54 | "name": "Sweden" 55 | } 56 | }, 57 | { 58 | "id": 2, 59 | "country": { 60 | "name": "Denmark" 61 | } 62 | } 63 | ] 64 | } 65 | }); 66 | 67 | assert_json_include!(actual: a, expected: b) 68 | ``` 69 | 70 | This will panic with the error message: 71 | 72 | ``` 73 | json atoms at path ".data.users[0].country.name" are not equal: 74 | expected: 75 | "Sweden" 76 | actual: 77 | "Denmark" 78 | 79 | json atoms at path ".data.users[1].id" are not equal: 80 | expected: 81 | 2 82 | actual: 83 | 24 84 | ``` 85 | 86 | [`assert_json_include`](macro.assert_json_include.html) allows extra data in `actual` but not in `expected`. That is so you can verify just a part 87 | of the JSON without having to specify the whole thing. For example this test passes: 88 | 89 | ```rust 90 | use assert_json_diff::assert_json_include; 91 | use serde_json::json; 92 | 93 | assert_json_include!( 94 | actual: json!({ 95 | "a": { "b": 1 }, 96 | }), 97 | expected: json!({ 98 | "a": {}, 99 | }) 100 | ) 101 | ``` 102 | 103 | However `expected` cannot contain additional data so this test fails: 104 | 105 | ```rust 106 | use assert_json_diff::assert_json_include; 107 | use serde_json::json; 108 | 109 | assert_json_include!( 110 | actual: json!({ 111 | "a": {}, 112 | }), 113 | expected: json!({ 114 | "a": { "b": 1 }, 115 | }) 116 | ) 117 | ``` 118 | 119 | That will print 120 | 121 | ``` 122 | json atom at path ".a.b" is missing from actual 123 | ``` 124 | 125 | ### Exact matching 126 | 127 | If you want to ensure two JSON values are *exactly* the same, use [`assert_json_eq`](macro.assert_json_eq.html). 128 | 129 | ```rust 130 | use assert_json_diff::assert_json_eq; 131 | use serde_json::json; 132 | 133 | assert_json_eq!( 134 | json!({ "a": { "b": 1 } }), 135 | json!({ "a": {} }) 136 | ) 137 | ``` 138 | 139 | This will panic with the error message: 140 | 141 | ``` 142 | json atom at path ".a.b" is missing from lhs 143 | ``` 144 | 145 | ### Further customization 146 | 147 | You can use [`assert_json_matches`] to further customize the comparison. 148 | 149 | License: MIT 150 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | confirm() { 5 | while true; do 6 | read -p "$1? Please double check. y/n? " yn 7 | case $yn in 8 | [Yy]* ) break;; 9 | [Nn]* ) exit 1;; 10 | * ) echo "Please answer yes or no.";; 11 | esac 12 | done 13 | } 14 | 15 | cargo fmt --all -- --check 16 | echo "✔ code formatting looks good!" 17 | 18 | cargo check 19 | echo "✔ types look good" 20 | 21 | cargo readme > README.md 22 | echo "✔ README.md compiled" 23 | 24 | cargo test > /dev/null 25 | echo "✔ tests are passing" 26 | 27 | confirm "Updated Cargo.toml" 28 | confirm "Updated CHANGELOG.md" 29 | 30 | version="$1" 31 | version_without_v="`sed \"s/v//g\" <(echo $version)`" 32 | 33 | if (echo $version | egrep "v\d+\.\d+\.\d+" > /dev/null) 34 | then 35 | confirm "Ready to release $version (as $version_without_v)?" 36 | else 37 | echo "Invalid version number: $1" 38 | exit 1 39 | fi 40 | 41 | version_in_toml=$(cat Cargo.toml | egrep "^version = \"$version_without_v\"") 42 | 43 | if [[ "$version_in_toml" == "version = \"$version_without_v\"" ]] 44 | then 45 | true 46 | else 47 | echo "Cargo.toml isn't set to version $version_without_v" 48 | fi 49 | 50 | GIT_COMMITTER_DATE=$(git log -n1 --pretty=%aD) git tag -a -m "Release $version" $version 51 | git push --tags 52 | 53 | cargo publish --dry-run 54 | cargo publish || true 55 | -------------------------------------------------------------------------------- /src/core_ext.rs: -------------------------------------------------------------------------------- 1 | pub trait Indent { 2 | fn indent(&self, level: u32) -> String; 3 | } 4 | 5 | impl Indent for T 6 | where 7 | T: ToString, 8 | { 9 | fn indent(&self, level: u32) -> String { 10 | let mut indent = String::new(); 11 | for _ in 0..level { 12 | indent.push(' '); 13 | } 14 | 15 | self.to_string() 16 | .lines() 17 | .map(|line| format!("{}{}", indent, line)) 18 | .collect::>() 19 | .join("\n") 20 | } 21 | } 22 | 23 | pub trait Indexes { 24 | fn indexes(&self) -> Vec; 25 | } 26 | 27 | impl Indexes for Vec { 28 | fn indexes(&self) -> Vec { 29 | if self.is_empty() { 30 | vec![] 31 | } else { 32 | (0..=self.len() - 1).collect() 33 | } 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | #[test] 42 | fn test_indent() { 43 | assert_eq!(" foo", "foo".indent(2)); 44 | assert_eq!(" foo\n bar", "foo\nbar".indent(2)); 45 | } 46 | 47 | #[test] 48 | fn test_indexes() { 49 | let empty: Vec = vec![]; 50 | let empty_indexes: Vec = vec![]; 51 | assert_eq!(empty.indexes(), empty_indexes); 52 | 53 | assert_eq!(vec!['a', 'b'].indexes(), vec![0, 1]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/diff.rs: -------------------------------------------------------------------------------- 1 | use crate::core_ext::{Indent, Indexes}; 2 | use crate::{CompareMode, Config, NumericMode}; 3 | use serde_json::Value; 4 | use std::{collections::HashSet, fmt}; 5 | 6 | pub(crate) fn diff<'a>(lhs: &'a Value, rhs: &'a Value, config: Config) -> Vec> { 7 | let mut acc = vec![]; 8 | diff_with(lhs, rhs, config, Path::Root, &mut acc); 9 | acc 10 | } 11 | 12 | fn diff_with<'a>( 13 | lhs: &'a Value, 14 | rhs: &'a Value, 15 | config: Config, 16 | path: Path<'a>, 17 | acc: &mut Vec>, 18 | ) { 19 | let mut folder = DiffFolder { 20 | rhs, 21 | path, 22 | acc, 23 | config, 24 | }; 25 | 26 | fold_json(lhs, &mut folder); 27 | } 28 | 29 | #[derive(Debug)] 30 | struct DiffFolder<'a, 'b> { 31 | rhs: &'a Value, 32 | path: Path<'a>, 33 | acc: &'b mut Vec>, 34 | config: Config, 35 | } 36 | 37 | macro_rules! direct_compare { 38 | ($name:ident) => { 39 | fn $name(&mut self, lhs: &'a Value) { 40 | if self.rhs != lhs { 41 | self.acc.push(Difference { 42 | lhs: Some(lhs), 43 | rhs: Some(&self.rhs), 44 | path: self.path.clone(), 45 | config: self.config.clone(), 46 | }); 47 | } 48 | } 49 | }; 50 | } 51 | 52 | impl<'a, 'b> DiffFolder<'a, 'b> { 53 | direct_compare!(on_null); 54 | direct_compare!(on_bool); 55 | direct_compare!(on_string); 56 | 57 | fn on_number(&mut self, lhs: &'a Value) { 58 | let is_equal = match self.config.numeric_mode { 59 | NumericMode::Strict => self.rhs == lhs, 60 | NumericMode::AssumeFloat => self.rhs.as_f64() == lhs.as_f64(), 61 | }; 62 | if !is_equal { 63 | self.acc.push(Difference { 64 | lhs: Some(lhs), 65 | rhs: Some(self.rhs), 66 | path: self.path.clone(), 67 | config: self.config.clone(), 68 | }); 69 | } 70 | } 71 | 72 | fn on_array(&mut self, lhs: &'a Value) { 73 | if let Some(rhs) = self.rhs.as_array() { 74 | let lhs = lhs.as_array().unwrap(); 75 | 76 | match self.config.compare_mode { 77 | CompareMode::Inclusive => { 78 | for (idx, rhs) in rhs.iter().enumerate() { 79 | let path = self.path.append(Key::Idx(idx)); 80 | 81 | if let Some(lhs) = lhs.get(idx) { 82 | diff_with(lhs, rhs, self.config.clone(), path, self.acc) 83 | } else { 84 | self.acc.push(Difference { 85 | lhs: None, 86 | rhs: Some(self.rhs), 87 | path, 88 | config: self.config.clone(), 89 | }); 90 | } 91 | } 92 | } 93 | CompareMode::Strict => { 94 | let all_keys = rhs 95 | .indexes() 96 | .into_iter() 97 | .chain(lhs.indexes()) 98 | .collect::>(); 99 | for key in all_keys { 100 | let path = self.path.append(Key::Idx(key)); 101 | 102 | match (lhs.get(key), rhs.get(key)) { 103 | (Some(lhs), Some(rhs)) => { 104 | diff_with(lhs, rhs, self.config.clone(), path, self.acc); 105 | } 106 | (None, Some(rhs)) => { 107 | self.acc.push(Difference { 108 | lhs: None, 109 | rhs: Some(rhs), 110 | path, 111 | config: self.config.clone(), 112 | }); 113 | } 114 | (Some(lhs), None) => { 115 | self.acc.push(Difference { 116 | lhs: Some(lhs), 117 | rhs: None, 118 | path, 119 | config: self.config.clone(), 120 | }); 121 | } 122 | (None, None) => { 123 | unreachable!("at least one of the maps should have the key") 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } else { 130 | self.acc.push(Difference { 131 | lhs: Some(lhs), 132 | rhs: Some(self.rhs), 133 | path: self.path.clone(), 134 | config: self.config.clone(), 135 | }); 136 | } 137 | } 138 | 139 | fn on_object(&mut self, lhs: &'a Value) { 140 | if let Some(rhs) = self.rhs.as_object() { 141 | let lhs = lhs.as_object().unwrap(); 142 | 143 | match self.config.compare_mode { 144 | CompareMode::Inclusive => { 145 | for (key, rhs) in rhs.iter() { 146 | let path = self.path.append(Key::Field(key)); 147 | 148 | if let Some(lhs) = lhs.get(key) { 149 | diff_with(lhs, rhs, self.config.clone(), path, self.acc) 150 | } else { 151 | self.acc.push(Difference { 152 | lhs: None, 153 | rhs: Some(self.rhs), 154 | path, 155 | config: self.config.clone(), 156 | }); 157 | } 158 | } 159 | } 160 | CompareMode::Strict => { 161 | let all_keys = rhs.keys().chain(lhs.keys()).collect::>(); 162 | for key in all_keys { 163 | let path = self.path.append(Key::Field(key)); 164 | 165 | match (lhs.get(key), rhs.get(key)) { 166 | (Some(lhs), Some(rhs)) => { 167 | diff_with(lhs, rhs, self.config.clone(), path, self.acc); 168 | } 169 | (None, Some(rhs)) => { 170 | self.acc.push(Difference { 171 | lhs: None, 172 | rhs: Some(rhs), 173 | path, 174 | config: self.config.clone(), 175 | }); 176 | } 177 | (Some(lhs), None) => { 178 | self.acc.push(Difference { 179 | lhs: Some(lhs), 180 | rhs: None, 181 | path, 182 | config: self.config.clone(), 183 | }); 184 | } 185 | (None, None) => { 186 | unreachable!("at least one of the maps should have the key") 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } else { 193 | self.acc.push(Difference { 194 | lhs: Some(lhs), 195 | rhs: Some(self.rhs), 196 | path: self.path.clone(), 197 | config: self.config.clone(), 198 | }); 199 | } 200 | } 201 | } 202 | 203 | #[derive(Debug, PartialEq)] 204 | pub(crate) struct Difference<'a> { 205 | path: Path<'a>, 206 | lhs: Option<&'a Value>, 207 | rhs: Option<&'a Value>, 208 | config: Config, 209 | } 210 | 211 | impl<'a> fmt::Display for Difference<'a> { 212 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 213 | let json_to_string = |json: &Value| serde_json::to_string_pretty(json).unwrap(); 214 | 215 | match (&self.config.compare_mode, &self.lhs, &self.rhs) { 216 | (CompareMode::Inclusive, Some(actual), Some(expected)) => { 217 | writeln!(f, "json atoms at path \"{}\" are not equal:", self.path)?; 218 | writeln!(f, " expected:")?; 219 | writeln!(f, "{}", json_to_string(expected).indent(8))?; 220 | writeln!(f, " actual:")?; 221 | write!(f, "{}", json_to_string(actual).indent(8))?; 222 | } 223 | (CompareMode::Inclusive, None, Some(_expected)) => { 224 | write!( 225 | f, 226 | "json atom at path \"{}\" is missing from actual", 227 | self.path 228 | )?; 229 | } 230 | (CompareMode::Inclusive, Some(_actual), None) => { 231 | unreachable!("stuff missing actual wont produce an error") 232 | } 233 | (CompareMode::Inclusive, None, None) => unreachable!("can't both be missing"), 234 | 235 | (CompareMode::Strict, Some(lhs), Some(rhs)) => { 236 | writeln!(f, "json atoms at path \"{}\" are not equal:", self.path)?; 237 | writeln!(f, " lhs:")?; 238 | writeln!(f, "{}", json_to_string(lhs).indent(8))?; 239 | writeln!(f, " rhs:")?; 240 | write!(f, "{}", json_to_string(rhs).indent(8))?; 241 | } 242 | (CompareMode::Strict, None, Some(_)) => { 243 | write!(f, "json atom at path \"{}\" is missing from lhs", self.path)?; 244 | } 245 | (CompareMode::Strict, Some(_), None) => { 246 | write!(f, "json atom at path \"{}\" is missing from rhs", self.path)?; 247 | } 248 | (CompareMode::Strict, None, None) => unreachable!("can't both be missing"), 249 | } 250 | 251 | Ok(()) 252 | } 253 | } 254 | 255 | #[derive(Debug, Clone, PartialEq)] 256 | enum Path<'a> { 257 | Root, 258 | Keys(Vec>), 259 | } 260 | 261 | impl<'a> Path<'a> { 262 | fn append(&self, next: Key<'a>) -> Path<'a> { 263 | match self { 264 | Path::Root => Path::Keys(vec![next]), 265 | Path::Keys(list) => { 266 | let mut copy = list.clone(); 267 | copy.push(next); 268 | Path::Keys(copy) 269 | } 270 | } 271 | } 272 | } 273 | 274 | impl<'a> fmt::Display for Path<'a> { 275 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 276 | match self { 277 | Path::Root => write!(f, "(root)"), 278 | Path::Keys(keys) => { 279 | for key in keys { 280 | write!(f, "{}", key)?; 281 | } 282 | Ok(()) 283 | } 284 | } 285 | } 286 | } 287 | 288 | #[derive(Debug, Copy, Clone, PartialEq)] 289 | enum Key<'a> { 290 | Idx(usize), 291 | Field(&'a str), 292 | } 293 | 294 | impl<'a> fmt::Display for Key<'a> { 295 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 296 | match self { 297 | Key::Idx(idx) => write!(f, "[{}]", idx), 298 | Key::Field(key) => write!(f, ".{}", key), 299 | } 300 | } 301 | } 302 | 303 | fn fold_json<'a>(json: &'a Value, folder: &mut DiffFolder<'a, '_>) { 304 | match json { 305 | Value::Null => folder.on_null(json), 306 | Value::Bool(_) => folder.on_bool(json), 307 | Value::Number(_) => folder.on_number(json), 308 | Value::String(_) => folder.on_string(json), 309 | Value::Array(_) => folder.on_array(json), 310 | Value::Object(_) => folder.on_object(json), 311 | } 312 | } 313 | 314 | #[cfg(test)] 315 | mod test { 316 | #[allow(unused_imports)] 317 | use super::*; 318 | use serde_json::json; 319 | 320 | #[test] 321 | fn test_diffing_leaf_json() { 322 | let diffs = diff( 323 | &json!(null), 324 | &json!(null), 325 | Config::new(CompareMode::Inclusive), 326 | ); 327 | assert_eq!(diffs, vec![]); 328 | 329 | let diffs = diff( 330 | &json!(false), 331 | &json!(false), 332 | Config::new(CompareMode::Inclusive), 333 | ); 334 | assert_eq!(diffs, vec![]); 335 | 336 | let diffs = diff( 337 | &json!(true), 338 | &json!(true), 339 | Config::new(CompareMode::Inclusive), 340 | ); 341 | assert_eq!(diffs, vec![]); 342 | 343 | let diffs = diff( 344 | &json!(false), 345 | &json!(true), 346 | Config::new(CompareMode::Inclusive), 347 | ); 348 | assert_eq!(diffs.len(), 1); 349 | 350 | let diffs = diff( 351 | &json!(true), 352 | &json!(false), 353 | Config::new(CompareMode::Inclusive), 354 | ); 355 | assert_eq!(diffs.len(), 1); 356 | 357 | let actual = json!(1); 358 | let expected = json!(1); 359 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 360 | assert_eq!(diffs, vec![]); 361 | 362 | let actual = json!(2); 363 | let expected = json!(1); 364 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 365 | assert_eq!(diffs.len(), 1); 366 | 367 | let actual = json!(1); 368 | let expected = json!(2); 369 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 370 | assert_eq!(diffs.len(), 1); 371 | 372 | let actual = json!(1.0); 373 | let expected = json!(1.0); 374 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 375 | assert_eq!(diffs, vec![]); 376 | 377 | let actual = json!(1); 378 | let expected = json!(1.0); 379 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 380 | assert_eq!(diffs.len(), 1); 381 | 382 | let actual = json!(1.0); 383 | let expected = json!(1); 384 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 385 | assert_eq!(diffs.len(), 1); 386 | 387 | let actual = json!(1); 388 | let expected = json!(1.0); 389 | let diffs = diff( 390 | &actual, 391 | &expected, 392 | Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat), 393 | ); 394 | assert_eq!(diffs, vec![]); 395 | 396 | let actual = json!(1.0); 397 | let expected = json!(1); 398 | let diffs = diff( 399 | &actual, 400 | &expected, 401 | Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat), 402 | ); 403 | assert_eq!(diffs, vec![]); 404 | } 405 | 406 | #[test] 407 | fn test_diffing_array() { 408 | // empty 409 | let actual = json!([]); 410 | let expected = json!([]); 411 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 412 | assert_eq!(diffs, vec![]); 413 | 414 | let actual = json!([1]); 415 | let expected = json!([]); 416 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 417 | assert_eq!(diffs.len(), 0); 418 | 419 | let actual = json!([]); 420 | let expected = json!([1]); 421 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 422 | assert_eq!(diffs.len(), 1); 423 | 424 | // eq 425 | let actual = json!([1]); 426 | let expected = json!([1]); 427 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 428 | assert_eq!(diffs, vec![]); 429 | 430 | // actual longer 431 | let actual = json!([1, 2]); 432 | let expected = json!([1]); 433 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 434 | assert_eq!(diffs, vec![]); 435 | 436 | // expected longer 437 | let actual = json!([1]); 438 | let expected = json!([1, 2]); 439 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 440 | assert_eq!(diffs.len(), 1); 441 | 442 | // eq length but different 443 | let actual = json!([1, 3]); 444 | let expected = json!([1, 2]); 445 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 446 | assert_eq!(diffs.len(), 1); 447 | 448 | // different types 449 | let actual = json!(1); 450 | let expected = json!([1]); 451 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 452 | assert_eq!(diffs.len(), 1); 453 | 454 | let actual = json!([1]); 455 | let expected = json!(1); 456 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 457 | assert_eq!(diffs.len(), 1); 458 | } 459 | 460 | #[test] 461 | fn test_array_strict() { 462 | let actual = json!([]); 463 | let expected = json!([]); 464 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); 465 | assert_eq!(diffs.len(), 0); 466 | 467 | let actual = json!([1, 2]); 468 | let expected = json!([1, 2]); 469 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); 470 | assert_eq!(diffs.len(), 0); 471 | 472 | let actual = json!([1]); 473 | let expected = json!([1, 2]); 474 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); 475 | assert_eq!(diffs.len(), 1); 476 | 477 | let actual = json!([1, 2]); 478 | let expected = json!([1]); 479 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Strict)); 480 | assert_eq!(diffs.len(), 1); 481 | } 482 | 483 | #[test] 484 | fn test_object() { 485 | let actual = json!({}); 486 | let expected = json!({}); 487 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 488 | assert_eq!(diffs, vec![]); 489 | 490 | let actual = json!({ "a": 1 }); 491 | let expected = json!({ "a": 1 }); 492 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 493 | assert_eq!(diffs, vec![]); 494 | 495 | let actual = json!({ "a": 1, "b": 123 }); 496 | let expected = json!({ "a": 1 }); 497 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 498 | assert_eq!(diffs, vec![]); 499 | 500 | let actual = json!({ "a": 1 }); 501 | let expected = json!({ "b": 1 }); 502 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 503 | assert_eq!(diffs.len(), 1); 504 | 505 | let actual = json!({ "a": 1 }); 506 | let expected = json!({ "a": 2 }); 507 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 508 | assert_eq!(diffs.len(), 1); 509 | 510 | let actual = json!({ "a": { "b": true } }); 511 | let expected = json!({ "a": {} }); 512 | let diffs = diff(&actual, &expected, Config::new(CompareMode::Inclusive)); 513 | assert_eq!(diffs, vec![]); 514 | } 515 | 516 | #[test] 517 | fn test_object_strict() { 518 | let lhs = json!({}); 519 | let rhs = json!({ "a": 1 }); 520 | let diffs = diff(&lhs, &rhs, Config::new(CompareMode::Strict)); 521 | assert_eq!(diffs.len(), 1); 522 | 523 | let lhs = json!({ "a": 1 }); 524 | let rhs = json!({}); 525 | let diffs = diff(&lhs, &rhs, Config::new(CompareMode::Strict)); 526 | assert_eq!(diffs.len(), 1); 527 | 528 | let json = json!({ "a": 1 }); 529 | let diffs = diff(&json, &json, Config::new(CompareMode::Strict)); 530 | assert_eq!(diffs, vec![]); 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate includes macros for comparing two serializable values by diffing their JSON 2 | //! representations. It is designed to give much more helpful error messages than the standard 3 | //! [`assert_eq!`]. It basically does a diff of the two objects and tells you the exact 4 | //! differences. This is useful when asserting that two large JSON objects are the same. 5 | //! 6 | //! It uses the [serde] and [serde_json] to perform the serialization. 7 | //! 8 | //! [serde]: https://crates.io/crates/serde 9 | //! [serde_json]: https://crates.io/crates/serde_json 10 | //! [`assert_eq!`]: https://doc.rust-lang.org/std/macro.assert_eq.html 11 | //! 12 | //! ## Partial matching 13 | //! 14 | //! If you want to assert that one JSON value is "included" in another use 15 | //! [`assert_json_include`](macro.assert_json_include.html): 16 | //! 17 | //! ```should_panic 18 | //! use assert_json_diff::assert_json_include; 19 | //! use serde_json::json; 20 | //! 21 | //! let a = json!({ 22 | //! "data": { 23 | //! "users": [ 24 | //! { 25 | //! "id": 1, 26 | //! "country": { 27 | //! "name": "Denmark" 28 | //! } 29 | //! }, 30 | //! { 31 | //! "id": 24, 32 | //! "country": { 33 | //! "name": "Denmark" 34 | //! } 35 | //! } 36 | //! ] 37 | //! } 38 | //! }); 39 | //! 40 | //! let b = json!({ 41 | //! "data": { 42 | //! "users": [ 43 | //! { 44 | //! "id": 1, 45 | //! "country": { 46 | //! "name": "Sweden" 47 | //! } 48 | //! }, 49 | //! { 50 | //! "id": 2, 51 | //! "country": { 52 | //! "name": "Denmark" 53 | //! } 54 | //! } 55 | //! ] 56 | //! } 57 | //! }); 58 | //! 59 | //! assert_json_include!(actual: a, expected: b) 60 | //! ``` 61 | //! 62 | //! This will panic with the error message: 63 | //! 64 | //! ```text 65 | //! json atoms at path ".data.users[0].country.name" are not equal: 66 | //! expected: 67 | //! "Sweden" 68 | //! actual: 69 | //! "Denmark" 70 | //! 71 | //! json atoms at path ".data.users[1].id" are not equal: 72 | //! expected: 73 | //! 2 74 | //! actual: 75 | //! 24 76 | //! ``` 77 | //! 78 | //! [`assert_json_include`](macro.assert_json_include.html) allows extra data in `actual` but not in `expected`. That is so you can verify just a part 79 | //! of the JSON without having to specify the whole thing. For example this test passes: 80 | //! 81 | //! ``` 82 | //! use assert_json_diff::assert_json_include; 83 | //! use serde_json::json; 84 | //! 85 | //! assert_json_include!( 86 | //! actual: json!({ 87 | //! "a": { "b": 1 }, 88 | //! }), 89 | //! expected: json!({ 90 | //! "a": {}, 91 | //! }) 92 | //! ) 93 | //! ``` 94 | //! 95 | //! However `expected` cannot contain additional data so this test fails: 96 | //! 97 | //! ```should_panic 98 | //! use assert_json_diff::assert_json_include; 99 | //! use serde_json::json; 100 | //! 101 | //! assert_json_include!( 102 | //! actual: json!({ 103 | //! "a": {}, 104 | //! }), 105 | //! expected: json!({ 106 | //! "a": { "b": 1 }, 107 | //! }) 108 | //! ) 109 | //! ``` 110 | //! 111 | //! That will print 112 | //! 113 | //! ```text 114 | //! json atom at path ".a.b" is missing from actual 115 | //! ``` 116 | //! 117 | //! ## Exact matching 118 | //! 119 | //! If you want to ensure two JSON values are *exactly* the same, use [`assert_json_eq`](macro.assert_json_eq.html). 120 | //! 121 | //! ```rust,should_panic 122 | //! use assert_json_diff::assert_json_eq; 123 | //! use serde_json::json; 124 | //! 125 | //! assert_json_eq!( 126 | //! json!({ "a": { "b": 1 } }), 127 | //! json!({ "a": {} }) 128 | //! ) 129 | //! ``` 130 | //! 131 | //! This will panic with the error message: 132 | //! 133 | //! ```text 134 | //! json atom at path ".a.b" is missing from lhs 135 | //! ``` 136 | //! 137 | //! ## Further customization 138 | //! 139 | //! You can use [`assert_json_matches`] to further customize the comparison. 140 | 141 | #![deny( 142 | missing_docs, 143 | unused_imports, 144 | missing_debug_implementations, 145 | missing_copy_implementations, 146 | trivial_casts, 147 | trivial_numeric_casts, 148 | unsafe_code, 149 | unstable_features, 150 | unused_import_braces, 151 | unused_qualifications, 152 | unknown_lints 153 | )] 154 | 155 | use diff::diff; 156 | use serde::Serialize; 157 | 158 | mod core_ext; 159 | mod diff; 160 | 161 | /// Compare two JSON values for an inclusive match. 162 | /// 163 | /// It allows `actual` to contain additional data. If you want an exact match use 164 | /// [`assert_json_eq`](macro.assert_json_eq.html) instead. 165 | /// 166 | /// See [crate documentation](index.html) for examples. 167 | #[macro_export] 168 | macro_rules! assert_json_include { 169 | (actual: $actual:expr, expected: $expected:expr $(,)?) => {{ 170 | $crate::assert_json_matches!( 171 | $actual, 172 | $expected, 173 | $crate::Config::new($crate::CompareMode::Inclusive) 174 | ) 175 | }}; 176 | (expected: $expected:expr, actual: $actual:expr $(,)?) => {{ 177 | $crate::assert_json_include!(actual: $actual, expected: $expected) 178 | }}; 179 | } 180 | 181 | /// Compare two JSON values for an exact match. 182 | /// 183 | /// If you want an inclusive match use [`assert_json_include`](macro.assert_json_include.html) instead. 184 | /// 185 | /// See [crate documentation](index.html) for examples. 186 | #[macro_export] 187 | macro_rules! assert_json_eq { 188 | ($lhs:expr, $rhs:expr $(,)?) => {{ 189 | $crate::assert_json_matches!($lhs, $rhs, $crate::Config::new($crate::CompareMode::Strict)) 190 | }}; 191 | } 192 | 193 | /// Compare two JSON values according to a configuration. 194 | /// 195 | /// ``` 196 | /// use assert_json_diff::{ 197 | /// CompareMode, 198 | /// Config, 199 | /// NumericMode, 200 | /// assert_json_matches, 201 | /// }; 202 | /// use serde_json::json; 203 | /// 204 | /// let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat); 205 | /// 206 | /// assert_json_matches!( 207 | /// json!({ 208 | /// "a": { "b": [1, 2, 3.0] }, 209 | /// }), 210 | /// json!({ 211 | /// "a": { "b": [1, 2.0, 3] }, 212 | /// }), 213 | /// config, 214 | /// ) 215 | /// ``` 216 | /// 217 | /// When using `CompareMode::Inclusive` the first argument is `actual` and the second argument is 218 | /// `expected`. Example: 219 | /// 220 | /// ``` 221 | /// # use assert_json_diff::{ 222 | /// # CompareMode, 223 | /// # Config, 224 | /// # NumericMode, 225 | /// # assert_json_matches, 226 | /// # assert_json_include, 227 | /// # }; 228 | /// # use serde_json::json; 229 | /// # 230 | /// // This 231 | /// assert_json_matches!( 232 | /// json!({ 233 | /// "a": { "b": 1 }, 234 | /// }), 235 | /// json!({ 236 | /// "a": {}, 237 | /// }), 238 | /// Config::new(CompareMode::Inclusive), 239 | /// ); 240 | /// 241 | /// // Is the same as this 242 | /// assert_json_include!( 243 | /// actual: json!({ 244 | /// "a": { "b": 1 }, 245 | /// }), 246 | /// expected: json!({ 247 | /// "a": {}, 248 | /// }), 249 | /// ); 250 | /// ``` 251 | #[macro_export] 252 | macro_rules! assert_json_matches { 253 | ($lhs:expr, $rhs:expr, $config:expr $(,)?) => {{ 254 | if let Err(error) = $crate::assert_json_matches_no_panic(&$lhs, &$rhs, $config) { 255 | panic!("\n\n{}\n\n", error); 256 | } 257 | }}; 258 | } 259 | 260 | /// Compares two JSON values without panicking. 261 | /// 262 | /// Instead it returns a `Result` where the error is the message that would be passed to `panic!`. 263 | /// This is might be useful if you want to control how failures are reported and don't want to deal 264 | /// with panics. 265 | pub fn assert_json_matches_no_panic( 266 | lhs: &Lhs, 267 | rhs: &Rhs, 268 | config: Config, 269 | ) -> Result<(), String> 270 | where 271 | Lhs: Serialize, 272 | Rhs: Serialize, 273 | { 274 | let lhs = serde_json::to_value(lhs).unwrap_or_else(|err| { 275 | panic!( 276 | "Couldn't convert left hand side value to JSON. Serde error: {}", 277 | err 278 | ) 279 | }); 280 | let rhs = serde_json::to_value(rhs).unwrap_or_else(|err| { 281 | panic!( 282 | "Couldn't convert right hand side value to JSON. Serde error: {}", 283 | err 284 | ) 285 | }); 286 | 287 | let diffs = diff(&lhs, &rhs, config); 288 | 289 | if diffs.is_empty() { 290 | Ok(()) 291 | } else { 292 | let msg = diffs 293 | .into_iter() 294 | .map(|d| d.to_string()) 295 | .collect::>() 296 | .join("\n\n"); 297 | Err(msg) 298 | } 299 | } 300 | 301 | /// Configuration for how JSON values should be compared. 302 | #[derive(Debug, Clone, PartialEq, Eq)] 303 | #[allow(missing_copy_implementations)] 304 | pub struct Config { 305 | pub(crate) compare_mode: CompareMode, 306 | pub(crate) numeric_mode: NumericMode, 307 | } 308 | 309 | impl Config { 310 | /// Create a new [`Config`] using the given [`CompareMode`]. 311 | /// 312 | /// The default `numeric_mode` is be [`NumericMode::Strict`]. 313 | pub fn new(compare_mode: CompareMode) -> Self { 314 | Self { 315 | compare_mode, 316 | numeric_mode: NumericMode::Strict, 317 | } 318 | } 319 | 320 | /// Change the config's numeric mode. 321 | /// 322 | /// The default `numeric_mode` is be [`NumericMode::Strict`]. 323 | pub fn numeric_mode(mut self, numeric_mode: NumericMode) -> Self { 324 | self.numeric_mode = numeric_mode; 325 | self 326 | } 327 | 328 | /// Change the config's compare mode. 329 | pub fn compare_mode(mut self, compare_mode: CompareMode) -> Self { 330 | self.compare_mode = compare_mode; 331 | self 332 | } 333 | } 334 | 335 | /// Mode for how JSON values should be compared. 336 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 337 | pub enum CompareMode { 338 | /// The two JSON values don't have to be exactly equal. The "actual" value is only required to 339 | /// be "contained" inside "expected". See [crate documentation](index.html) for examples. 340 | /// 341 | /// The mode used with [`assert_json_include`]. 342 | Inclusive, 343 | /// The two JSON values must be exactly equal. 344 | /// 345 | /// The mode used with [`assert_json_eq`]. 346 | Strict, 347 | } 348 | 349 | /// How should numbers be compared. 350 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 351 | pub enum NumericMode { 352 | /// Different numeric types aren't considered equal. 353 | Strict, 354 | /// All numeric types are converted to float before comparison. 355 | AssumeFloat, 356 | } 357 | 358 | #[cfg(test)] 359 | mod tests { 360 | use super::*; 361 | use serde_json::{json, Value}; 362 | use std::fmt::Write; 363 | 364 | #[test] 365 | fn boolean_root() { 366 | let result = test_partial_match(json!(true), json!(true)); 367 | assert_output_eq(result, Ok(())); 368 | 369 | let result = test_partial_match(json!(false), json!(false)); 370 | assert_output_eq(result, Ok(())); 371 | 372 | let result = test_partial_match(json!(false), json!(true)); 373 | assert_output_eq( 374 | result, 375 | Err(r#"json atoms at path "(root)" are not equal: 376 | expected: 377 | true 378 | actual: 379 | false"#), 380 | ); 381 | 382 | let result = test_partial_match(json!(true), json!(false)); 383 | assert_output_eq( 384 | result, 385 | Err(r#"json atoms at path "(root)" are not equal: 386 | expected: 387 | false 388 | actual: 389 | true"#), 390 | ); 391 | } 392 | 393 | #[test] 394 | fn string_root() { 395 | let result = test_partial_match(json!("true"), json!("true")); 396 | assert_output_eq(result, Ok(())); 397 | 398 | let result = test_partial_match(json!("false"), json!("false")); 399 | assert_output_eq(result, Ok(())); 400 | 401 | let result = test_partial_match(json!("false"), json!("true")); 402 | assert_output_eq( 403 | result, 404 | Err(r#"json atoms at path "(root)" are not equal: 405 | expected: 406 | "true" 407 | actual: 408 | "false""#), 409 | ); 410 | 411 | let result = test_partial_match(json!("true"), json!("false")); 412 | assert_output_eq( 413 | result, 414 | Err(r#"json atoms at path "(root)" are not equal: 415 | expected: 416 | "false" 417 | actual: 418 | "true""#), 419 | ); 420 | } 421 | 422 | #[test] 423 | fn number_root() { 424 | let result = test_partial_match(json!(1), json!(1)); 425 | assert_output_eq(result, Ok(())); 426 | 427 | let result = test_partial_match(json!(0), json!(0)); 428 | assert_output_eq(result, Ok(())); 429 | 430 | let result = test_partial_match(json!(0), json!(1)); 431 | assert_output_eq( 432 | result, 433 | Err(r#"json atoms at path "(root)" are not equal: 434 | expected: 435 | 1 436 | actual: 437 | 0"#), 438 | ); 439 | 440 | let result = test_partial_match(json!(1), json!(0)); 441 | assert_output_eq( 442 | result, 443 | Err(r#"json atoms at path "(root)" are not equal: 444 | expected: 445 | 0 446 | actual: 447 | 1"#), 448 | ); 449 | } 450 | 451 | #[test] 452 | fn null_root() { 453 | let result = test_partial_match(json!(null), json!(null)); 454 | assert_output_eq(result, Ok(())); 455 | 456 | let result = test_partial_match(json!(null), json!(1)); 457 | assert_output_eq( 458 | result, 459 | Err(r#"json atoms at path "(root)" are not equal: 460 | expected: 461 | 1 462 | actual: 463 | null"#), 464 | ); 465 | 466 | let result = test_partial_match(json!(1), json!(null)); 467 | assert_output_eq( 468 | result, 469 | Err(r#"json atoms at path "(root)" are not equal: 470 | expected: 471 | null 472 | actual: 473 | 1"#), 474 | ); 475 | } 476 | 477 | #[test] 478 | fn into_object() { 479 | let result = test_partial_match(json!({ "a": true }), json!({ "a": true })); 480 | assert_output_eq(result, Ok(())); 481 | 482 | let result = test_partial_match(json!({ "a": false }), json!({ "a": true })); 483 | assert_output_eq( 484 | result, 485 | Err(r#"json atoms at path ".a" are not equal: 486 | expected: 487 | true 488 | actual: 489 | false"#), 490 | ); 491 | 492 | let result = 493 | test_partial_match(json!({ "a": { "b": true } }), json!({ "a": { "b": true } })); 494 | assert_output_eq(result, Ok(())); 495 | 496 | let result = test_partial_match(json!({ "a": true }), json!({ "a": { "b": true } })); 497 | assert_output_eq( 498 | result, 499 | Err(r#"json atoms at path ".a" are not equal: 500 | expected: 501 | { 502 | "b": true 503 | } 504 | actual: 505 | true"#), 506 | ); 507 | 508 | let result = test_partial_match(json!({}), json!({ "a": true })); 509 | assert_output_eq( 510 | result, 511 | Err(r#"json atom at path ".a" is missing from actual"#), 512 | ); 513 | 514 | let result = test_partial_match(json!({ "a": { "b": true } }), json!({ "a": true })); 515 | assert_output_eq( 516 | result, 517 | Err(r#"json atoms at path ".a" are not equal: 518 | expected: 519 | true 520 | actual: 521 | { 522 | "b": true 523 | }"#), 524 | ); 525 | } 526 | 527 | #[test] 528 | fn into_array() { 529 | let result = test_partial_match(json!([1]), json!([1])); 530 | assert_output_eq(result, Ok(())); 531 | 532 | let result = test_partial_match(json!([2]), json!([1])); 533 | assert_output_eq( 534 | result, 535 | Err(r#"json atoms at path "[0]" are not equal: 536 | expected: 537 | 1 538 | actual: 539 | 2"#), 540 | ); 541 | 542 | let result = test_partial_match(json!([1, 2, 4]), json!([1, 2, 3])); 543 | assert_output_eq( 544 | result, 545 | Err(r#"json atoms at path "[2]" are not equal: 546 | expected: 547 | 3 548 | actual: 549 | 4"#), 550 | ); 551 | 552 | let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2, 4]})); 553 | assert_output_eq( 554 | result, 555 | Err(r#"json atoms at path ".a[2]" are not equal: 556 | expected: 557 | 4 558 | actual: 559 | 3"#), 560 | ); 561 | 562 | let result = test_partial_match(json!({ "a": [1, 2, 3]}), json!({ "a": [1, 2]})); 563 | assert_output_eq(result, Ok(())); 564 | 565 | let result = test_partial_match(json!({ "a": [1, 2]}), json!({ "a": [1, 2, 3]})); 566 | assert_output_eq( 567 | result, 568 | Err(r#"json atom at path ".a[2]" is missing from actual"#), 569 | ); 570 | } 571 | 572 | #[test] 573 | fn exact_matching() { 574 | let result = test_exact_match(json!(true), json!(true)); 575 | assert_output_eq(result, Ok(())); 576 | 577 | let result = test_exact_match(json!("s"), json!("s")); 578 | assert_output_eq(result, Ok(())); 579 | 580 | let result = test_exact_match(json!("a"), json!("b")); 581 | assert_output_eq( 582 | result, 583 | Err(r#"json atoms at path "(root)" are not equal: 584 | lhs: 585 | "a" 586 | rhs: 587 | "b""#), 588 | ); 589 | 590 | let result = test_exact_match( 591 | json!({ "a": [1, { "b": 2 }] }), 592 | json!({ "a": [1, { "b": 3 }] }), 593 | ); 594 | assert_output_eq( 595 | result, 596 | Err(r#"json atoms at path ".a[1].b" are not equal: 597 | lhs: 598 | 2 599 | rhs: 600 | 3"#), 601 | ); 602 | } 603 | 604 | #[test] 605 | fn exact_match_output_message() { 606 | let result = test_exact_match(json!({ "a": { "b": 1 } }), json!({ "a": {} })); 607 | assert_output_eq( 608 | result, 609 | Err(r#"json atom at path ".a.b" is missing from rhs"#), 610 | ); 611 | 612 | let result = test_exact_match(json!({ "a": {} }), json!({ "a": { "b": 1 } })); 613 | assert_output_eq( 614 | result, 615 | Err(r#"json atom at path ".a.b" is missing from lhs"#), 616 | ); 617 | } 618 | 619 | fn assert_output_eq(actual: Result<(), String>, expected: Result<(), &str>) { 620 | match (actual, expected) { 621 | (Ok(()), Ok(())) => {} 622 | 623 | (Err(actual_error), Ok(())) => { 624 | let mut f = String::new(); 625 | writeln!(f, "Did not expect error, but got").unwrap(); 626 | writeln!(f, "{}", actual_error).unwrap(); 627 | panic!("{}", f); 628 | } 629 | 630 | (Ok(()), Err(expected_error)) => { 631 | let expected_error = expected_error.to_string(); 632 | let mut f = String::new(); 633 | writeln!(f, "Expected error, but did not get one. Expected error:").unwrap(); 634 | writeln!(f, "{}", expected_error).unwrap(); 635 | panic!("{}", f); 636 | } 637 | 638 | (Err(actual_error), Err(expected_error)) => { 639 | let expected_error = expected_error.to_string(); 640 | if actual_error != expected_error { 641 | let mut f = String::new(); 642 | writeln!(f, "Errors didn't match").unwrap(); 643 | writeln!(f, "Expected:").unwrap(); 644 | writeln!(f, "{}", expected_error).unwrap(); 645 | writeln!(f, "Got:").unwrap(); 646 | writeln!(f, "{}", actual_error).unwrap(); 647 | panic!("{}", f); 648 | } 649 | } 650 | } 651 | } 652 | 653 | fn test_partial_match(lhs: Value, rhs: Value) -> Result<(), String> { 654 | assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Inclusive)) 655 | } 656 | 657 | fn test_exact_match(lhs: Value, rhs: Value) -> Result<(), String> { 658 | assert_json_matches_no_panic(&lhs, &rhs, Config::new(CompareMode::Strict)) 659 | } 660 | } 661 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use assert_json_diff::{ 2 | assert_json_eq, assert_json_include, assert_json_matches, assert_json_matches_no_panic, 3 | CompareMode, Config, NumericMode, 4 | }; 5 | use serde::Serialize; 6 | use serde_json::json; 7 | 8 | #[test] 9 | fn can_pass() { 10 | assert_json_include!( 11 | actual: json!({ "a": { "b": true }, "c": [true, null, 1] }), 12 | expected: json!({ "a": { "b": true }, "c": [true, null, 1] }) 13 | ); 14 | 15 | assert_json_include!( 16 | actual: json!({ "a": { "b": true } }), 17 | expected: json!({ "a": {} }) 18 | ); 19 | 20 | assert_json_include!( 21 | actual: json!({ "a": { "b": true } }), 22 | expected: json!({ "a": {} }), 23 | ); 24 | 25 | assert_json_include!( 26 | expected: json!({ "a": {} }), 27 | actual: json!({ "a": { "b": true } }), 28 | ); 29 | } 30 | 31 | #[test] 32 | #[should_panic] 33 | fn can_fail() { 34 | assert_json_include!( 35 | actual: json!({ "a": { "b": true }, "c": [true, null, 1] }), 36 | expected: json!({ "a": { "b": false }, "c": [false, null, {}] }) 37 | ); 38 | } 39 | 40 | #[test] 41 | #[should_panic] 42 | fn different_numeric_types_include_should_fail() { 43 | assert_json_include!( 44 | actual: json!({ "a": { "b": true }, "c": 1 }), 45 | expected: json!({ "a": { "b": true }, "c": 1.0 }) 46 | ); 47 | } 48 | 49 | #[test] 50 | #[should_panic] 51 | fn different_numeric_types_eq_should_fail() { 52 | assert_json_eq!( 53 | json!({ "a": { "b": true }, "c": 1 }), 54 | json!({ "a": { "b": true }, "c": 1.0 }) 55 | ); 56 | } 57 | 58 | #[test] 59 | fn different_numeric_types_assume_float() { 60 | let actual = json!({ "a": { "b": true }, "c": [true, null, 1] }); 61 | let expected = json!({ "a": { "b": true }, "c": [true, null, 1.0] }); 62 | let config = Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloat); 63 | assert_json_matches!(actual, expected, config.clone()); 64 | 65 | assert_json_matches!(actual, expected, config.compare_mode(CompareMode::Strict)) 66 | } 67 | 68 | #[test] 69 | fn can_pass_with_exact_match() { 70 | assert_json_eq!(json!({ "a": { "b": true } }), json!({ "a": { "b": true } })); 71 | assert_json_eq!(json!({ "a": { "b": true } }), json!({ "a": { "b": true } }),); 72 | } 73 | 74 | #[test] 75 | #[should_panic] 76 | fn can_fail_with_exact_match() { 77 | assert_json_eq!(json!({ "a": { "b": true } }), json!({ "a": {} })); 78 | } 79 | 80 | #[test] 81 | fn inclusive_match_without_panicking() { 82 | assert!(assert_json_matches_no_panic( 83 | &json!({ "a": 1, "b": 2 }), 84 | &json!({ "b": 2}), 85 | Config::new(CompareMode::Inclusive,).numeric_mode(NumericMode::Strict), 86 | ) 87 | .is_ok()); 88 | 89 | assert!(assert_json_matches_no_panic( 90 | &json!({ "a": 1, "b": 2 }), 91 | &json!("foo"), 92 | Config::new(CompareMode::Inclusive,).numeric_mode(NumericMode::Strict), 93 | ) 94 | .is_err()); 95 | } 96 | 97 | #[test] 98 | fn exact_match_without_panicking() { 99 | assert!(assert_json_matches_no_panic( 100 | &json!([1, 2, 3]), 101 | &json!([1, 2, 3]), 102 | Config::new(CompareMode::Strict).numeric_mode(NumericMode::Strict) 103 | ) 104 | .is_ok()); 105 | 106 | assert!(assert_json_matches_no_panic( 107 | &json!([1, 2, 3]), 108 | &json!("foo"), 109 | Config::new(CompareMode::Strict).numeric_mode(NumericMode::Strict) 110 | ) 111 | .is_err()); 112 | } 113 | 114 | #[derive(Serialize)] 115 | struct User { 116 | id: i32, 117 | username: String, 118 | } 119 | 120 | #[test] 121 | fn include_with_serializable() { 122 | let user = User { 123 | id: 1, 124 | username: "bob".to_string(), 125 | }; 126 | 127 | assert_json_include!( 128 | actual: json!({ 129 | "id": 1, 130 | "username": "bob", 131 | "email": "bob@example.com" 132 | }), 133 | expected: user, 134 | ); 135 | } 136 | 137 | #[test] 138 | fn include_with_serializable_ref() { 139 | let user = User { 140 | id: 1, 141 | username: "bob".to_string(), 142 | }; 143 | 144 | assert_json_include!( 145 | actual: &json!({ 146 | "id": 1, 147 | "username": "bob", 148 | "email": "bob@example.com" 149 | }), 150 | expected: &user, 151 | ); 152 | } 153 | 154 | #[test] 155 | fn eq_with_serializable() { 156 | let user = User { 157 | id: 1, 158 | username: "bob".to_string(), 159 | }; 160 | 161 | assert_json_eq!( 162 | json!({ 163 | "id": 1, 164 | "username": "bob" 165 | }), 166 | user, 167 | ); 168 | } 169 | 170 | #[test] 171 | fn eq_with_serializable_ref() { 172 | let user = User { 173 | id: 1, 174 | username: "bob".to_string(), 175 | }; 176 | 177 | assert_json_eq!( 178 | &json!({ 179 | "id": 1, 180 | "username": "bob" 181 | }), 182 | &user, 183 | ); 184 | } 185 | -------------------------------------------------------------------------------- /tests/version-numbers.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate version_sync; 3 | 4 | #[test] 5 | fn test_readme_deps() { 6 | assert_markdown_deps_updated!("README.md"); 7 | } 8 | 9 | #[test] 10 | fn test_html_root_url() { 11 | assert_html_root_url_updated!("src/lib.rs"); 12 | } 13 | --------------------------------------------------------------------------------