insta-1.3.0/.cargo_vcs_info.json0000644000000001121375552205200122110ustar { "git": { "sha1": "e23b3a17680bd7fffb546fb7b30d4f0785c97d14" } } insta-1.3.0/.github/workflows/clippy.yml010064400007650000024000000002351375417450100164370ustar 00000000000000name: Clippy on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Run clippy run: make lint insta-1.3.0/.github/workflows/rustfmt.yml010064400007650000024000000002471375417450100166460ustar 00000000000000name: Rustfmt on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Run rustfmt run: make format-check insta-1.3.0/.github/workflows/tests.yml010064400007650000024000000002261375417450100163010ustar 00000000000000name: Tests on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Test run: make test insta-1.3.0/.gitignore010064400007650000024000000000361374651470200130100ustar 00000000000000target **/*.rs.bk /Cargo.lock insta-1.3.0/.vscode/settings.json010064400007650000024000000002011352771310300150770ustar 00000000000000{ "editor.formatOnSave": true, "rust.clippy_preference": "on", "rust.cfg_test": true, "rust.all_features": true }insta-1.3.0/CHANGELOG.md010064400007650000024000000065121375552046600126420ustar 00000000000000# Changelog ## 1.3.0 * Expose more useful methods from `Content`. * Fixes for latest rustc version. ## 1.2.0 * Fix invalid offset calculation for inline snapshot (#137) * Added support for newtype variant redactions. (#139) ## 1.1.0 * Added the `INSTA_SNAPSHOT_REFERENCES_FILE` environment variable to support deletions of unreferenced snapshot files. (#136) * Added support for TOML serializations. * Avoid diff calculation on large input files. (#135) * Added `prepend_module_to_snapshot` flag to disable prepending of module names to snapshot files. (#133) * Made `console` dependency optional. The `colors` feature can be disabled now which disables colored output. ## 1.0.0 * Globs now follow links (#132) * Added CSV Support (#134) * Changed globs to also include directories not just files. * Support snapshots outside source folder. (#70) * Update RON to 0.6. ## 0.16.1 * Add `Settings::bind_async` when the `async` feature is enabled. (#121) * Bumped `console` dependency to 0.11. (#124) * Fixed incorrect path handling for `glob!`. (#123) * Remove `cargo-insta` from workspace and add `Cargo.lock`. (#116) ## 0.16.0 * Made snapshot names optional for inline snapshots. (#106) * Remove legacy macros. (#115) * Made small improvements to cargo-insta's messaging and flags (#114) * Added new logo. * Added `glob` support. (#112) * Made `MetaData` fields internal. (#111) ## 0.15.0 * Added test output control (`INSTA_OUTPUT` envvar). (#103) ## 0.14.0 * Dependency bump for `console` (lowers total dependency count) * Change binary name to `cargo insta` in help pages. ## 0.13.1 * Added support for `INSTA_UPDATE=unseen` to write out unseen snapshots without review (#96) * Added the `backtrace` feature which adds support for test name (and thus snapshot name) recovery from the backtrace if rust-test is not used in concurrent mode (#94, #98) ## 0.13 * Add support for deep wildcard matches (#92) * Use module paths for test names (#87) * Do not emit useless indentations for empty lines (#88) ## 0.12 * Improve redactions support (#81) * Deprecated macros are now hidden * Reduce number of dependencies further. * Added support for newtype struct redactions. * Fixed bugs with recursive content operations (#80) ## 0.11 * redactions are now an optional feature that must be turned on to be used (`redactions`). * RON format is now an optional feature that must be turned on to be used (`ron`). * added support for sorting maps before serialization. * added settings support. * added support for overriding the snapshot path. * correctly handle nested macros that might contain inline snapshots. * use thread name as snapshot name for inline snapshots. * use leading whitespace normalization for inline snapshots. * removed `creator` and `created` field from snapshot metadata. * removed the `_matches` suffix from all macros. * added an `--accept` option to `cargo insta test` * added `--force-update-snapshots` option to `cargo insta test` * added `--jobs` and `--release` argument to `cargo insta test`. To upgrade to the new insta macros and snapshot formats you can use [`fastmod`](https://crates.io/crates/fastmod) and `cargo-insta` together: $ cargo install fastmod $ cargo install cargo-insta $ fastmod '\bassert_([a-z]+_snapshot)_matches!' 'assert_${1}!' -e rs --accept-all $ cargo insta test --all --force-update-snapshots --accept insta-1.3.0/Cargo.toml0000644000000034361375552205200102230ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "insta" version = "1.3.0" authors = ["Armin Ronacher "] exclude = ["assets/*"] description = "A snapshot testing library for Rust" homepage = "https://github.com/mitsuhiko/insta" readme = "README.md" keywords = ["snapshot", "testing", "jest", "approval"] license = "Apache-2.0" repository = "https://github.com/mitsuhiko/insta" [package.metadata.docs.rs] all-features = true [dependencies.backtrace] version = "0.3.42" optional = true [dependencies.console] version = "0.12.0" optional = true default-features = false [dependencies.csv] version = "1.1.3" optional = true [dependencies.difference] version = "2.0.0" [dependencies.globwalk] version = "0.8.0" optional = true [dependencies.lazy_static] version = "1.4.0" [dependencies.pest] version = "2.1.0" optional = true [dependencies.pest_derive] version = "2.1.0" optional = true [dependencies.ron] version = "0.6.2" optional = true [dependencies.serde] version = "1.0.85" features = ["derive"] [dependencies.serde_json] version = "1.0.36" [dependencies.serde_yaml] version = "0.8.14" [dependencies.toml] version = "0.5.6" optional = true [features] colors = ["console"] default = ["colors"] glob = ["globwalk"] redactions = ["pest", "pest_derive"] serialization = [] insta-1.3.0/Cargo.toml.orig010064400007650000024000000025051375552177600137230ustar 00000000000000[package] name = "insta" version = "1.3.0" license = "Apache-2.0" authors = ["Armin Ronacher "] description = "A snapshot testing library for Rust" edition = "2018" homepage = "https://github.com/mitsuhiko/insta" repository = "https://github.com/mitsuhiko/insta" keywords = ["snapshot", "testing", "jest", "approval"] readme = "README.md" exclude = [ "assets/*" ] [package.metadata.docs.rs] all-features = true [features] default = ["colors"] # when the redactions feature is enabled values can be redacted in serialized # snapshots. redactions = ["pest", "pest_derive"] # Glob support glob = ["globwalk"] # Color support colors = ["console"] # This feature is now just always enabled because we use yaml internally now. serialization = [] [dependencies] csv = { version = "1.1.3", optional = true } difference = "2.0.0" serde = { version = "1.0.85", features = ["derive"] } serde_yaml = "0.8.14" console = { version = "0.12.0", optional = true, default-features = false } serde_json = "1.0.36" lazy_static = "1.4.0" pest = { version = "2.1.0", optional = true } pest_derive = { version = "2.1.0", optional = true } ron = { version = "0.6.2", optional = true } backtrace = { version = "0.3.42", optional = true } globwalk = { version = "0.8.0", optional = true } toml = { version = "0.5.6", optional = true } insta-1.3.0/LICENSE010064400007650000024000000251371352771310300120270ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. insta-1.3.0/Makefile010064400007650000024000000016301375551703600124640ustar 00000000000000all: test build: @cargo build --all-features doc: @cargo doc --all-features test: cargotest cargo-insta-tests cargo-insta-tests: @echo "CARGO-INSTA INTEGRATION TESTS" @cd cargo-insta/integration-tests; cargo run cargotest: @echo "CARGO TESTS" @rustup component add rustfmt 2> /dev/null @cargo test @cargo test --all-features @cargo test --no-default-features @cargo test --features redactions,backtrace -- --test-threads 1 @cd cargo-insta; cargo test format: @rustup component add rustfmt 2> /dev/null @cargo fmt --all format-check: @rustup component add rustfmt 2> /dev/null @cargo fmt --all -- --check lint: @rustup component add clippy 2> /dev/null @cargo clippy update-readme: @cargo readme | perl -pe 's/\[`(.*?)`]/`$$1`/g' | perl -pe 's/\[(.*?)\](?![(])/$$1/g' > README.md @cd cargo-insta; cargo readme > README.md .PHONY: all doc test cargotest format format-check lint update-readme insta-1.3.0/README.md010064400007650000024000000267471375551722400123220ustar 00000000000000# insta

insta: a snapshot testing library for Rust

## What are snapshot tests Snapshots tests (also sometimes called approval tests) are tests that assert values against a reference value (the snapshot). This is similar to how `assert_eq!` lets you compare a value against a reference value but unlike simple string assertions, snapshot tests let you test against complex values and come with comprehensive tools to review changes. Snapshot tests are particularly useful if your reference values are very large or change often. ## What it looks like: ```rust #test fn test_hello_world() { insta::assert_debug_snapshot!(vec!1, 2, 3); } ``` Curious? There is a screencast that shows the entire workflow: [watch the insta introduction screencast](https://www.youtube.com/watch?v=rCHrMqE4JOY&feature=youtu.be). Or if you're not into videos, read the [one minute introduction](#introduction). ## Introduction Install `insta`: Recommended way if you have `cargo-edit` installed: ``` $ cargo add --dev insta ``` Alternatively edit your `Cargo.toml` manually and add `insta` as manual dependency. And for an improved review experience also install `cargo-insta`: ``` $ cargo install cargo-insta ``` ```rust use insta::assert_debug_snapshot; #test fn test_snapshots() { assert_debug_snapshot!(vec!1, 2, 3); } ``` The recommended flow is to run the tests once, have them fail and check if the result is okay. By default the new snapshots are stored next to the old ones with the extra `.new` extension. Once you are satisifed move the new files over. To simplify this workflow you can use `cargo insta review` which will let you interactively review them: ``` $ cargo test $ cargo insta review ``` For more information on updating see Snapshot Updating. Snapshot Updating: #snapshot-updating ## How it operates This crate exports multiple macros for snapshot testing: - `assert_snapshot!` for comparing basic string snapshots. - `assert_debug_snapshot!` for comparing `Debug` outputs of values. - `assert_display_snapshot!` for comparing `Display` outputs of values. - `assert_csv_snapshot!` for comparing CSV serialized output of types implementing `serde::Serialize`. (requires the `csv` feature) - `assert_toml_snapshot!` for comparing TOML serialized output of types implementing `serde::Serialize`. (requires the `toml` feature) - `assert_yaml_snapshot!` for comparing YAML serialized output of types implementing `serde::Serialize`. - `assert_ron_snapshot!` for comparing RON serialized output of types implementing `serde::Serialize`. (requires the `ron` feature) - `assert_json_snapshot!` for comparing JSON serialized output of types implementing `serde::Serialize`. Snapshots are stored in the `snapshots` folder right next to the test file where this is used. The name of the file is `__.snap` where the `name` of the snapshot. Snapshots can either be explicitly named or the name is derived from the test name. Additionally snapshots can also be stored inline. In that case the `cargo-insta`(https://crates.io/crates/cargo-insta) tool is necessary. See [inline snapshots](#inline-snapshots) for more information. For macros that work with `serde::Serialize` this crate also permits redacting of partial values. See [redactions](#redactions) for more information. ## Snapshot files The committed snapshot files will have a header with some meta information that can make debugging easier and the snapshot: ``` --- expression: "vec!1, 2, 3" source: tests/test_basic.rs --- [ 1, 2, 3 ] ``` ## Snapshot updating During test runs snapshots will be updated according to the `INSTA_UPDATE` environment variable. The default is `auto` which will write all new snapshots into `.snap.new` files if no CI is detected so that `cargo-insta` can pick them up. Normally you don't have to change this variable. `INSTA_UPDATE` modes: - `auto`: the default. `no` for CI environments or `new` otherwise - `always`: overwrites old snapshot files with new ones unasked - `unseen`: behaves like `always` for new snapshots and `new` for others - `new`: write new snapshots into `.snap.new` files - `no`: does not update snapshot files at all (just runs tests) When `new` or `auto` is used as mode the `cargo-insta` command can be used to review the snapshots conveniently: ``` $ cargo install cargo-insta $ cargo test $ cargo insta review ``` "enter" or "a" accepts a new snapshot, "escape" or "r" rejects, "space" or "s" skips the snapshot for now. For more information invoke `cargo insta --help`. ## Test assertions By default the tests will fail when the snapshot assertion fails. However if a test produces more than one snapshot it can be useful to force a test to pass so that all new snapshots are created in one go. This can be enabled by setting `INSTA_FORCE_PASS` to `1`: ``` $ INSTA_FORCE_PASS=1 cargo test --no-fail-fast ``` A better way to do this is to run `cargo insta test --review` which will run all tests with force pass and then bring up the review tool: ``` $ cargo insta test --review ``` ## Named snapshots All snapshot assertion functions let you leave out the snapshot name in which case the snapshot name is derived from the test name (with an optional leading `test_` prefix removed. This works because the rust test runner names the thread by the test name and the name is taken from the thread name. In case your test spawns additional threads this will not work and you will need to provide a name explicitly. There are some situations in which rust test does not name or use threads. In these cases insta will panic with an error. The `backtrace` feature can be enabled in which case insta will attempt to recover the test name from the backtrace. Explicit snapshot naming can also otherwise be useful to be more explicit when multiple snapshots are tested within one function as the default behavior would be to just count up the snapshot names. To provide an explicit name provide the name of the snapshot as first argument to the macro: ```rust #test fn test_something() { assert_snapshot!("first_snapshot", "first value"); assert_snapshot!("second_snapshot", "second value"); } ``` This will create two snapshots: `first_snapshot` for the first value and `second_snapshot` for the second value. Without explicit naming the snapshots would be called `something` and `something-2`. ## Test Output Control Insta by default will output quite a lot of information as tests run. For instance it will print out all the diffs. This can be controlled by setting the `INSTA_OUTPUT` environment variable. The following values are possible: * `diff` (default): prints the diffs * `summary`: prints only summaries (name of snapshot files etc.) * `minimal`: like `summary` but more minimal * `none`: insta will not output any extra information ## Redactions **Feature:** `redactions` For all snapshots created based on `serde::Serialize` output `insta` supports redactions. This permits replacing values with hardcoded other values to make snapshots stable when otherwise random or otherwise changing values are involved. Redactions became an optional feature in insta 0.11 and can be enabled with the `redactions` feature. Redactions can be defined as the third argument to those macros with the syntax `{ selector => replacement_value }`. The following selectors exist: - `.key`: selects the given key - `"key"`: alternative syntax for keys - `index`: selects the given index in an array - ``: selects all items on an array - `:end`: selects all items up to `end` (excluding, supports negative indexing) - `start:`: selects all items starting with `start` - `start:end`: selects all items from `start` to `end` (end excluding, supports negative indexing). - `.*`: selects all keys on that depth - `.**`: performs a deep match (zero or more items). Can only be used once. Example usage: ```rust #derive(Serialize) pub struct User { id: Uuid, username: String, extra: HashMap, } assert_yaml_snapshot!(&User { id: Uuid::new_v4(), username: "john_doe".to_string(), extra: { let mut map = HashMap::new(); map.insert("ssn".to_string(), "123-123-123".to_string()); map }, }, { ".id" => "uuid", ".extra.ssn" => "ssn" }); ``` It's also possible to execute a callback that can produce a new value instead of hardcoding a replacement value by using the `dynamic_redaction` function: ```rust assert_yaml_snapshot!(&User { id: Uuid::new_v4(), username: "john_doe".to_string(), }, { ".id" => dynamic_redaction(|value, _| { // assert that the value looks like a uuid here "uuid" }), }); ``` ## Globbing **Feature:** `glob` Sometimes it can be useful to run code against multiple input files. The easiest way to accomplish this is to use the `glob!` macro which runs a closure for each input path that matches. Before the closure is executed the settings are updated to set a reference to the input file and the appropriate snapshot suffix. Example: ```rust use std::fs; glob!("inputs/*.txt", |path| { let input = fs::read_to_string(path).unwrap(); assert_json_snapshot!(input.to_uppercase()); }); ``` The path to the glob macro is relative to the location of the test file. It uses the `globwalk` crate for actual glob operations. ## Inline Snapshots Additionally snapshots can also be stored inline. In that case the format for the snapshot macros is `assert_snapshot!(reference_value, @"snapshot")`. The leading at sign (`@`) indicates that the following string is the reference value. `cargo-insta` will then update that string with the new value on review. Example: ```rust #derive(Serialize) pub struct User { username: String, } assert_yaml_snapshot!(User { username: "john_doe".to_string(), }, @""); ``` After the initial test failure you can run `cargo insta review` to accept the change. The file will then be updated automatically. ## Features The following features exist: * `csv`: enables CSV support (`assert_csv_snapshot!`) * `ron`: enables RON support (`assert_ron_snapshot!`) * `toml`: enables TOML support (`assert_toml_snapshot!`) * `redactions`: enables support for redactions * `glob`: enables support for globbing (`glob!`) * `colors`: enables color output (enabled by default) ## Settings There are some settings that can be changed on a per-thread (and thus per-test) basis. For more information see Settings. ## Legacy Snapshot Formats With insta 0.11 the snapshot format was improved for inline snapshots. The old snapshot format will continue to be available but if you want to upgrade them make sure the tests pass first and then run the following command to force a rewrite of them all: ``` $ cargo insta test --accept --force-update-snapshots ``` ## Deleting Unused Snapshots Insta cannot detect unused snapshot files. The reason for this is that insta does not control the execution of the entirety of the tests so it cannot spot which files are actually unreferenced. However you can use the `INSTA_SNAPSHOT_REFERENCES_FILE` environment variable to instruct insta to append all referenced files into a list. This can then be used to delete all files not referenced. For instance one could use [ripgrep](https://github.com/BurntSushi/ripgrep) like this: ``` export INSTA_SNAPSHOT_REFERENCES_FILE="$(mktemp)" cargo test rg --files -lg '*.snap' "$(pwd)" | grep -vFf "$INSTA_SNAPSHOT_REFERENCES_FILE" | xargs rm rm -f $INSTA_SNAPSHOT_REFERENCES_FILE ``` License: Apache-2.0 insta-1.3.0/scripts/bump-version.sh010075500007650000024000000005651375422202300154720ustar 00000000000000#!/bin/bash set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $SCRIPT_DIR/.. NEW_VERSION="${1}" echo "Bumping version: ${NEW_VERSION}" perl -pi -e "s/^version = \".*?\"/version = \"$NEW_VERSION\"/" Cargo.toml perl -pi -e "s/^(insta.*)?version = \".*?\"/\$1version = \"$NEW_VERSION\"/" cargo-insta/Cargo.toml cd cargo-insta; cargo check insta-1.3.0/src/content.rs010064400007650000024000000572511375551270500136430ustar 00000000000000// this module is based on the content module in serde::private::ser use serde::ser::{self, Serialize, Serializer}; use std::marker::PhantomData; /// Represents variable typed content. /// /// This is used for the serialization system to represent values /// before the actual snapshots are written and is also exposed to /// dynamic redaction functions. /// /// Some enum variants are intentionally not exposed to user code. /// It's generally recommended to construct content objects by /// using the [`From`](std::convert::From) trait and by using the /// accessor methods to assert on it. /// /// While matching on the content is possible in theory it is /// recommended against. The reason for this is that the content /// enum holds variants that can "wrap" values where it's not /// expected. For instance if a field holds an `Option` /// you cannot use pattern matching to extract the string as it /// will be contained in an internal `Some` variant that is not /// exposed. On the other hand the `as_str` method will /// automatically resolve such internal wrappers. /// /// If you do need to pattern match you should use the /// `resolve_inner` method to resolve such internal wrappers. #[derive(Debug, Clone)] pub enum Content { Bool(bool), U8(u8), U16(u16), U32(u32), U64(u64), I8(i8), I16(i16), I32(i32), I64(i64), F32(f32), F64(f64), Char(char), String(String), Bytes(Vec), #[doc(hidden)] None, #[doc(hidden)] Some(Box), #[doc(hidden)] Unit, #[doc(hidden)] UnitStruct(&'static str), #[doc(hidden)] UnitVariant(&'static str, u32, &'static str), #[doc(hidden)] NewtypeStruct(&'static str, Box), #[doc(hidden)] NewtypeVariant(&'static str, u32, &'static str, Box), Seq(Vec), #[doc(hidden)] Tuple(Vec), #[doc(hidden)] TupleStruct(&'static str, Vec), #[doc(hidden)] TupleVariant(&'static str, u32, &'static str, Vec), Map(Vec<(Content, Content)>), #[doc(hidden)] Struct(&'static str, Vec<(&'static str, Content)>), #[doc(hidden)] StructVariant( &'static str, u32, &'static str, Vec<(&'static str, Content)>, ), } #[derive(PartialEq, PartialOrd, Debug)] pub enum Key<'a> { Bool(bool), U64(u64), I64(i64), F64(f64), Str(&'a str), Bytes(&'a [u8]), Other, } impl<'a> Eq for Key<'a> {} impl<'a> Ord for Key<'a> { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Less) } } macro_rules! impl_from { ($ty:ty, $newty:ident) => { impl From<$ty> for Content { fn from(value: $ty) -> Content { Content::$newty(value) } } }; } impl_from!(bool, Bool); impl_from!(u8, U8); impl_from!(u16, U16); impl_from!(u32, U32); impl_from!(u64, U64); impl_from!(i8, I8); impl_from!(i16, I16); impl_from!(i32, I32); impl_from!(i64, I64); impl_from!(f32, F32); impl_from!(f64, F64); impl_from!(char, Char); impl_from!(String, String); impl_from!(Vec, Bytes); impl From<()> for Content { fn from(_value: ()) -> Content { Content::Unit } } impl<'a> From<&'a str> for Content { fn from(value: &'a str) -> Content { Content::String(value.to_string()) } } impl<'a> From<&'a [u8]> for Content { fn from(value: &'a [u8]) -> Content { Content::Bytes(value.to_vec()) } } impl Content { /// This resolves the innermost content in a chain of /// wrapped content. /// /// For instance if you encounter an `Option>` /// field the content will be wrapped twice in an internal /// option wrapper. If you need to pattern match you will /// need in some situations to first resolve the inner value /// before such matching can take place as there is no exposed /// way to match on these wrappers. /// /// This method does not need to be called for the `as_` /// methods which resolve automatically. pub fn resolve_inner(&self) -> &Content { match *self { Content::Some(ref v) | Content::NewtypeStruct(_, ref v) | Content::NewtypeVariant(_, _, _, ref v) => v.resolve_inner(), ref other => other, } } /// Returns the value as string pub fn as_str(&self) -> Option<&str> { match self.resolve_inner() { Content::String(ref s) => Some(s.as_str()), _ => None, } } /// Returns the value as bytes pub fn as_bytes(&self) -> Option<&[u8]> { match self.resolve_inner() { Content::Bytes(ref b) => Some(&*b), _ => None, } } /// Returns the value as slice of content values. pub fn as_slice(&self) -> Option<&[Content]> { match self.resolve_inner() { Content::Seq(ref v) | Content::Tuple(ref v) | Content::TupleVariant(_, _, _, ref v) => { Some(&v[..]) } _ => None, } } /// Returns true if the value is nil. pub fn is_nil(&self) -> bool { match self.resolve_inner() { Content::None | Content::Unit => true, _ => false, } } pub(crate) fn as_key(&self) -> Key<'_> { match *self.resolve_inner() { Content::Bool(val) => Key::Bool(val), Content::Char(val) => Key::U64(val as u64), Content::U16(val) => Key::U64(val.into()), Content::U32(val) => Key::U64(val.into()), Content::U64(val) => Key::U64(val), Content::I16(val) => Key::I64(val.into()), Content::I32(val) => Key::I64(val.into()), Content::I64(val) => Key::I64(val), Content::F32(val) => Key::F64(val.into()), Content::F64(val) => Key::F64(val), Content::String(ref val) => Key::Str(&val.as_str()), Content::Bytes(ref val) => Key::Bytes(&val[..]), _ => Key::Other, } } /// Returns the value as bool pub fn as_bool(&self) -> Option { match *self.resolve_inner() { Content::Bool(val) => Some(val), _ => None, } } /// Returns the value as u64 pub fn as_u64(&self) -> Option { match *self.resolve_inner() { Content::U8(v) => Some(u64::from(v)), Content::U16(v) => Some(u64::from(v)), Content::U32(v) => Some(u64::from(v)), Content::U64(v) => Some(v), Content::I8(v) if v >= 0 => Some(v as u64), Content::I16(v) if v >= 0 => Some(v as u64), Content::I32(v) if v >= 0 => Some(v as u64), Content::I64(v) if v >= 0 => Some(v as u64), _ => None, } } /// Returns the value as i64 pub fn as_i64(&self) -> Option { match *self.resolve_inner() { Content::U8(v) => Some(i64::from(v)), Content::U16(v) => Some(i64::from(v)), Content::U32(v) => Some(i64::from(v)), Content::U64(v) => { let rv = v as i64; if rv as u64 == v { Some(rv) } else { None } } Content::I8(v) => Some(i64::from(v)), Content::I16(v) => Some(i64::from(v)), Content::I32(v) => Some(i64::from(v)), Content::I64(v) => Some(v), _ => None, } } /// Returns the value as f64 pub fn as_f64(&self) -> Option { match *self.resolve_inner() { Content::F32(v) => Some(f64::from(v)), Content::F64(v) => Some(v), _ => None, } } pub(crate) fn sort_maps(&mut self) { self.walk(&mut |content| { if let Content::Map(ref mut items) = content { items.sort_by(|a, b| a.0.as_key().cmp(&b.0.as_key())); } true }) } /// Recursively walks the content structure mutably. /// /// The callback is invoked for every content in the tree. pub fn walk bool>(&mut self, visit: &mut F) { if !visit(self) { return; } match *self { Content::Some(ref mut inner) => { Self::walk(&mut *inner, visit); } Content::NewtypeStruct(_, ref mut inner) => { Self::walk(&mut *inner, visit); } Content::NewtypeVariant(_, _, _, ref mut inner) => { Self::walk(&mut *inner, visit); } Content::Seq(ref mut vec) => { for inner in vec.iter_mut() { Self::walk(inner, visit); } } Content::Map(ref mut vec) => { for inner in vec.iter_mut() { Self::walk(&mut inner.0, visit); Self::walk(&mut inner.1, visit); } } Content::Struct(_, ref mut vec) => { for inner in vec.iter_mut() { Self::walk(&mut inner.1, visit); } } Content::StructVariant(_, _, _, ref mut vec) => { for inner in vec.iter_mut() { Self::walk(&mut inner.1, visit); } } Content::Tuple(ref mut vec) => { for inner in vec.iter_mut() { Self::walk(inner, visit); } } Content::TupleStruct(_, ref mut vec) => { for inner in vec.iter_mut() { Self::walk(inner, visit); } } Content::TupleVariant(_, _, _, ref mut vec) => { for inner in vec.iter_mut() { Self::walk(inner, visit); } } _ => {} } } } impl Serialize for Content { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match *self { Content::Bool(b) => serializer.serialize_bool(b), Content::U8(u) => serializer.serialize_u8(u), Content::U16(u) => serializer.serialize_u16(u), Content::U32(u) => serializer.serialize_u32(u), Content::U64(u) => serializer.serialize_u64(u), Content::I8(i) => serializer.serialize_i8(i), Content::I16(i) => serializer.serialize_i16(i), Content::I32(i) => serializer.serialize_i32(i), Content::I64(i) => serializer.serialize_i64(i), Content::F32(f) => serializer.serialize_f32(f), Content::F64(f) => serializer.serialize_f64(f), Content::Char(c) => serializer.serialize_char(c), Content::String(ref s) => serializer.serialize_str(s), Content::Bytes(ref b) => serializer.serialize_bytes(b), Content::None => serializer.serialize_none(), Content::Some(ref c) => serializer.serialize_some(&**c), Content::Unit => serializer.serialize_unit(), Content::UnitStruct(n) => serializer.serialize_unit_struct(n), Content::UnitVariant(n, i, v) => serializer.serialize_unit_variant(n, i, v), Content::NewtypeStruct(n, ref c) => serializer.serialize_newtype_struct(n, &**c), Content::NewtypeVariant(n, i, v, ref c) => { serializer.serialize_newtype_variant(n, i, v, &**c) } Content::Seq(ref elements) => elements.serialize(serializer), Content::Tuple(ref elements) => { use serde::ser::SerializeTuple; let mut tuple = serializer.serialize_tuple(elements.len())?; for e in elements { tuple.serialize_element(e)?; } tuple.end() } Content::TupleStruct(n, ref fields) => { use serde::ser::SerializeTupleStruct; let mut ts = serializer.serialize_tuple_struct(n, fields.len())?; for f in fields { ts.serialize_field(f)?; } ts.end() } Content::TupleVariant(n, i, v, ref fields) => { use serde::ser::SerializeTupleVariant; let mut tv = serializer.serialize_tuple_variant(n, i, v, fields.len())?; for f in fields { tv.serialize_field(f)?; } tv.end() } Content::Map(ref entries) => { use serde::ser::SerializeMap; let mut map = serializer.serialize_map(Some(entries.len()))?; for &(ref k, ref v) in entries { map.serialize_entry(k, v)?; } map.end() } Content::Struct(n, ref fields) => { use serde::ser::SerializeStruct; let mut s = serializer.serialize_struct(n, fields.len())?; for &(k, ref v) in fields { s.serialize_field(k, v)?; } s.end() } Content::StructVariant(n, i, v, ref fields) => { use serde::ser::SerializeStructVariant; let mut sv = serializer.serialize_struct_variant(n, i, v, fields.len())?; for &(k, ref v) in fields { sv.serialize_field(k, v)?; } sv.end() } } } } pub struct ContentSerializer { error: PhantomData, } impl ContentSerializer { pub fn new() -> Self { ContentSerializer { error: PhantomData } } } impl Serializer for ContentSerializer where E: ser::Error, { type Ok = Content; type Error = E; type SerializeSeq = SerializeSeq; type SerializeTuple = SerializeTuple; type SerializeTupleStruct = SerializeTupleStruct; type SerializeTupleVariant = SerializeTupleVariant; type SerializeMap = SerializeMap; type SerializeStruct = SerializeStruct; type SerializeStructVariant = SerializeStructVariant; fn serialize_bool(self, v: bool) -> Result { Ok(Content::Bool(v)) } fn serialize_i8(self, v: i8) -> Result { Ok(Content::I8(v)) } fn serialize_i16(self, v: i16) -> Result { Ok(Content::I16(v)) } fn serialize_i32(self, v: i32) -> Result { Ok(Content::I32(v)) } fn serialize_i64(self, v: i64) -> Result { Ok(Content::I64(v)) } fn serialize_u8(self, v: u8) -> Result { Ok(Content::U8(v)) } fn serialize_u16(self, v: u16) -> Result { Ok(Content::U16(v)) } fn serialize_u32(self, v: u32) -> Result { Ok(Content::U32(v)) } fn serialize_u64(self, v: u64) -> Result { Ok(Content::U64(v)) } fn serialize_f32(self, v: f32) -> Result { Ok(Content::F32(v)) } fn serialize_f64(self, v: f64) -> Result { Ok(Content::F64(v)) } fn serialize_char(self, v: char) -> Result { Ok(Content::Char(v)) } fn serialize_str(self, value: &str) -> Result { Ok(Content::String(value.to_owned())) } fn serialize_bytes(self, value: &[u8]) -> Result { Ok(Content::Bytes(value.to_owned())) } fn serialize_none(self) -> Result { Ok(Content::None) } fn serialize_some(self, value: &T) -> Result where T: Serialize, { Ok(Content::Some(Box::new(value.serialize(self)?))) } fn serialize_unit(self) -> Result { Ok(Content::Unit) } fn serialize_unit_struct(self, name: &'static str) -> Result { Ok(Content::UnitStruct(name)) } fn serialize_unit_variant( self, name: &'static str, variant_index: u32, variant: &'static str, ) -> Result { Ok(Content::UnitVariant(name, variant_index, variant)) } fn serialize_newtype_struct( self, name: &'static str, value: &T, ) -> Result where T: Serialize, { Ok(Content::NewtypeStruct( name, Box::new(value.serialize(self)?), )) } fn serialize_newtype_variant( self, name: &'static str, variant_index: u32, variant: &'static str, value: &T, ) -> Result where T: Serialize, { Ok(Content::NewtypeVariant( name, variant_index, variant, Box::new(value.serialize(self)?), )) } fn serialize_seq(self, len: Option) -> Result { Ok(SerializeSeq { elements: Vec::with_capacity(len.unwrap_or(0)), error: PhantomData, }) } fn serialize_tuple(self, len: usize) -> Result { Ok(SerializeTuple { elements: Vec::with_capacity(len), error: PhantomData, }) } fn serialize_tuple_struct( self, name: &'static str, len: usize, ) -> Result { Ok(SerializeTupleStruct { name, fields: Vec::with_capacity(len), error: PhantomData, }) } fn serialize_tuple_variant( self, name: &'static str, variant_index: u32, variant: &'static str, len: usize, ) -> Result { Ok(SerializeTupleVariant { name, variant_index, variant, fields: Vec::with_capacity(len), error: PhantomData, }) } fn serialize_map(self, len: Option) -> Result { Ok(SerializeMap { entries: Vec::with_capacity(len.unwrap_or(0)), key: None, error: PhantomData, }) } fn serialize_struct(self, name: &'static str, len: usize) -> Result { Ok(SerializeStruct { name, fields: Vec::with_capacity(len), error: PhantomData, }) } fn serialize_struct_variant( self, name: &'static str, variant_index: u32, variant: &'static str, len: usize, ) -> Result { Ok(SerializeStructVariant { name, variant_index, variant, fields: Vec::with_capacity(len), error: PhantomData, }) } } pub struct SerializeSeq { elements: Vec, error: PhantomData, } impl ser::SerializeSeq for SerializeSeq where E: ser::Error, { type Ok = Content; type Error = E; fn serialize_element(&mut self, value: &T) -> Result<(), E> where T: Serialize, { let value = value.serialize(ContentSerializer::::new())?; self.elements.push(value); Ok(()) } fn end(self) -> Result { Ok(Content::Seq(self.elements)) } } pub struct SerializeTuple { elements: Vec, error: PhantomData, } impl ser::SerializeTuple for SerializeTuple where E: ser::Error, { type Ok = Content; type Error = E; fn serialize_element(&mut self, value: &T) -> Result<(), E> where T: Serialize, { let value = value.serialize(ContentSerializer::::new())?; self.elements.push(value); Ok(()) } fn end(self) -> Result { Ok(Content::Tuple(self.elements)) } } pub struct SerializeTupleStruct { name: &'static str, fields: Vec, error: PhantomData, } impl ser::SerializeTupleStruct for SerializeTupleStruct where E: ser::Error, { type Ok = Content; type Error = E; fn serialize_field(&mut self, value: &T) -> Result<(), E> where T: Serialize, { let value = value.serialize(ContentSerializer::::new())?; self.fields.push(value); Ok(()) } fn end(self) -> Result { Ok(Content::TupleStruct(self.name, self.fields)) } } pub struct SerializeTupleVariant { name: &'static str, variant_index: u32, variant: &'static str, fields: Vec, error: PhantomData, } impl ser::SerializeTupleVariant for SerializeTupleVariant where E: ser::Error, { type Ok = Content; type Error = E; fn serialize_field(&mut self, value: &T) -> Result<(), E> where T: Serialize, { let value = value.serialize(ContentSerializer::::new())?; self.fields.push(value); Ok(()) } fn end(self) -> Result { Ok(Content::TupleVariant( self.name, self.variant_index, self.variant, self.fields, )) } } pub struct SerializeMap { entries: Vec<(Content, Content)>, key: Option, error: PhantomData, } impl ser::SerializeMap for SerializeMap where E: ser::Error, { type Ok = Content; type Error = E; fn serialize_key(&mut self, key: &T) -> Result<(), E> where T: Serialize, { let key = key.serialize(ContentSerializer::::new())?; self.key = Some(key); Ok(()) } fn serialize_value(&mut self, value: &T) -> Result<(), E> where T: Serialize, { let key = self .key .take() .expect("serialize_value called before serialize_key"); let value = value.serialize(ContentSerializer::::new())?; self.entries.push((key, value)); Ok(()) } fn end(self) -> Result { Ok(Content::Map(self.entries)) } fn serialize_entry(&mut self, key: &K, value: &V) -> Result<(), E> where K: Serialize, V: Serialize, { let key = key.serialize(ContentSerializer::::new())?; let value = value.serialize(ContentSerializer::::new())?; self.entries.push((key, value)); Ok(()) } } pub struct SerializeStruct { name: &'static str, fields: Vec<(&'static str, Content)>, error: PhantomData, } impl ser::SerializeStruct for SerializeStruct where E: ser::Error, { type Ok = Content; type Error = E; fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), E> where T: Serialize, { let value = value.serialize(ContentSerializer::::new())?; self.fields.push((key, value)); Ok(()) } fn end(self) -> Result { Ok(Content::Struct(self.name, self.fields)) } } pub struct SerializeStructVariant { name: &'static str, variant_index: u32, variant: &'static str, fields: Vec<(&'static str, Content)>, error: PhantomData, } impl ser::SerializeStructVariant for SerializeStructVariant where E: ser::Error, { type Ok = Content; type Error = E; fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), E> where T: Serialize, { let value = value.serialize(ContentSerializer::::new())?; self.fields.push((key, value)); Ok(()) } fn end(self) -> Result { Ok(Content::StructVariant( self.name, self.variant_index, self.variant, self.fields, )) } } insta-1.3.0/src/glob.rs010064400007650000024000000012741375421627400131070ustar 00000000000000use std::path::Path; use globwalk::{FileType, GlobWalkerBuilder}; use crate::settings::Settings; pub fn glob_exec(base: &Path, pattern: &str, mut f: F) { let walker = GlobWalkerBuilder::new(base, pattern) .case_insensitive(true) .follow_links(true) .file_type(FileType::all()) .build() .unwrap(); for file in walker { let file = file.unwrap(); let path = file.path(); let mut settings = Settings::clone_current(); settings.set_input_file(&path); settings.set_snapshot_suffix(path.file_name().unwrap().to_str().unwrap()); settings.bind(|| { f(path); }); } } insta-1.3.0/src/lib.rs010064400007650000024000000356421375551722200127360ustar 00000000000000//!
//! //!

insta: a snapshot testing library for Rust

//!
//! //! # What are snapshot tests //! //! Snapshots tests (also sometimes called approval tests) are tests that //! assert values against a reference value (the snapshot). This is similar //! to how `assert_eq!` lets you compare a value against a reference value but //! unlike simple string assertions, snapshot tests let you test against complex //! values and come with comprehensive tools to review changes. //! //! Snapshot tests are particularly useful if your reference values are very //! large or change often. //! //! # What it looks like: //! //! ```no_run //! #[test] //! fn test_hello_world() { //! insta::assert_debug_snapshot!(vec![1, 2, 3]); //! } //! ``` //! //! Curious? There is a screencast that shows the entire workflow: [watch the insta //! introduction screencast](https://www.youtube.com/watch?v=rCHrMqE4JOY&feature=youtu.be). //! Or if you're not into videos, read the [one minute introduction](#introduction). //! //! # Introduction //! //! Install `insta`: //! //! Recommended way if you have `cargo-edit` installed: //! //! ```text //! $ cargo add --dev insta //! ``` //! //! Alternatively edit your `Cargo.toml` manually and add `insta` as manual //! dependency. //! //! And for an improved review experience also install `cargo-insta`: //! //! ```text //! $ cargo install cargo-insta //! ``` //! //! ```no_run //! use insta::assert_debug_snapshot; //! //! #[test] //! fn test_snapshots() { //! assert_debug_snapshot!(vec![1, 2, 3]); //! } //! ``` //! //! The recommended flow is to run the tests once, have them fail and check //! if the result is okay. By default the new snapshots are stored next //! to the old ones with the extra `.new` extension. Once you are satisifed //! move the new files over. To simplify this workflow you can use //! `cargo insta review` which will let you interactively review them: //! //! ```text //! $ cargo test //! $ cargo insta review //! ``` //! //! For more information on updating see [Snapshot Updating]. //! //! [Snapshot Updating]: #snapshot-updating //! //! # How it operates //! //! This crate exports multiple macros for snapshot testing: //! //! - `assert_snapshot!` for comparing basic string snapshots. //! - `assert_debug_snapshot!` for comparing `Debug` outputs of values. //! - `assert_display_snapshot!` for comparing `Display` outputs of values. //! - `assert_csv_snapshot!` for comparing CSV serialized output of //! types implementing `serde::Serialize`. (requires the `csv` feature) //! - `assert_toml_snapshot!` for comparing TOML serialized output of //! types implementing `serde::Serialize`. (requires the `toml` feature) //! - `assert_yaml_snapshot!` for comparing YAML serialized //! output of types implementing `serde::Serialize`. //! - `assert_ron_snapshot!` for comparing RON serialized output of //! types implementing `serde::Serialize`. (requires the `ron` feature) //! - `assert_json_snapshot!` for comparing JSON serialized output of //! types implementing `serde::Serialize`. //! //! Snapshots are stored in the `snapshots` folder right next to the test file //! where this is used. The name of the file is `__.snap` where //! the `name` of the snapshot. Snapshots can either be explicitly named or the //! name is derived from the test name. //! //! Additionally snapshots can also be stored inline. In that case the //! [`cargo-insta`](https://crates.io/crates/cargo-insta) tool is necessary. //! See [inline snapshots](#inline-snapshots) for more information. //! //! For macros that work with `serde::Serialize` this crate also permits //! redacting of partial values. See [redactions](#redactions) for more //! information. //! //! # Snapshot files //! //! The committed snapshot files will have a header with some meta information //! that can make debugging easier and the snapshot: //! //! ```text //! --- //! expression: "vec![1, 2, 3]" //! source: tests/test_basic.rs //! --- //! [ //! 1, //! 2, //! 3 //! ] //! ``` //! //! # Snapshot updating //! //! During test runs snapshots will be updated according to the `INSTA_UPDATE` //! environment variable. The default is `auto` which will write all new //! snapshots into `.snap.new` files if no CI is detected so that `cargo-insta` //! can pick them up. Normally you don't have to change this variable. //! //! `INSTA_UPDATE` modes: //! //! - `auto`: the default. `no` for CI environments or `new` otherwise //! - `always`: overwrites old snapshot files with new ones unasked //! - `unseen`: behaves like `always` for new snapshots and `new` for others //! - `new`: write new snapshots into `.snap.new` files //! - `no`: does not update snapshot files at all (just runs tests) //! //! When `new` or `auto` is used as mode the `cargo-insta` command can be used //! to review the snapshots conveniently: //! //! ```text //! $ cargo install cargo-insta //! $ cargo test //! $ cargo insta review //! ``` //! //! "enter" or "a" accepts a new snapshot, "escape" or "r" rejects, //! "space" or "s" skips the snapshot for now. //! //! For more information invoke `cargo insta --help`. //! //! # Test assertions //! //! By default the tests will fail when the snapshot assertion fails. However //! if a test produces more than one snapshot it can be useful to force a test //! to pass so that all new snapshots are created in one go. //! //! This can be enabled by setting `INSTA_FORCE_PASS` to `1`: //! //! ```text //! $ INSTA_FORCE_PASS=1 cargo test --no-fail-fast //! ``` //! //! A better way to do this is to run `cargo insta test --review` which will //! run all tests with force pass and then bring up the review tool: //! //! ```text //! $ cargo insta test --review //! ``` //! //! # Named snapshots //! //! All snapshot assertion functions let you leave out the snapshot name in //! which case the snapshot name is derived from the test name (with an optional //! leading `test_` prefix removed. //! //! This works because the rust test runner names the thread by the test name //! and the name is taken from the thread name. In case your test spawns additional //! threads this will not work and you will need to provide a name explicitly. //! There are some situations in which rust test does not name or use threads. //! In these cases insta will panic with an error. The `backtrace` feature can //! be enabled in which case insta will attempt to recover the test name from //! the backtrace. //! //! Explicit snapshot naming can also otherwise be useful to be more explicit //! when multiple snapshots are tested within one function as the default //! behavior would be to just count up the snapshot names. //! //! To provide an explicit name provide the name of the snapshot as first //! argument to the macro: //! //! ```no_run //! #[test] //! fn test_something() { //! assert_snapshot!("first_snapshot", "first value"); //! assert_snapshot!("second_snapshot", "second value"); //! } //! ``` //! //! This will create two snapshots: `first_snapshot` for the first value and //! `second_snapshot` for the second value. Without explicit naming the //! snapshots would be called `something` and `something-2`. //! //! # Test Output Control //! //! Insta by default will output quite a lot of information as tests run. For //! instance it will print out all the diffs. This can be controlled by setting //! the `INSTA_OUTPUT` environment variable. The following values are possible: //! //! * `diff` (default): prints the diffs //! * `summary`: prints only summaries (name of snapshot files etc.) //! * `minimal`: like `summary` but more minimal //! * `none`: insta will not output any extra information //! //! # Redactions //! //! **Feature:** `redactions` //! //! For all snapshots created based on `serde::Serialize` output `insta` //! supports redactions. This permits replacing values with hardcoded other //! values to make snapshots stable when otherwise random or otherwise changing //! values are involved. Redactions became an optional feature in insta //! 0.11 and can be enabled with the `redactions` feature. //! //! Redactions can be defined as the third argument to those macros with //! the syntax `{ selector => replacement_value }`. //! //! The following selectors exist: //! //! - `.key`: selects the given key //! - `["key"]`: alternative syntax for keys //! - `[index]`: selects the given index in an array //! - `[]`: selects all items on an array //! - `[:end]`: selects all items up to `end` (excluding, supports negative indexing) //! - `[start:]`: selects all items starting with `start` //! - `[start:end]`: selects all items from `start` to `end` (end excluding, //! supports negative indexing). //! - `.*`: selects all keys on that depth //! - `.**`: performs a deep match (zero or more items). Can only be used once. //! //! Example usage: //! //! ```no_run //! # #[cfg(feature = "redactions")] { //! # use insta::*; use serde::Serialize; use std::collections::HashMap; //! # #[derive(Serialize)] struct Uuid; impl Uuid { fn new_v4() -> Self { Uuid } } //! #[derive(Serialize)] //! pub struct User { //! id: Uuid, //! username: String, //! extra: HashMap, //! } //! //! assert_yaml_snapshot!(&User { //! id: Uuid::new_v4(), //! username: "john_doe".to_string(), //! extra: { //! let mut map = HashMap::new(); //! map.insert("ssn".to_string(), "123-123-123".to_string()); //! map //! }, //! }, { //! ".id" => "[uuid]", //! ".extra.ssn" => "[ssn]" //! }); //! # } //! ``` //! //! It's also possible to execute a callback that can produce a new value //! instead of hardcoding a replacement value by using the //! [`dynamic_redaction`] function: //! //! ```no_run //! # #[cfg(feature = "redactions")] { //! # use insta::*; use serde::Serialize; //! # #[derive(Serialize)] struct Uuid; impl Uuid { fn new_v4() -> Self { Uuid } } //! # #[derive(Serialize)] //! # pub struct User { //! # id: Uuid, //! # username: String, //! # } //! assert_yaml_snapshot!(&User { //! id: Uuid::new_v4(), //! username: "john_doe".to_string(), //! }, { //! ".id" => dynamic_redaction(|value, _| { //! // assert that the value looks like a uuid here //! "[uuid]" //! }), //! }); //! # } //! ``` //! //! # Globbing //! //! **Feature:** `glob` //! //! Sometimes it can be useful to run code against multiple input files. //! The easiest way to accomplish this is to use the `glob!` macro which //! runs a closure for each input path that matches. Before the closure //! is executed the settings are updated to set a reference to the input //! file and the appropriate snapshot suffix. //! //! Example: //! //! ```rust,ignore //! use std::fs; //! //! glob!("inputs/*.txt", |path| { //! let input = fs::read_to_string(path).unwrap(); //! assert_json_snapshot!(input.to_uppercase()); //! }); //! ``` //! //! The path to the glob macro is relative to the location of the test //! file. It uses the [`globwalk`] crate for actual glob operations. //! //! # Inline Snapshots //! //! Additionally snapshots can also be stored inline. In that case the format //! for the snapshot macros is `assert_snapshot!(reference_value, @"snapshot")`. //! The leading at sign (`@`) indicates that the following string is the //! reference value. `cargo-insta` will then update that string with the new //! value on review. //! //! Example: //! //! ```no_run //! # use insta::*; use serde::Serialize; //! #[derive(Serialize)] //! pub struct User { //! username: String, //! } //! //! assert_yaml_snapshot!(User { //! username: "john_doe".to_string(), //! }, @""); //! ``` //! //! After the initial test failure you can run `cargo insta review` to //! accept the change. The file will then be updated automatically. //! //! # Features //! //! The following features exist: //! //! * `csv`: enables CSV support ([`assert_csv_snapshot!`]) //! * `ron`: enables RON support ([`assert_ron_snapshot!`]) //! * `toml`: enables TOML support ([`assert_toml_snapshot!`]) //! * `redactions`: enables support for redactions //! * `glob`: enables support for globbing ([`glob!`]) //! * `colors`: enables color output (enabled by default) //! //! # Settings //! //! There are some settings that can be changed on a per-thread (and thus //! per-test) basis. For more information see [Settings]. //! //! # Legacy Snapshot Formats //! //! With insta 0.11 the snapshot format was improved for inline snapshots. The //! old snapshot format will continue to be available but if you want to upgrade //! them make sure the tests pass first and then run the following command //! to force a rewrite of them all: //! //! ```text //! $ cargo insta test --accept --force-update-snapshots //! ``` //! //! # Deleting Unused Snapshots //! //! Insta cannot detect unused snapshot files. The reason for this is that //! insta does not control the execution of the entirety of the tests so it //! cannot spot which files are actually unreferenced. However you can use //! the `INSTA_SNAPSHOT_REFERENCES_FILE` environment variable to //! instruct insta to append all referenced files into a list. This can then //! be used to delete all files not referenced. For instance one could use //! [ripgrep](https://github.com/BurntSushi/ripgrep) like this: //! //! ```text //! export INSTA_SNAPSHOT_REFERENCES_FILE="$(mktemp)" //! cargo test //! rg --files -lg '*.snap' "$(pwd)" | grep -vFf "$INSTA_SNAPSHOT_REFERENCES_FILE" | xargs rm //! rm -f $INSTA_SNAPSHOT_REFERENCES_FILE //! ``` #[macro_use] mod macros; mod content; mod runtime; mod serialization; mod settings; mod snapshot; mod utils; #[cfg(feature = "redactions")] mod redaction; #[cfg(feature = "glob")] mod glob; #[cfg(test)] mod test; pub use crate::settings::Settings; pub use crate::snapshot::{MetaData, Snapshot}; /// Exposes some library internals. /// /// You're unlikely to want to work with these objects but they /// are exposed for documentation primarily. pub mod internals { pub use crate::content::Content; pub use crate::runtime::AutoName; pub use crate::snapshot::{MetaData, SnapshotContents}; #[cfg(feature = "redactions")] pub use crate::{ redaction::{ContentPath, Redaction}, settings::Redactions, }; } // exported for cargo-insta only #[doc(hidden)] pub use crate::{ runtime::print_snapshot_diff, snapshot::PendingInlineSnapshot, snapshot::SnapshotContents, }; // useful for redactions #[cfg(feature = "redactions")] pub use crate::redaction::dynamic_redaction; // these are here to make the macros work #[doc(hidden)] pub mod _macro_support { pub use crate::content::Content; pub use crate::runtime::{assert_snapshot, get_cargo_workspace, AutoName, ReferenceValue}; pub use crate::serialization::{serialize_value, SerializationFormat, SnapshotLocation}; #[cfg(feature = "glob")] pub use crate::glob::glob_exec; #[cfg(feature = "redactions")] pub use crate::{ redaction::Redaction, redaction::Selector, serialization::serialize_value_redacted, }; } insta-1.3.0/src/macros.rs010064400007650000024000000406261375551357500134610ustar 00000000000000/// Asserts a `Serialize` snapshot in CSV format. /// /// **Feature:** `csv` (disabled by default) /// /// This works exactly like [`assert_yaml_snapshot!`] /// but serializes in [CSV](https://github.com/burntsushi/rust-csv) format instead of /// YAML. /// /// Example: /// /// ```no_run,ignore /// assert_csv_snapshot!(vec[1, 2, 3]); /// ``` /// /// The third argument to the macro can be an object expression for redaction. /// It's in the form `{ selector => replacement }`. For more information /// about redactions see [redactions](index.html#redactions). /// /// The snapshot name is optional but can be provided as first argument. /// For more information see [named snapshots](index.html#named-snapshots) #[cfg(feature = "csv")] #[macro_export] macro_rules! assert_csv_snapshot { ($value:expr, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, Csv, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, {$($k => $v),*}, Csv, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, {$($k => $v),*}, Csv); }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, Csv); }}; ($name:expr, $value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, {$($k => $v),*}, Csv); }}; ($value:expr) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, Csv); }}; } /// Asserts a `Serialize` snapshot in TOML format. /// /// **Feature:** `toml` (disabled by default) /// /// This works exactly like [`assert_toml_snapshot!`] /// but serializes in [TOML](https://github.com/alexcrichton/toml-rs) format instead of /// YAML. Note that TOML cannot represent all values due to limitations in the /// format. /// /// Example: /// /// ```no_run,ignore /// assert_toml_snapshot!(vec[1, 2, 3]); /// ``` /// /// The third argument to the macro can be an object expression for redaction. /// It's in the form `{ selector => replacement }`. For more information /// about redactions see [redactions](index.html#redactions). /// /// The snapshot name is optional but can be provided as first argument. /// For more information see [named snapshots](index.html#named-snapshots) #[cfg(feature = "toml")] #[macro_export] macro_rules! assert_toml_snapshot { ($value:expr, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, Toml, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, {$($k => $v),*}, Toml, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, {$($k => $v),*}, Toml); }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, Toml); }}; ($name:expr, $value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, {$($k => $v),*}, Toml); }}; ($value:expr) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, Toml); }}; } /// Asserts a `Serialize` snapshot in YAML format. /// /// The value needs to implement the `serde::Serialize` trait and the snapshot /// will be serialized in YAML format. This does mean that unlike the debug /// snapshot variant the type of the value does not appear in the output. /// You can however use the `assert_ron_snapshot!` macro to dump out /// the value in [RON](https://github.com/ron-rs/ron/) format which retains some /// type information for more accurate comparisions. /// /// Example: /// /// ```no_run /// # use insta::*; /// assert_yaml_snapshot!(vec![1, 2, 3]); /// ``` /// /// Unlike the [`assert_debug_snapshot!`] /// macro, this one has a secondary mode where redactions can be defined. /// /// The third argument to the macro can be an object expression for redaction. /// It's in the form `{ selector => replacement }`. For more information /// about redactions see [redactions](index.html#redactions). /// /// Example: /// #[cfg_attr(feature = "redactions", doc = " ```no_run")] #[cfg_attr(not(feature = "redactions"), doc = " ```ignore")] /// # use insta::*; use serde::Serialize; /// # #[derive(Serialize)] struct Value; let value = Value; /// assert_yaml_snapshot!(value, { /// ".key.to.redact" => "[replacement value]", /// ".another.key.*.to.redact" => 42 /// }); /// ``` /// /// The replacement value can be a string, integer or any other primitive value. /// /// For inline usage the format is `(expression, @reference_value)` where the /// reference value must be a string literal. If you make the initial snapshot /// just use an empty string (`@""`). For more information see /// [inline snapshots](index.html#inline-snapshots). /// /// The snapshot name is optional but can be provided as first argument. /// For more information see [named snapshots](index.html#named-snapshots) #[macro_export] macro_rules! assert_yaml_snapshot { ($value:expr, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, Yaml, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, {$($k => $v),*}, Yaml, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, {$($k => $v),*}, Yaml); }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, Yaml); }}; ($name:expr, $value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, {$($k => $v),*}, Yaml); }}; ($value:expr) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, Yaml); }}; } /// Asserts a `Serialize` snapshot in RON format. /// /// **Feature:** `ron` (disabled by default) /// /// This works exactly like [`assert_yaml_snapshot!`] /// but serializes in [RON](https://github.com/ron-rs/ron/) format instead of /// YAML which retains some type information for more accurate comparisions. /// /// Example: /// /// ```no_run /// # use insta::*; /// assert_ron_snapshot!(vec![1, 2, 3]); /// ``` /// /// The third argument to the macro can be an object expression for redaction. /// It's in the form `{ selector => replacement }`. For more information /// about redactions see [redactions](index.html#redactions). /// /// The snapshot name is optional but can be provided as first argument. /// For more information see [named snapshots](index.html#named-snapshots) #[cfg(feature = "ron")] #[macro_export] macro_rules! assert_ron_snapshot { ($value:expr, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, Ron, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, {$($k => $v),*}, Ron, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, {$($k => $v),*}, Ron); }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, Ron); }}; ($name:expr, $value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, {$($k => $v),*}, Ron); }}; ($value:expr) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, Ron); }}; } /// Asserts a `Serialize` snapshot in JSON format. /// /// This works exactly like [`assert_yaml_snapshot!`] but serializes in JSON format. /// This is normally not recommended because it makes diffs less reliable, but it can /// be useful for certain specialized situations. /// /// Example: /// /// ```no_run /// # use insta::*; /// assert_json_snapshot!(vec![1, 2, 3]); /// ``` /// /// The third argument to the macro can be an object expression for redaction. /// It's in the form `{ selector => replacement }`. For more information /// about redactions see [redactions](index.html#redactions). /// /// The snapshot name is optional but can be provided as first argument. /// For more information see [named snapshots](index.html#named-snapshots) #[macro_export] macro_rules! assert_json_snapshot { ($value:expr, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, Json, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}, @$snapshot:literal) => {{ $crate::_assert_serialized_snapshot!($value, {$($k => $v),*}, Json, @$snapshot); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, {$($k => $v),*}, Json); }}; ($name:expr, $value:expr) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, Json); }}; ($name:expr, $value:expr, {$($k:expr => $v:expr),*$(,)?}) => {{ $crate::_assert_serialized_snapshot!(Some($name), $value, {$($k => $v),*}, Json); }}; ($value:expr) => {{ $crate::_assert_serialized_snapshot!($crate::_macro_support::AutoName, $value, Json); }}; } #[doc(hidden)] #[macro_export] macro_rules! _assert_serialized_snapshot { ($value:expr, $format:ident, @$snapshot:literal) => {{ let value = $crate::_macro_support::serialize_value( &$value, $crate::_macro_support::SerializationFormat::$format, $crate::_macro_support::SnapshotLocation::Inline ); $crate::assert_snapshot!( value, stringify!($value), @$snapshot ); }}; ($value:expr, {$($k:expr => $v:expr),*$(,)?}, $format:ident, @$snapshot:literal) => {{ let (vec, value) = $crate::_prepare_snapshot_for_redaction!($value, {$($k => $v),*}, $format, Inline); $crate::assert_snapshot!(value, stringify!($value), @$snapshot); }}; ($name:expr, $value:expr, $format:ident) => {{ let value = $crate::_macro_support::serialize_value( &$value, $crate::_macro_support::SerializationFormat::$format, $crate::_macro_support::SnapshotLocation::File ); $crate::assert_snapshot!( $name, value, stringify!($value) ); }}; ($name:expr, $value:expr, {$($k:expr => $v:expr),*$(,)?}, $format:ident) => {{ let (vec, value) = $crate::_prepare_snapshot_for_redaction!($value, {$($k => $v),*}, $format, File); $crate::assert_snapshot!($name, value, stringify!($value)); }} } #[cfg(feature = "redactions")] #[doc(hidden)] #[macro_export] macro_rules! _prepare_snapshot_for_redaction { ($value:expr, {$($k:expr => $v:expr),*$(,)?}, $format:ident, $location:ident) => { { let vec = vec![ $(( $crate::_macro_support::Selector::parse($k).unwrap(), $crate::_macro_support::Redaction::from($v) ),)* ]; let value = $crate::_macro_support::serialize_value_redacted( &$value, &vec, $crate::_macro_support::SerializationFormat::$format, $crate::_macro_support::SnapshotLocation::$location ); (vec, value) } } } #[cfg(not(feature = "redactions"))] #[doc(hidden)] #[macro_export] macro_rules! _prepare_snapshot_for_redaction { ($value:expr, {$($k:expr => $v:expr),*$(,)?}, $format:ident, $location:ident) => { compile_error!("insta was compiled without redaction support."); }; } /// Asserts a `Debug` snapshot. /// /// The value needs to implement the `fmt::Debug` trait. This is useful for /// simple values that do not implement the `Serialize` trait but does not /// permit redactions. /// /// Additionally the name is optional. For more information see /// [unnamed snapshots](index.html#unnamed-snapshots) #[macro_export] macro_rules! assert_debug_snapshot { ($value:expr, @$snapshot:literal) => {{ let value = format!("{:#?}", $value); $crate::assert_snapshot!(value, stringify!($value), @$snapshot); }}; ($name:expr, $value:expr) => {{ let value = format!("{:#?}", $value); $crate::assert_snapshot!(Some($name), value, stringify!($value)); }}; ($value:expr) => {{ let value = format!("{:#?}", $value); $crate::assert_snapshot!($crate::_macro_support::AutoName, value, stringify!($value)); }}; } /// Asserts a `Display` snapshot. /// /// The value needs to implement the `fmt::Display` trait. /// /// Additionally the name is optional. For more information see /// [unnamed snapshots](index.html#unnamed-snapshots) #[macro_export] macro_rules! assert_display_snapshot { ($value:expr, @$snapshot:literal) => {{ let value = format!("{}", $value); $crate::assert_snapshot!(value, stringify!($value), @$snapshot); }}; ($name:expr, $value:expr) => {{ let value = format!("{}", $value); $crate::assert_snapshot!(Some($name), value, stringify!($value)); }}; ($value:expr) => {{ let value = format!("{}", $value); $crate::assert_snapshot!($crate::_macro_support::AutoName, value, stringify!($value)); }}; } /// Asserts a string snapshot. /// /// This is the most simplistic of all assertion methods. It just accepts /// a string to store as snapshot an does not apply any other transformations /// on it. This is useful to build ones own primitives. /// /// ```no_run /// # use insta::*; /// assert_snapshot!("reference value to snapshot"); /// ``` /// /// Optionally a third argument can be given as expression which will be /// stringified as debug expression. For more information on this look at the /// source of this macro and other assertion macros. /// /// Additionally the name is optional. For more information see /// [unnamed snapshots](index.html#unnamed-snapshots) #[macro_export] macro_rules! assert_snapshot { ($value:expr, @$snapshot:literal) => { $crate::assert_snapshot!( $crate::_macro_support::ReferenceValue::Inline($snapshot), $value, stringify!($value) ) }; ($value:expr, $debug_expr:expr, @$snapshot:literal) => { $crate::assert_snapshot!( $crate::_macro_support::ReferenceValue::Inline($snapshot), $value, $debug_expr ) }; ($name:expr, $value:expr) => { $crate::assert_snapshot!($name, $value, stringify!($value)) }; ($name:expr, $value:expr, $debug_expr:expr) => { $crate::_macro_support::assert_snapshot( // Creates a ReferenceValue::Named variant $name.into(), &$value, env!("CARGO_MANIFEST_DIR"), module_path!(), file!(), line!(), $debug_expr, ) .unwrap(); }; ($value:expr) => { $crate::assert_snapshot!($crate::_macro_support::AutoName, $value, stringify!($value)) }; } /// Settings configuration macro. /// /// This macro lets you bind some settings temporarily. The first argument /// takes key value pairs that should be set, the second is the block to /// execute. All settings can be set (`sort_maps => value` maps roughly /// to `set_sort_maps(value)`). /// /// ```rust /// insta::with_settings!({sort_maps => true}, { /// // run snapshot test here /// }); /// ``` #[macro_export] macro_rules! with_settings { ({$($k:ident => $v:expr),*$(,)?}, $body:block) => {{ let mut settings = $crate::Settings::new(); $( settings._private_inner_mut().$k = $v.into(); )* settings.bind(|| $body) }} } /// Executes a closure for all input files matching a glob. /// /// The closure is passed the path to the file. #[cfg(feature = "glob")] #[macro_export] macro_rules! glob { ($glob:expr, $closure:expr) => {{ let base = $crate::_macro_support::get_cargo_workspace(env!("CARGO_MANIFEST_DIR")) .join(file!()) .parent() .unwrap() .canonicalize() .unwrap_or_else(|e| panic!("failed to canonicalize insta::glob! base path: {}", e)); $crate::_macro_support::glob_exec(&base, $glob, $closure); }}; } insta-1.3.0/src/redaction.rs010064400007650000024000000402651375551511400141330ustar 00000000000000use pest::Parser; use pest_derive::Parser; use std::borrow::Cow; use std::fmt; use crate::content::Content; #[derive(Debug)] pub struct SelectorParseError(pest::error::Error); impl SelectorParseError { /// Return the column of where the error ocurred. pub fn column(&self) -> usize { match self.0.line_col { pest::error::LineColLocation::Pos((_, col)) => col, pest::error::LineColLocation::Span((_, col), _) => col, } } } /// Represents a path for a callback function. /// /// This can be converted into a string with `to_string` to see a stringified /// path that the selector matched. #[derive(Clone, Debug)] pub struct ContentPath<'a>(&'a [PathItem]); impl<'a> fmt::Display for ContentPath<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for item in self.0.iter() { write!(f, ".")?; match *item { PathItem::Content(ref ctx) => { if let Some(s) = ctx.as_str() { write!(f, "{}", s)?; } else { write!(f, "")?; } } PathItem::Field(name) => write!(f, "{}", name)?, PathItem::Index(idx, _) => write!(f, "{}", idx)?, } } Ok(()) } } /// Replaces a value with another one. /// Represents a redaction. pub enum Redaction { /// Static redaction with new content. Static(Content), /// Redaction with new content. Dynamic(Box) -> Content + Sync + Send>), } macro_rules! impl_from { ($ty:ty) => { impl From<$ty> for Redaction { fn from(value: $ty) -> Redaction { Redaction::Static(Content::from(value)) } } }; } impl_from!(()); impl_from!(bool); impl_from!(u8); impl_from!(u16); impl_from!(u32); impl_from!(u64); impl_from!(i8); impl_from!(i16); impl_from!(i32); impl_from!(i64); impl_from!(f32); impl_from!(f64); impl_from!(char); impl_from!(String); impl_from!(Vec); impl<'a> From<&'a str> for Redaction { fn from(value: &'a str) -> Redaction { Redaction::Static(Content::from(value)) } } impl<'a> From<&'a [u8]> for Redaction { fn from(value: &'a [u8]) -> Redaction { Redaction::Static(Content::from(value)) } } /// Creates a dynamic redaction. /// /// This can be used to redact a value with a different value but instead of /// statically declaring it a dynamic value can be computed. This can also /// be used to perform assertions before replacing the value. /// /// The closure is passed two arguments: the value as [`Content`] /// and the path that was selected (as [`ContentPath`]) /// /// Example: /// /// ```rust /// # use insta::{Settings, dynamic_redaction}; /// # let mut settings = Settings::new(); /// settings.add_redaction(".id", dynamic_redaction(|value, path| { /// assert_eq!(path.to_string(), ".id"); /// assert_eq!( /// value /// .as_str() /// .unwrap() /// .chars() /// .filter(|&c| c == '-') /// .count(), /// 4 /// ); /// "[uuid]" /// })); /// ``` pub fn dynamic_redaction(func: F) -> Redaction where I: Into, F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static, { Redaction::Dynamic(Box::new(move |c, p| func(c, p).into())) } impl Redaction { /// Performs the redaction of the value at the given path. fn redact(&self, value: Content, path: &[PathItem]) -> Content { match *self { Redaction::Static(ref new_val) => new_val.clone(), Redaction::Dynamic(ref callback) => callback(value, ContentPath(path)), } } } #[derive(Parser)] #[grammar = "select_grammar.pest"] pub struct SelectParser; #[derive(Debug)] pub enum PathItem { Content(Content), Field(&'static str), Index(u64, u64), } impl PathItem { fn as_str(&self) -> Option<&str> { match *self { PathItem::Content(ref content) => content.as_str(), PathItem::Field(s) => Some(s), PathItem::Index(..) => None, } } fn as_u64(&self) -> Option { match *self { PathItem::Content(ref content) => content.as_u64(), PathItem::Field(_) => None, PathItem::Index(idx, _) => Some(idx), } } fn range_check(&self, start: Option, end: Option) -> bool { fn expand_range(sel: i64, len: i64) -> i64 { if sel < 0 { (len + sel).max(0) } else { sel } } let (idx, len) = match *self { PathItem::Index(idx, len) => (idx as i64, len as i64), _ => return false, }; match (start, end) { (None, None) => true, (None, Some(end)) => idx < expand_range(end, len), (Some(start), None) => idx >= expand_range(start, len), (Some(start), Some(end)) => { idx >= expand_range(start, len) && idx < expand_range(end, len) } } } } #[derive(Debug, Clone, PartialEq)] pub enum Segment<'a> { DeepWildcard, Wildcard, Key(Cow<'a, str>), Index(u64), Range(Option, Option), } #[derive(Debug, Clone)] pub struct Selector<'a> { selectors: Vec>>, } impl<'a> Selector<'a> { pub fn parse(selector: &'a str) -> Result, SelectorParseError> { let pair = SelectParser::parse(Rule::selectors, selector) .map_err(SelectorParseError)? .next() .unwrap(); let mut rv = vec![]; for selector_pair in pair.into_inner() { match selector_pair.as_rule() { Rule::EOI => break, other => assert_eq!(other, Rule::selector), } let mut segments = vec![]; let mut have_deep_wildcard = false; for segment_pair in selector_pair.into_inner() { segments.push(match segment_pair.as_rule() { Rule::identity => continue, Rule::wildcard => Segment::Wildcard, Rule::deep_wildcard => { if have_deep_wildcard { return Err(SelectorParseError(pest::error::Error::new_from_span( pest::error::ErrorVariant::CustomError { message: "deep wildcard used twice".into(), }, segment_pair.as_span(), ))); } have_deep_wildcard = true; Segment::DeepWildcard } Rule::key => Segment::Key(Cow::Borrowed(&segment_pair.as_str()[1..])), Rule::subscript => { let subscript_rule = segment_pair.into_inner().next().unwrap(); match subscript_rule.as_rule() { Rule::int => Segment::Index(subscript_rule.as_str().parse().unwrap()), Rule::string => { let sq = subscript_rule.as_str(); let s = &sq[1..sq.len() - 1]; let mut was_backslash = false; Segment::Key(if s.bytes().any(|x| x == b'\\') { Cow::Owned( s.chars() .filter_map(|c| { let rv = match c { '\\' if !was_backslash => { was_backslash = true; return None; } other => other, }; was_backslash = false; Some(rv) }) .collect(), ) } else { Cow::Borrowed(s) }) } _ => unreachable!(), } } Rule::full_range => Segment::Range(None, None), Rule::range => { let mut int_rule = segment_pair .into_inner() .map(|x| x.as_str().parse().unwrap()); Segment::Range(int_rule.next(), int_rule.next()) } Rule::range_to => { let int_rule = segment_pair.into_inner().next().unwrap(); Segment::Range(None, int_rule.as_str().parse().ok()) } Rule::range_from => { let int_rule = segment_pair.into_inner().next().unwrap(); Segment::Range(int_rule.as_str().parse().ok(), None) } _ => unreachable!(), }); } rv.push(segments); } Ok(Selector { selectors: rv }) } pub fn make_static(self) -> Selector<'static> { Selector { selectors: self .selectors .into_iter() .map(|parts| { parts .into_iter() .map(|x| match x { Segment::Key(x) => Segment::Key(Cow::Owned(x.into_owned())), Segment::Index(x) => Segment::Index(x), Segment::Wildcard => Segment::Wildcard, Segment::DeepWildcard => Segment::DeepWildcard, Segment::Range(a, b) => Segment::Range(a, b), }) .collect() }) .collect(), } } fn segment_is_match(&self, segment: &Segment, element: &PathItem) -> bool { match *segment { Segment::Wildcard => true, Segment::DeepWildcard => true, Segment::Key(ref k) => element.as_str() == Some(&k), Segment::Index(i) => element.as_u64() == Some(i), Segment::Range(start, end) => element.range_check(start, end), } } fn selector_is_match(&self, selector: &[Segment], path: &[PathItem]) -> bool { if let Some(idx) = selector.iter().position(|x| *x == Segment::DeepWildcard) { let forward_sel = &selector[..idx]; let backward_sel = &selector[idx + 1..]; if path.len() <= idx { return false; } for (segment, element) in forward_sel.iter().zip(path.iter()) { if !self.segment_is_match(segment, element) { return false; } } for (segment, element) in backward_sel.iter().rev().zip(path.iter().rev()) { if !self.segment_is_match(segment, element) { return false; } } true } else { if selector.len() != path.len() { return false; } for (segment, element) in selector.iter().zip(path.iter()) { if !self.segment_is_match(segment, element) { return false; } } true } } pub fn is_match(&self, path: &[PathItem]) -> bool { for selector in &self.selectors { if self.selector_is_match(&selector, path) { return true; } } false } pub fn redact(&self, value: Content, redaction: &Redaction) -> Content { self.redact_impl(value, redaction, &mut vec![]) } fn redact_seq( &self, seq: Vec, redaction: &Redaction, path: &mut Vec, ) -> Vec { let len = seq.len(); seq.into_iter() .enumerate() .map(|(idx, value)| { path.push(PathItem::Index(idx as u64, len as u64)); let new_value = self.redact_impl(value, redaction, path); path.pop(); new_value }) .collect() } fn redact_struct( &self, seq: Vec<(&'static str, Content)>, redaction: &Redaction, path: &mut Vec, ) -> Vec<(&'static str, Content)> { seq.into_iter() .map(|(key, value)| { path.push(PathItem::Field(key)); let new_value = self.redact_impl(value, redaction, path); path.pop(); (key, new_value) }) .collect() } fn redact_impl( &self, value: Content, redaction: &Redaction, path: &mut Vec, ) -> Content { if self.is_match(&path) { redaction.redact(value, path) } else { match value { Content::Map(map) => Content::Map( map.into_iter() .map(|(key, value)| { path.push(PathItem::Content(key.clone())); let new_value = self.redact_impl(value, redaction, path); path.pop(); (key, new_value) }) .collect(), ), Content::Seq(seq) => Content::Seq(self.redact_seq(seq, redaction, path)), Content::Tuple(seq) => Content::Tuple(self.redact_seq(seq, redaction, path)), Content::TupleStruct(name, seq) => { Content::TupleStruct(name, self.redact_seq(seq, redaction, path)) } Content::TupleVariant(name, variant_index, variant, seq) => Content::TupleVariant( name, variant_index, variant, self.redact_seq(seq, redaction, path), ), Content::Struct(name, seq) => { Content::Struct(name, self.redact_struct(seq, redaction, path)) } Content::StructVariant(name, variant_index, variant, seq) => { Content::StructVariant( name, variant_index, variant, self.redact_struct(seq, redaction, path), ) } Content::NewtypeStruct(name, inner) => Content::NewtypeStruct( name, Box::new(self.redact_impl(*inner, redaction, path)), ), Content::NewtypeVariant(name, index, variant_name, inner) => { Content::NewtypeVariant( name, index, variant_name, Box::new(self.redact_impl(*inner, redaction, path)), ) } Content::Some(contents) => { Content::Some(Box::new(self.redact_impl(*contents, redaction, path))) } other => other, } } } } #[test] fn test_range_checks() { assert_eq!(PathItem::Index(0, 10).range_check(None, Some(-1)), true); assert_eq!(PathItem::Index(9, 10).range_check(None, Some(-1)), false); assert_eq!(PathItem::Index(0, 10).range_check(Some(1), Some(-1)), false); assert_eq!(PathItem::Index(1, 10).range_check(Some(1), Some(-1)), true); assert_eq!(PathItem::Index(9, 10).range_check(Some(1), Some(-1)), false); assert_eq!(PathItem::Index(0, 10).range_check(Some(1), None), false); assert_eq!(PathItem::Index(1, 10).range_check(Some(1), None), true); assert_eq!(PathItem::Index(9, 10).range_check(Some(1), None), true); } insta-1.3.0/src/runtime.rs010064400007650000024000000744041374651470200136520ustar 00000000000000use std::borrow::Cow; use std::collections::BTreeMap; use std::env; use std::error::Error; use std::fmt; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::str; use std::sync::Mutex; use std::thread; use difference::{Changeset, Difference}; use lazy_static::lazy_static; use serde::Deserialize; use crate::settings::Settings; use crate::snapshot::{MetaData, PendingInlineSnapshot, Snapshot, SnapshotContents}; use crate::utils::{is_ci, style}; lazy_static! { static ref WORKSPACES: Mutex> = Mutex::new(BTreeMap::new()); static ref TEST_NAME_COUNTERS: Mutex> = Mutex::new(BTreeMap::new()); } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum UpdateBehavior { InPlace, NewFile, NoUpdate, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum OutputBehavior { Diff, Summary, Minimal, Nothing, } #[cfg(windows)] fn path_to_storage>(path: P) -> String { path.as_ref().to_str().unwrap().replace('\\', "/") } #[cfg(not(windows))] fn path_to_storage>(path: P) -> String { path.as_ref().to_string_lossy().into() } fn term_width() -> usize { #[cfg(feature = "colors")] { console::Term::stdout().size().1 as usize } #[cfg(not(feature = "colors"))] { 74 } } fn format_rust_expression(value: &str) -> Cow<'_, str> { const PREFIX: &str = "const x:() = "; const SUFFIX: &str = ";\n"; if let Ok(mut proc) = Command::new("rustfmt") .arg("--emit=stdout") .arg("--edition=2018") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() { { let stdin = proc.stdin.as_mut().unwrap(); stdin.write_all(PREFIX.as_bytes()).unwrap(); stdin.write_all(value.as_bytes()).unwrap(); stdin.write_all(SUFFIX.as_bytes()).unwrap(); } if let Ok(output) = proc.wait_with_output() { if output.status.success() { // slice between after the prefix and before the suffix // (currently 14 from the start and 2 before the end, respectively) let start = PREFIX.len() + 1; let end = output.stdout.len() - SUFFIX.len(); return str::from_utf8(&output.stdout[start..end]) .unwrap() .to_owned() .into(); } } } Cow::Borrowed(value) } #[test] fn test_format_rust_expression() { use crate::assert_snapshot; assert_snapshot!(format_rust_expression("vec![1,2,3]"), @"vec![1, 2, 3]"); assert_snapshot!(format_rust_expression("vec![1,2,3].iter()"), @"vec![1, 2, 3].iter()"); assert_snapshot!(format_rust_expression(r#" "aoeu""#), @r###""aoeu""###); assert_snapshot!(format_rust_expression(r#" "aoe😄""#), @r###""aoe😄""###); assert_snapshot!(format_rust_expression("😄😄😄😄😄"), @"😄😄😄😄😄") } fn update_snapshot_behavior(unseen: bool) -> UpdateBehavior { match env::var("INSTA_UPDATE").ok().as_deref() { None | Some("") | Some("auto") => { if is_ci() { UpdateBehavior::NoUpdate } else { UpdateBehavior::NewFile } } Some("always") | Some("1") => UpdateBehavior::InPlace, Some("new") => UpdateBehavior::NewFile, Some("unseen") => { if unseen { UpdateBehavior::NewFile } else { UpdateBehavior::InPlace } } Some("no") => UpdateBehavior::NoUpdate, _ => panic!("invalid value for INSTA_UPDATE"), } } fn memoize_snapshot_file(snapshot_file: &Path) { if let Ok(path) = env::var("INSTA_SNAPSHOT_REFERENCES_FILE") { let mut f = fs::OpenOptions::new() .write(true) .append(true) .create(true) .open(path) .unwrap(); f.write_all(format!("{}\n", snapshot_file.display()).as_bytes()) .unwrap(); } } fn output_snapshot_behavior() -> OutputBehavior { match env::var("INSTA_OUTPUT").ok().as_deref() { None | Some("") | Some("diff") => OutputBehavior::Diff, Some("summary") => OutputBehavior::Summary, Some("minimal") => OutputBehavior::Minimal, Some("none") => OutputBehavior::Nothing, _ => panic!("invalid value for INSTA_OUTPUT"), } } fn force_update_snapshots() -> bool { match env::var("INSTA_FORCE_UPDATE_SNAPSHOTS").ok().as_deref() { None | Some("") | Some("0") => false, Some("1") => true, _ => panic!("invalid value for INSTA_FORCE_UPDATE_SNAPSHOTS"), } } fn should_fail_in_tests() -> bool { match env::var("INSTA_FORCE_PASS").ok().as_deref() { None | Some("") | Some("0") => true, Some("1") => false, _ => panic!("invalid value for INSTA_FORCE_PASS"), } } fn get_cargo() -> String { env::var("CARGO") .ok() .unwrap_or_else(|| "cargo".to_string()) } pub fn get_cargo_workspace(manifest_dir: &str) -> &Path { // we really do not care about poisoning here. let mut workspaces = WORKSPACES.lock().unwrap_or_else(|x| x.into_inner()); if let Some(rv) = workspaces.get(manifest_dir) { rv } else { #[derive(Deserialize)] struct Manifest { workspace_root: String, } let output = std::process::Command::new(get_cargo()) .arg("metadata") .arg("--format-version=1") .arg("--no-deps") .current_dir(manifest_dir) .output() .unwrap(); let manifest: Manifest = serde_json::from_slice(&output.stdout).unwrap(); let path = Box::leak(Box::new(PathBuf::from(manifest.workspace_root))); workspaces.insert(manifest_dir.to_string(), path.as_path()); workspaces.get(manifest_dir).unwrap() } } fn print_changeset(changeset: Changeset, expr: Option<&str>) { let Changeset { diffs, .. } = changeset; #[derive(PartialEq, Debug)] enum Mode { Same, Add, Rem, } #[derive(PartialEq, Debug)] enum Lineno { NotPresent, Present(usize), } impl fmt::Display for Lineno { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Lineno::NotPresent => f.pad(""), Lineno::Present(lineno) => fmt::Display::fmt(&lineno, f), } } } let mut lines = vec![]; let mut lineno_a = 1; let mut lineno_b = 1; for diff in diffs.iter() { match *diff { Difference::Same(ref x) => { for line in x.split('\n') { lines.push(( Mode::Same, Lineno::Present(lineno_a), Lineno::Present(lineno_b), line.trim_end(), )); lineno_a += 1; lineno_b += 1; } } Difference::Add(ref x) => { for line in x.split('\n') { lines.push(( Mode::Add, Lineno::NotPresent, Lineno::Present(lineno_b), line.trim_end(), )); lineno_b += 1; } } Difference::Rem(ref x) => { for line in x.split('\n') { lines.push(( Mode::Rem, Lineno::Present(lineno_a), Lineno::NotPresent, line.trim_end(), )); lineno_a += 1; } } } } let width = term_width(); if let Some(expr) = expr { println!("{:─^1$}", "", width,); println!("{}", style(format_rust_expression(expr))); } println!("────────────┬{:─^1$}", "", width.saturating_sub(13),); let mut has_changes = false; for (i, (mode, lineno_a, lineno_b, line)) in lines.iter().enumerate() { match mode { Mode::Add => { has_changes = true; println!( "{:>5} {:>5} │{}{}", style(lineno_a).dim(), style(lineno_b).dim().bold(), style("+").green(), style(line).green() ); } Mode::Rem => { has_changes = true; println!( "{:>5} {:>5} │{}{}", style(lineno_a).dim(), style(lineno_b).dim().bold(), style("-").red(), style(line).red() ); } Mode::Same => { if lines[i.saturating_sub(5)..(i + 5).min(lines.len())] .iter() .any(|x| x.0 != Mode::Same) { println!( "{:>5} {:>5} │ {}", style(lineno_a).dim(), style(lineno_b).dim().bold(), style(line).dim() ); } } } } if !has_changes { println!( "{:>5} {:>5} │{}", "", style("-").dim(), style(" snapshots are matching").cyan(), ); } println!("────────────┴{:─^1$}", "", width.saturating_sub(13),); } pub fn get_snapshot_filename( module_path: &str, snapshot_name: &str, cargo_workspace: &Path, base: &str, ) -> PathBuf { let root = Path::new(cargo_workspace); let base = Path::new(base); Settings::with(|settings| { root.join(base.parent().unwrap()) .join(settings.snapshot_path()) .join({ use std::fmt::Write; let mut f = String::new(); if settings.prepend_module_to_snapshot() { write!(&mut f, "{}__", module_path.replace("::", "__")).unwrap(); } write!( &mut f, "{}.snap", snapshot_name.replace("/", "__").replace("\\", "__") ) .unwrap(); f }) }) } /// Prints the summary of a snapshot pub fn print_snapshot_summary( workspace_root: &Path, snapshot: &Snapshot, snapshot_file: Option<&Path>, line: Option, ) { if let Some(snapshot_file) = snapshot_file { let snapshot_file = workspace_root .join(snapshot_file) .strip_prefix(workspace_root) .ok() .map(|x| x.to_path_buf()) .unwrap_or_else(|| snapshot_file.to_path_buf()); println!( "Snapshot file: {}", style(snapshot_file.display()).cyan().underlined() ); } if let Some(name) = snapshot.snapshot_name() { println!("Snapshot: {}", style(name).yellow()); } else { println!("Snapshot: {}", style("").dim()); } if let Some(ref value) = snapshot.metadata().get_relative_source(workspace_root) { println!( "Source: {}{}", style(value.display()).cyan(), if let Some(line) = line { format!(":{}", style(line).bold()) } else { "".to_string() } ); } if let Some(ref value) = snapshot.metadata().input_file() { println!("Input file: {}", style(value).cyan()); } } /// Calculates `difference::Changeset` of given files. /// Falls back to manually constructed `Changeset` /// for large inputs to avoid OOM in `difference`. fn get_changeset(orig: &str, edit: &str) -> Changeset { const FALLBACK_THRESHOLD: usize = 10_000_000; let orig_cnt = orig.lines().count(); let edited_cnt = edit.lines().count(); if orig_cnt * edited_cnt > FALLBACK_THRESHOLD { // `difference` crate will likely use hundreds of megabytes // possibly leading to OOM. let mut changeset = Changeset { diffs: Vec::new(), // following two fields will not be read anyway split: String::new(), distance: 0, }; for line in orig.lines() { changeset.diffs.push(Difference::Rem(line.to_string())); } for line in edit.lines() { changeset.diffs.push(Difference::Add(line.to_string())); } return changeset; } Changeset::new(orig, edit, "\n") } /// Prints a diff against an old snapshot. pub fn print_snapshot_diff( workspace_root: &Path, new: &Snapshot, old_snapshot: Option<&Snapshot>, snapshot_file: Option<&Path>, line: Option, ) { print_snapshot_summary(workspace_root, new, snapshot_file, line); let changeset = get_changeset( old_snapshot.as_ref().map_or("", |x| x.contents_str()), &new.contents_str(), ); if old_snapshot.is_some() { println!("{}", style("-old snapshot").red()); println!("{}", style("+new results").green()); } else { println!("{}", style("+new results").green()); } print_changeset(changeset, new.metadata().expression.as_deref()); } fn print_snapshot_diff_with_title( workspace_root: &Path, new_snapshot: &Snapshot, old_snapshot: Option<&Snapshot>, line: u32, snapshot_file: Option<&Path>, ) { let width = term_width(); println!( "{title:━^width$}", title = style(" Snapshot Differences ").bold(), width = width ); print_snapshot_diff( workspace_root, new_snapshot, old_snapshot, snapshot_file, Some(line), ); } fn print_snapshot_summary_with_title( workspace_root: &Path, new_snapshot: &Snapshot, old_snapshot: Option<&Snapshot>, line: u32, snapshot_file: Option<&Path>, ) { let _old_snapshot = old_snapshot; let width = term_width(); println!( "{title:━^width$}", title = style(" Snapshot Summary ").bold(), width = width ); print_snapshot_summary(workspace_root, new_snapshot, snapshot_file, Some(line)); println!("{title:━^width$}", title = "", width = width); } /// Special marker to use an automatic name. /// /// This can be passed as a snapshot name in a macro to explicitly tell /// insta to use the automatic name. This is useful in ambiguous syntax /// situations. #[derive(Debug)] pub struct AutoName; impl From for ReferenceValue<'static> { fn from(_value: AutoName) -> ReferenceValue<'static> { ReferenceValue::Named(None) } } impl From> for ReferenceValue<'static> { fn from(value: Option) -> ReferenceValue<'static> { ReferenceValue::Named(value.map(Cow::Owned)) } } impl From for ReferenceValue<'static> { fn from(value: String) -> ReferenceValue<'static> { ReferenceValue::Named(Some(Cow::Owned(value))) } } impl<'a> From> for ReferenceValue<'a> { fn from(value: Option<&'a str>) -> ReferenceValue<'a> { ReferenceValue::Named(value.map(Cow::Borrowed)) } } impl<'a> From<&'a str> for ReferenceValue<'a> { fn from(value: &'a str) -> ReferenceValue<'a> { ReferenceValue::Named(Some(Cow::Borrowed(value))) } } pub enum ReferenceValue<'a> { Named(Option>), Inline(&'a str), } #[cfg(feature = "backtrace")] fn test_name_from_backtrace(module_path: &str) -> Result { let backtrace = backtrace::Backtrace::new(); let frames = backtrace.frames(); let mut found_run_wrapper = false; for symbol in frames .iter() .rev() .flat_map(|x| x.symbols()) .filter_map(|x| x.name()) .map(|x| format!("{}", x)) { if !found_run_wrapper { if symbol.starts_with("test::run_test::") { found_run_wrapper = true; } } else if symbol.starts_with(module_path) { let mut rv = &symbol[..symbol.len() - 19]; if rv.ends_with("::{{closure}}") { rv = &rv[..rv.len() - 13]; } return Ok(rv.to_string()); } } Err( "Cannot determine test name from backtrace, no snapshot name \ can be generated. Did you forget to enable debug info?", ) } fn generate_snapshot_name_for_thread(module_path: &str) -> Result { let thread = thread::current(); #[allow(unused_mut)] let mut name = Cow::Borrowed( thread .name() .ok_or("test thread is unnamed, no snapshot name can be generated.")?, ); if name == "main" { #[cfg(feature = "backtrace")] { name = Cow::Owned(test_name_from_backtrace(module_path)?); } #[cfg(not(feature = "backtrace"))] { return Err("tests run with disabled concurrency, automatic snapshot \ name generation is not supported. Consider using the \ \"backtrace\" feature of insta which tries to recover test \ names from the call stack."); } } // clean test name first let mut name = name.rsplit("::").next().unwrap(); if name.starts_with("test_") { name = &name[5..]; } // next check if we need to add a suffix let name = add_suffix_to_snapshot_name(Cow::Borrowed(name)); let key = format!("{}::{}", module_path.replace("::", "__"), name); // if the snapshot name clashes we need to increment a counter. // we really do not care about poisoning here. let mut counters = TEST_NAME_COUNTERS.lock().unwrap_or_else(|x| x.into_inner()); let test_idx = counters.get(&key).cloned().unwrap_or(0) + 1; let rv = if test_idx == 1 { name.to_string() } else { format!("{}-{}", name, test_idx) }; counters.insert(key, test_idx); Ok(rv) } /// Helper function that returns the real inline snapshot value from a given /// frozen value string. If the string starts with the '⋮' character /// (optionally prefixed by whitespace) the alternative serialization format /// is picked which has slightly improved indentation semantics. pub(super) fn get_inline_snapshot_value(frozen_value: &str) -> String { // TODO: could move this into the SnapshotContents `from_inline` method // (the only call site) if frozen_value.trim_start().starts_with('⋮') { // legacy format - retain so old snapshots still work let mut buf = String::new(); let mut line_iter = frozen_value.lines(); let mut indentation = 0; for line in &mut line_iter { let line_trimmed = line.trim_start(); if line_trimmed.is_empty() { continue; } indentation = line.len() - line_trimmed.len(); // 3 because '⋮' is three utf-8 bytes long buf.push_str(&line_trimmed[3..]); buf.push('\n'); break; } for line in &mut line_iter { if let Some(prefix) = line.get(..indentation) { if !prefix.trim().is_empty() { return "".to_string(); } } if let Some(remainder) = line.get(indentation..) { if remainder.starts_with('⋮') { // 3 because '⋮' is three utf-8 bytes long buf.push_str(&remainder[3..]); buf.push('\n'); } else if remainder.trim().is_empty() { continue; } else { return "".to_string(); } } } buf.trim_end().to_string() } else { normalize_inline_snapshot(frozen_value) } } #[test] fn test_inline_snapshot_value_newline() { // https://github.com/mitsuhiko/insta/issues/39 assert_eq!(get_inline_snapshot_value("\n"), ""); } fn count_leading_spaces(value: &str) -> usize { value.chars().take_while(|x| x.is_whitespace()).count() } fn min_indentation(snapshot: &str) -> usize { let lines = snapshot.trim_end().lines(); if lines.clone().count() <= 1 { // not a multi-line string return 0; } lines .skip_while(|l| l.is_empty()) .map(count_leading_spaces) .min() .unwrap_or(0) } #[test] fn test_min_indentation() { let t = r#" 1 2 "#; assert_eq!(min_indentation(t), 3); let t = r#" 1 2"#; assert_eq!(min_indentation(t), 4); let t = r#" 1 2 "#; assert_eq!(min_indentation(t), 12); let t = r#" 1 2 "#; assert_eq!(min_indentation(t), 3); let t = r#" a "#; assert_eq!(min_indentation(t), 8); let t = ""; assert_eq!(min_indentation(t), 0); let t = r#" a b c "#; assert_eq!(min_indentation(t), 0); let t = r#" a "#; assert_eq!(min_indentation(t), 0); let t = " a"; assert_eq!(min_indentation(t), 4); let t = r#"a a"#; assert_eq!(min_indentation(t), 0); } // Removes excess indentation, removes excess whitespace at start & end fn normalize_inline_snapshot(snapshot: &str) -> String { let indentation = min_indentation(snapshot); snapshot .trim_end() .lines() .skip_while(|l| l.is_empty()) .map(|l| &l[indentation..]) .collect::>() .join("\n") } #[test] fn test_normalize_inline_snapshot() { // here we do exact matching (rather than `assert_snapshot`) // to ensure we're not incorporating the modifications this library makes let t = r#" 1 2 "#; assert_eq!( normalize_inline_snapshot(t), r###" 1 2"###[1..] ); let t = r#" 1 2"#; assert_eq!( normalize_inline_snapshot(t), r###" 1 2"###[1..] ); let t = r#" 1 2 "#; assert_eq!( normalize_inline_snapshot(t), r###" 1 2"###[1..] ); let t = r#" 1 2 "#; assert_eq!( normalize_inline_snapshot(t), r###" 1 2"###[1..] ); let t = r#" a "#; assert_eq!(normalize_inline_snapshot(t), "a"); let t = ""; assert_eq!(normalize_inline_snapshot(t), ""); let t = r#" a b c "#; assert_eq!( normalize_inline_snapshot(t), r###" a b c"###[1..] ); let t = r#" a "#; assert_eq!(normalize_inline_snapshot(t), "a"); let t = " a"; assert_eq!(normalize_inline_snapshot(t), "a"); let t = r#"a a"#; assert_eq!( normalize_inline_snapshot(t), r###" a a"###[1..] ); } fn update_snapshots( snapshot_file: Option<&Path>, new: Snapshot, old: Option, line: u32, pending_snapshots: Option, output_behavior: OutputBehavior, ) -> Result<(), Box> { let unseen = snapshot_file.map_or(false, |x| fs::metadata(x).is_ok()); let should_print = output_behavior != OutputBehavior::Nothing; match update_snapshot_behavior(unseen) { UpdateBehavior::InPlace => { if let Some(ref snapshot_file) = snapshot_file { new.save(snapshot_file)?; if should_print { eprintln!( "{} {}", if unseen { style("created previously unseen snapshot").green() } else { style("updated snapshot").green() }, style(snapshot_file.display()).cyan().underlined(), ); } return Ok(()); } else if should_print { eprintln!( "{}", style("error: cannot update inline snapshots in-place") .red() .bold(), ); } } UpdateBehavior::NewFile => { if let Some(ref snapshot_file) = snapshot_file { let mut new_path = snapshot_file.to_path_buf(); new_path.set_extension("snap.new"); new.save(&new_path)?; if should_print { eprintln!( "{} {}", style("stored new snapshot").green(), style(new_path.display()).cyan().underlined(), ); } } else { PendingInlineSnapshot::new(Some(new), old, line) .save(pending_snapshots.unwrap())?; } } UpdateBehavior::NoUpdate => {} } Ok(()) } /// If there is a suffix on the settings, append it to the snapshot name. fn add_suffix_to_snapshot_name(name: Cow<'_, str>) -> Cow<'_, str> { Settings::with(|settings| { settings .snapshot_suffix() .map(|suffix| Cow::Owned(format!("{}@{}", name, suffix))) .unwrap_or_else(|| name) }) } #[allow(clippy::too_many_arguments)] pub fn assert_snapshot( refval: ReferenceValue<'_>, new_snapshot: &str, manifest_dir: &str, module_path: &str, file: &str, line: u32, expr: &str, ) -> Result<(), Box> { let cargo_workspace = get_cargo_workspace(manifest_dir); let output_behavior = output_snapshot_behavior(); let (snapshot_name, snapshot_file, old, pending_snapshots) = match refval { ReferenceValue::Named(snapshot_name) => { let snapshot_name = match snapshot_name { Some(snapshot_name) => add_suffix_to_snapshot_name(snapshot_name), None => generate_snapshot_name_for_thread(module_path) .unwrap() .into(), }; let snapshot_file = get_snapshot_filename(module_path, &snapshot_name, &cargo_workspace, file); let old = if fs::metadata(&snapshot_file).is_ok() { Some(Snapshot::from_file(&snapshot_file)?) } else { None }; (Some(snapshot_name), Some(snapshot_file), old, None) } ReferenceValue::Inline(contents) => { let snapshot_name = generate_snapshot_name_for_thread(module_path) .ok() .map(Cow::Owned); let mut filename = cargo_workspace.join(file); filename.set_file_name(format!( ".{}.pending-snap", filename .file_name() .expect("no filename") .to_str() .expect("non unicode filename") )); ( snapshot_name, None, Some(Snapshot::from_components( module_path.replace("::", "__"), None, MetaData::default(), SnapshotContents::from_inline(contents), )), Some(filename), ) } }; let new_snapshot_contents: SnapshotContents = new_snapshot.into(); let new = Snapshot::from_components( module_path.replace("::", "__"), snapshot_name.as_ref().map(|x| x.to_string()), MetaData { source: Some(path_to_storage(file)), expression: Some(expr.to_string()), input_file: Settings::with(|settings| { settings .input_file() .and_then(|x| cargo_workspace.join(x).canonicalize().ok()) .and_then(|s| { s.strip_prefix(cargo_workspace) .ok() .map(|x| x.to_path_buf()) }) .map(path_to_storage) }), }, new_snapshot_contents, ); // memoize the snapshot file if requested. if let Some(ref snapshot_file) = snapshot_file { memoize_snapshot_file(snapshot_file); } // if the snapshot matches we're done. if let Some(ref old_snapshot) = old { if old_snapshot.contents() == new.contents() { // let's just make sure there are no more pending files lingering // around. if let Some(ref snapshot_file) = snapshot_file { let mut snapshot_file = snapshot_file.clone(); snapshot_file.set_extension("snap.new"); fs::remove_file(snapshot_file).ok(); } // and add a null pending snapshot to a pending snapshot file if needed if let Some(ref pending_snapshots) = pending_snapshots { if fs::metadata(pending_snapshots).is_ok() { PendingInlineSnapshot::new(None, None, line).save(pending_snapshots)?; } } if force_update_snapshots() { update_snapshots( snapshot_file.as_deref(), new, old, line, pending_snapshots, output_behavior, )?; } return Ok(()); } } match output_behavior { OutputBehavior::Summary => { print_snapshot_summary_with_title( cargo_workspace, &new, old.as_ref(), line, snapshot_file.as_deref(), ); } OutputBehavior::Diff => { print_snapshot_diff_with_title( cargo_workspace, &new, old.as_ref(), line, snapshot_file.as_deref(), ); } _ => {} } update_snapshots( snapshot_file.as_deref(), new, old, line, pending_snapshots, output_behavior, )?; if output_behavior != OutputBehavior::Nothing { println!( "{hint}", hint = style("To update snapshots run `cargo insta review`").dim(), ); } if should_fail_in_tests() { panic!( "snapshot assertion for '{}' failed in line {}", snapshot_name.as_ref().map_or("unnamed snapshot", |x| &*x), line ); } Ok(()) } insta-1.3.0/src/select_grammar.pest010064400007650000024000000011621374651470200154720ustar 00000000000000WHITESPACE = _{ WHITE_SPACE } ident = @{ XID_START ~ XID_CONTINUE* } deep_wildcard = { "." ~ "**" } wildcard = { "." ~ "*" } key = @{ "." ~ ident } int = { "-"? ~ NUMBER+ } string = @{ "\"" ~ (!("\"") ~ ANY)* ~ "\""} subscript = { "[" ~ ( string | int ) ~ "]" } full_range = { "[" ~ "]" } range = { "[" ~ int ~ ":" ~ int ~ "]" } range_to = { "[" ~ ":" ~ int ~ "]" } range_from = { "[" ~ int ~ ":]" } segment = _{ deep_wildcard | wildcard | key | subscript | full_range | range | range_to | range_from } identity = { "." } selector = { (segment+ | identity) } selectors = { SOI ~ selector ~ ("," ~ selector)* ~ ","? ~ EOI }insta-1.3.0/src/serialization.rs010064400007650000024000000061661374651470200150440ustar 00000000000000use serde::de::value::Error as ValueError; use serde::Serialize; use crate::content::{Content, ContentSerializer}; use crate::settings::Settings; pub enum SerializationFormat { #[cfg(feature = "csv")] Csv, #[cfg(feature = "ron")] Ron, #[cfg(feature = "toml")] Toml, Yaml, Json, } pub enum SnapshotLocation { Inline, File, } pub fn serialize_content( mut content: Content, format: SerializationFormat, location: SnapshotLocation, ) -> String { content = Settings::with(|settings| { if settings.sort_maps() { content.sort_maps(); } #[cfg(feature = "redactions")] { for (selector, redaction) in settings.iter_redactions() { content = selector.redact(content, redaction); } } content }); match format { SerializationFormat::Yaml => { let serialized = serde_yaml::to_string(&content).unwrap(); match location { SnapshotLocation::Inline => serialized, SnapshotLocation::File => serialized[4..].to_string(), } } SerializationFormat::Json => serde_json::to_string_pretty(&content).unwrap(), #[cfg(feature = "csv")] SerializationFormat::Csv => { let mut buf = Vec::with_capacity(128); { let mut writer = csv::Writer::from_writer(&mut buf); writer.serialize(&content).unwrap(); writer.flush().unwrap(); } String::from_utf8(buf).unwrap() } #[cfg(feature = "ron")] SerializationFormat::Ron => { let mut buf = Vec::new(); let mut config = ron::ser::PrettyConfig::new(); config.new_line = "\n".to_string(); config.indentor = " ".to_string(); let mut serializer = ron::ser::Serializer::new(&mut buf, Some(config), true).unwrap(); content.serialize(&mut serializer).unwrap(); String::from_utf8(buf).unwrap() } #[cfg(feature = "toml")] SerializationFormat::Toml => { let mut rv = toml::to_string_pretty(&content).unwrap(); if rv.ends_with('\n') { rv.truncate(rv.len() - 1); } rv } } } pub fn serialize_value( s: &S, format: SerializationFormat, location: SnapshotLocation, ) -> String { let serializer = ContentSerializer::::new(); let content = Serialize::serialize(s, serializer).unwrap(); serialize_content(content, format, location) } #[cfg(feature = "redactions")] pub fn serialize_value_redacted( s: &S, redactions: &[(crate::redaction::Selector, crate::redaction::Redaction)], format: SerializationFormat, location: SnapshotLocation, ) -> String { let serializer = ContentSerializer::::new(); let mut content = Serialize::serialize(s, serializer).unwrap(); for (selector, redaction) in redactions { content = selector.redact(content, &redaction); } serialize_content(content, format, location) } insta-1.3.0/src/settings.rs010064400007650000024000000242161374651470200140230ustar 00000000000000use lazy_static::lazy_static; use std::cell::RefCell; use std::future::Future; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; #[cfg(feature = "redactions")] use crate::{ content::Content, redaction::{dynamic_redaction, ContentPath, Redaction, Selector}, }; lazy_static! { static ref DEFAULT_SETTINGS: Arc = Arc::new(ActualSettings { sort_maps: false, snapshot_path: "snapshots".into(), snapshot_suffix: "".into(), input_file: None, prepend_module_to_snapshot: true, #[cfg(feature = "redactions")] redactions: Redactions::default(), }); } thread_local!(static CURRENT_SETTINGS: RefCell = RefCell::new(Settings::new())); /// Represents stored redactions. #[cfg(feature = "redactions")] #[derive(Clone, Default)] pub struct Redactions(Vec<(Selector<'static>, Arc)>); #[cfg(feature = "redactions")] impl<'a> From> for Redactions { fn from(value: Vec<(&'a str, Redaction)>) -> Redactions { Redactions( value .into_iter() .map(|x| (Selector::parse(x.0).unwrap().make_static(), Arc::new(x.1))) .collect(), ) } } #[derive(Clone)] #[doc(hidden)] pub struct ActualSettings { pub sort_maps: bool, pub snapshot_path: PathBuf, pub snapshot_suffix: String, pub input_file: Option, pub prepend_module_to_snapshot: bool, #[cfg(feature = "redactions")] pub redactions: Redactions, } /// Configures how insta operates at test time. /// /// Settings are always bound to a thread and some default settings /// are always available. Settings can be either temporarily bound /// of permanently. /// /// This can be used to influence how the snapshot macros operate. /// For instance it can be useful to force ordering of maps when /// unordered structures are used through settings. /// /// Settings can also be configured with the `with_settings!` macro. /// /// Example: /// /// ```ignore /// use insta; /// /// let mut settings = insta::Settings::clone_current(); /// settings.set_sort_maps(true); /// settings.bind(|| { /// insta::assert_snapshot!(...); /// }); /// ``` #[derive(Clone)] pub struct Settings { inner: Arc, } impl Default for Settings { fn default() -> Settings { Settings { inner: DEFAULT_SETTINGS.clone(), } } } impl Settings { /// Returns the default settings. /// /// It's recommended to use `clone_current` instead so that /// already applied modifications are not discarded. pub fn new() -> Settings { Settings::default() } /// Returns a copy of the current settings. pub fn clone_current() -> Settings { Settings::with(|x| x.clone()) } /// Internal helper for macros #[doc(hidden)] pub fn _private_inner_mut(&mut self) -> &mut ActualSettings { Arc::make_mut(&mut self.inner) } /// Enables forceful sorting of maps before serialization. /// /// Note that this only applies to snapshots that undergo serialization /// (eg: does not work for `assert_debug_snapshot!`.) /// /// The default value is `false`. pub fn set_sort_maps(&mut self, value: bool) { self._private_inner_mut().sort_maps = value; } /// Returns the current value for map sorting. pub fn sort_maps(&self) -> bool { self.inner.sort_maps } /// Disbales prepending of modules to the snapshot filename. /// /// By default the filename of a snapshot is `__.snap`. /// Setting this flag to `false` changes the snapshot filename to just /// `.snap`. /// /// The default value is `true`. pub fn set_prepend_module_to_snapshot(&mut self, value: bool) { self._private_inner_mut().prepend_module_to_snapshot = value; } /// Returns the current value for module name prepending. pub fn prepend_module_to_snapshot(&self) -> bool { self.inner.prepend_module_to_snapshot } /// Sets the snapshot suffix. /// /// The snapshot suffix is added to all snapshot names with an `@` sign /// between. For instance if the snapshot suffix is set to `"foo"` and /// the snapshot would be named `"snapshot"` it turns into `"snapshot@foo"`. /// This is useful to separate snapshots if you want to use test /// parameterization. pub fn set_snapshot_suffix>(&mut self, suffix: I) { self._private_inner_mut().snapshot_suffix = suffix.into(); } /// Removes the snapshot suffix. pub fn remove_snapshot_suffix(&mut self) { self.set_snapshot_suffix(""); } /// Returns the current snapshot suffix. pub fn snapshot_suffix(&self) -> Option<&str> { if self.inner.snapshot_suffix.is_empty() { None } else { Some(&self.inner.snapshot_suffix) } } /// Sets the input file reference. /// /// This value is completely unused by the snapshot testing system but /// it lets you store some meta data with a snapshot that refers you back /// to the input file. The path stored here is made relative to the /// workspace root before storing with the snapshot. pub fn set_input_file>(&mut self, p: P) { self._private_inner_mut().input_file = Some(p.as_ref().to_path_buf()); } /// Removes the input file reference. pub fn remove_input_file(&mut self) { self._private_inner_mut().input_file = None; } /// Returns the current input file reference. pub fn input_file(&self) -> Option<&Path> { self.inner.input_file.as_deref() } /// Registers redactions that should be applied. /// /// This can be useful if redactions must be shared across multiple /// snapshots. /// /// Note that this only applies to snapshots that undergo serialization /// (eg: does not work for `assert_debug_snapshot!`.) #[cfg(feature = "redactions")] pub fn add_redaction>(&mut self, selector: &str, replacement: R) { self._private_inner_mut().redactions.0.push(( Selector::parse(selector).unwrap().make_static(), Arc::new(replacement.into()), )); } /// Registers a replacement callback. /// /// This works similar to a redaction but instead of changing the value it /// asserts the value at a certain place. This function is internally /// supposed to call things like `assert_eq!`. /// /// This is a shortcut to `add_redaction(dynamic_redaction(...))`; #[cfg(feature = "redactions")] pub fn add_dynamic_redaction(&mut self, selector: &str, func: F) where I: Into, F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static, { self.add_redaction(selector, dynamic_redaction(func)); } /// Replaces the currently set redactions. /// /// The default set is empty. #[cfg(feature = "redactions")] pub fn set_redactions>(&mut self, redactions: R) { self._private_inner_mut().redactions = redactions.into(); } /// Removes all redactions. #[cfg(feature = "redactions")] pub fn clear_redactions(&mut self) { self._private_inner_mut().redactions.0.clear(); } /// Iterate over the redactions. #[cfg(feature = "redactions")] pub(crate) fn iter_redactions(&self) -> impl Iterator { self.inner .redactions .0 .iter() .map(|&(ref a, ref b)| (a, &**b)) } /// Sets the snapshot path. /// /// If not absolute it's relative to where the test is in. /// /// Defaults to `snapshots`. pub fn set_snapshot_path>(&mut self, path: P) { self._private_inner_mut().snapshot_path = path.as_ref().to_path_buf(); } /// Returns the snapshot path. pub fn snapshot_path(&self) -> &Path { &self.inner.snapshot_path } /// Runs a function with the current settings bound to the thread. pub fn bind(&self, f: F) { CURRENT_SETTINGS.with(|x| { let old = { let mut current = x.borrow_mut(); let old = current.inner.clone(); current.inner = self.inner.clone(); old }; f(); let mut current = x.borrow_mut(); current.inner = old; }) } /// Like `bind` but for futures. /// /// This lets you bind settings for the duration of a future like this: /// /// ```rust /// # use insta::Settings; /// # async fn foo() { /// let settings = Settings::new(); /// settings.bind_async(async { /// // do assertions here /// }).await; /// # } /// ``` pub fn bind_async, T>(&self, future: F) -> impl Future { struct BindingFuture(Arc, F); impl Future for BindingFuture { type Output = F::Output; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let inner = self.0.clone(); let future = unsafe { self.map_unchecked_mut(|s| &mut s.1) }; CURRENT_SETTINGS.with(|x| { let old = { let mut current = x.borrow_mut(); let old = current.inner.clone(); current.inner = inner; old }; let rv = future.poll(cx); let mut current = x.borrow_mut(); current.inner = old; rv }) } } BindingFuture(self.inner.clone(), future) } /// Binds the settings to the current thread permanently. pub fn bind_to_thread(&self) { CURRENT_SETTINGS.with(|x| { x.borrow_mut().inner = self.inner.clone(); }) } /// Runs a function with the current settings. pub(crate) fn with R>(f: F) -> R { CURRENT_SETTINGS.with(|x| f(&*x.borrow())) } } insta-1.3.0/src/snapshot.rs010064400007650000024000000244761374651470200140320ustar 00000000000000use std::error::Error; use std::fs; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use super::runtime::get_inline_snapshot_value; lazy_static! { static ref RUN_ID: String = { let d = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); format!("{}-{}", d.as_secs(), d.subsec_nanos()) }; } #[derive(Debug, Serialize, Deserialize)] pub struct PendingInlineSnapshot { pub run_id: String, pub line: u32, pub new: Option, pub old: Option, } impl PendingInlineSnapshot { pub fn new(new: Option, old: Option, line: u32) -> PendingInlineSnapshot { PendingInlineSnapshot { new, old, line, run_id: RUN_ID.clone(), } } pub fn load_batch>(p: P) -> Result, Box> { let f = BufReader::new(fs::File::open(p)?); let iter = serde_json::Deserializer::from_reader(f).into_iter::(); let mut rv = iter.collect::, _>>()?; // remove all but the last run if let Some(last_run_id) = rv.last().map(|x| x.run_id.clone()) { rv.retain(|x| x.run_id == last_run_id); } Ok(rv) } pub fn save_batch>( p: P, batch: &[PendingInlineSnapshot], ) -> Result<(), Box> { fs::remove_file(&p).ok(); for snap in batch { snap.save(&p)?; } Ok(()) } pub fn save>(&self, p: P) -> Result<(), Box> { let mut f = fs::OpenOptions::new().create(true).append(true).open(p)?; let mut s = serde_json::to_string(self)?; s.push('\n'); f.write_all(s.as_bytes())?; Ok(()) } } /// Snapshot metadata information. #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct MetaData { /// The source file (relative to workspace root). #[serde(skip_serializing_if = "Option::is_none")] pub(crate) source: Option, /// Optionally the expression that created the snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) expression: Option, /// Reference to the input file. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) input_file: Option, } impl MetaData { /// Returns the absolute source path. pub fn source(&self) -> Option<&str> { self.source.as_deref() } /// Returns the expression that created the snapshot. pub fn expression(&self) -> Option<&str> { self.expression.as_deref() } /// Returns the relative source path. pub fn get_relative_source(&self, base: &Path) -> Option { self.source.as_ref().map(|source| { base.join(source) .canonicalize() .ok() .and_then(|s| s.strip_prefix(base).ok().map(|x| x.to_path_buf())) .unwrap_or_else(|| base.to_path_buf()) }) } /// Returns the input file reference. pub fn input_file(&self) -> Option<&str> { self.input_file.as_deref() } } /// A helper to work with stored snapshots. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Snapshot { module_name: String, #[serde(skip_serializing_if = "Option::is_none")] snapshot_name: Option, metadata: MetaData, snapshot: SnapshotContents, } impl Snapshot { /// Loads a snapshot from a file. pub fn from_file>(p: P) -> Result> { let mut f = BufReader::new(fs::File::open(p.as_ref())?); let mut buf = String::new(); f.read_line(&mut buf)?; // yaml format let metadata = if buf.trim_end() == "---" { loop { let read = f.read_line(&mut buf)?; if read == 0 { break; } if buf[buf.len() - read..].trim_end() == "---" { buf.truncate(buf.len() - read); break; } } serde_yaml::from_str(&buf)? // legacy format } else { let mut rv = MetaData::default(); loop { buf.clear(); let read = f.read_line(&mut buf)?; if read == 0 || buf.trim_end().is_empty() { buf.truncate(buf.len() - read); break; } let mut iter = buf.splitn(2, ':'); if let Some(key) = iter.next() { if let Some(value) = iter.next() { let value = value.trim(); match key.to_lowercase().as_str() { "expression" => rv.expression = Some(value.to_string()), "source" => rv.source = Some(value.into()), _ => {} } } } } rv }; buf.clear(); for (idx, line) in f.lines().enumerate() { let line = line?; if idx > 0 { buf.push('\n'); } buf.push_str(&line); } let module_name = p .as_ref() .file_name() .unwrap() .to_str() .unwrap_or("") .split("__") .next() .unwrap_or("") .to_string(); let snapshot_name = p .as_ref() .file_name() .unwrap() .to_str() .unwrap_or("") .split('.') .next() .unwrap_or("") .splitn(2, "__") .nth(1) .map(|x| x.to_string()); Ok(Snapshot::from_components( module_name, snapshot_name, metadata, buf.into(), )) } /// Creates an empty snapshot. pub(crate) fn from_components( module_name: String, snapshot_name: Option, metadata: MetaData, snapshot: SnapshotContents, ) -> Snapshot { Snapshot { module_name, snapshot_name, metadata, snapshot, } } /// Returns the module name. pub fn module_name(&self) -> &str { &self.module_name } /// Returns the snapshot name. pub fn snapshot_name(&self) -> Option<&str> { self.snapshot_name.as_deref() } /// The metadata in the snapshot. pub fn metadata(&self) -> &MetaData { &self.metadata } /// The snapshot contents pub fn contents(&self) -> &SnapshotContents { &self.snapshot } /// The snapshot contents as a &str pub fn contents_str(&self) -> &str { &self.snapshot.0 } pub(crate) fn save>(&self, path: P) -> Result<(), Box> { let path = path.as_ref(); if let Some(folder) = path.parent() { fs::create_dir_all(&folder)?; } let mut f = fs::File::create(&path)?; serde_yaml::to_writer(&mut f, &self.metadata)?; f.write_all(b"\n---\n")?; f.write_all(self.contents_str().as_bytes())?; f.write_all(b"\n")?; Ok(()) } } /// The contents of a Snapshot // Could be Cow, but I think limited savings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SnapshotContents(String); impl SnapshotContents { pub fn from_inline(value: &str) -> SnapshotContents { SnapshotContents(get_inline_snapshot_value(value)) } pub fn to_inline(&self, indentation: usize) -> String { let contents = &self.0; let mut out = String::new(); let is_escape = contents.lines().count() > 1 || contents.contains(&['\\', '"'][..]); out.push_str(if is_escape { "r###\"" } else { "\"" }); // if we have more than one line we want to change into the block // representation mode if contents.lines().count() > 1 { out.extend( contents .lines() // newline needs to be at the start, since we don't want the end // finishing with a newline - the closing suffix should be on the same line .map(|l| { format!( "\n{:width$}{l}", "", width = if l.is_empty() { 0 } else { indentation }, l = l ) }) // `lines` removes the final line ending - add back .chain(Some(format!("\n{:width$}", "", width = indentation)).into_iter()), ); } else { out.push_str(contents); } out.push_str(if is_escape { "\"###" } else { "\"" }); out } } impl From<&str> for SnapshotContents { fn from(value: &str) -> SnapshotContents { SnapshotContents(value.to_string()) } } impl From for SnapshotContents { fn from(value: String) -> SnapshotContents { SnapshotContents(value) } } impl From for String { fn from(value: SnapshotContents) -> String { value.0 } } impl PartialEq for SnapshotContents { fn eq(&self, other: &Self) -> bool { self.0.trim_end() == other.0.trim_end() } } #[test] fn test_snapshot_contents() { let snapshot_contents = SnapshotContents("testing".to_string()); assert_eq!(snapshot_contents.to_inline(0), r#""testing""#); let t = &" a b"[1..]; assert_eq!( SnapshotContents(t.to_string()).to_inline(0), "r###\" a b \"###" ); let t = &" a b"[1..]; assert_eq!( SnapshotContents(t.to_string()).to_inline(4), "r###\" a b \"###" ); let t = &" a b"[1..]; assert_eq!( SnapshotContents(t.to_string()).to_inline(0), "r###\" a b \"###" ); let t = &" a b"[1..]; assert_eq!( SnapshotContents(t.to_string()).to_inline(0), "r###\" a b \"###" ); let t = "ab"; assert_eq!(SnapshotContents(t.to_string()).to_inline(0), r##""ab""##); } insta-1.3.0/src/snapshots/insta__test__embedded.snap010064400007650000024000000001121374651470200207730ustar 00000000000000--- source: src/test.rs expression: "\"Just a string\"" --- Just a string insta-1.3.0/src/test.rs010064400007650000024000000001271353142461700131320ustar 00000000000000#[test] fn test_embedded_test() { assert_snapshot!("embedded", "Just a string"); } insta-1.3.0/src/utils.rs010064400007650000024000000016771374651470200133310ustar 00000000000000use std::env; /// Are we running in in a CI environment? pub fn is_ci() -> bool { env::var("CI").is_ok() || env::var("TF_BUILD").is_ok() } #[cfg(feature = "colors")] pub use console::style; #[cfg(not(feature = "colors"))] mod fake_colors { pub struct FakeStyledObject(D); macro_rules! style_attr { ($($name:ident)*) => { $( #[inline] pub fn $name(self) -> FakeStyledObject { self } )* } } impl FakeStyledObject { style_attr!(red green yellow cyan bold dim underlined); } impl std::fmt::Display for FakeStyledObject { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) } } pub fn style(val: D) -> FakeStyledObject { FakeStyledObject(val) } } #[cfg(not(feature = "colors"))] pub use self::fake_colors::*; insta-1.3.0/tests/inputs/goodbye.txt010064400007650000024000000000231374651470200156710ustar 00000000000000Contents of goodbyeinsta-1.3.0/tests/inputs/hello.txt010064400007650000024000000000211374651470200153420ustar 00000000000000Contents of helloinsta-1.3.0/tests/link-to-inputs/goodbye.txt010064400007650000024000000000231374651470200172440ustar 00000000000000Contents of goodbyeinsta-1.3.0/tests/link-to-inputs/hello.txt010064400007650000024000000000211374651470200167150ustar 00000000000000Contents of helloinsta-1.3.0/tests/snapshots/snapshot_no_module_prepending.snap010064400007650000024000000001171374651470200232020ustar 00000000000000--- source: tests/test_settings.rs expression: "vec![1, 2, 3]" --- - 1 - 2 - 3 insta-1.3.0/tests/snapshots/test_basic__debug_vector.snap010064400007650000024000000001311374651470200220720ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3]" --- [ 1, 2, 3, ] insta-1.3.0/tests/snapshots/test_basic__display.snap010064400007650000024000000001061374651470200210710ustar 00000000000000--- source: tests/test_basic.rs expression: td --- TestDisplay struct insta-1.3.0/tests/snapshots/test_basic__json_vector.snap010064400007650000024000000001221374651470200217550ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3]" --- [ 1, 2, 3 ] insta-1.3.0/tests/snapshots/test_basic__nested__nested_module.snap010064400007650000024000000001001374651470200237460ustar 00000000000000--- source: tests/test_basic.rs expression: "\"aoeu\"" --- aoeu insta-1.3.0/tests/snapshots/test_basic__unnamed_debug_vector-2.snap010064400007650000024000000001431374651470200237430ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3, 4]" --- [ 1, 2, 3, 4, ] insta-1.3.0/tests/snapshots/test_basic__unnamed_debug_vector-3.snap010064400007650000024000000001551374651470200237470ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3, 4, 5]" --- [ 1, 2, 3, 4, 5, ] insta-1.3.0/tests/snapshots/test_basic__unnamed_debug_vector.snap010064400007650000024000000001311374651470200236010ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3]" --- [ 1, 2, 3, ] insta-1.3.0/tests/snapshots/test_basic__unnamed_display-2.snap010064400007650000024000000001101374651470200227320ustar 00000000000000--- source: tests/test_basic.rs expression: "\"whatever\"" --- whatever insta-1.3.0/tests/snapshots/test_basic__unnamed_display.snap010064400007650000024000000001061374651470200226000ustar 00000000000000--- source: tests/test_basic.rs expression: td --- TestDisplay struct insta-1.3.0/tests/snapshots/test_basic__unnamed_json_vector-2.snap010064400007650000024000000001321374651470200236240ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3, 4]" --- [ 1, 2, 3, 4 ] insta-1.3.0/tests/snapshots/test_basic__unnamed_json_vector-3.snap010064400007650000024000000001421374651470200236260ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3, 4, 5]" --- [ 1, 2, 3, 4, 5 ] insta-1.3.0/tests/snapshots/test_basic__unnamed_json_vector.snap010064400007650000024000000001221374651470200234640ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3]" --- [ 1, 2, 3 ] insta-1.3.0/tests/snapshots/test_basic__unnamed_yaml_vector-2.snap010064400007650000024000000001231374651470200236150ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3, 4]" --- - 1 - 2 - 3 - 4 insta-1.3.0/tests/snapshots/test_basic__unnamed_yaml_vector-3.snap010064400007650000024000000001321374651470200236160ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3, 4, 5]" --- - 1 - 2 - 3 - 4 - 5 insta-1.3.0/tests/snapshots/test_basic__unnamed_yaml_vector.snap010064400007650000024000000001141374651470200234560ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3]" --- - 1 - 2 - 3 insta-1.3.0/tests/snapshots/test_basic__yaml_vector.snap010064400007650000024000000001141374651470200217470ustar 00000000000000--- source: tests/test_basic.rs expression: "vec![1, 2, 3]" --- - 1 - 2 - 3 insta-1.3.0/tests/snapshots/test_glob__basic_globbing@goodbye.txt.snap010064400007650000024000000001661374651470200245070ustar 00000000000000--- source: tests/test_glob.rs expression: "&contents" input_file: tests/inputs/goodbye.txt --- "Contents of goodbye" insta-1.3.0/tests/snapshots/test_glob__basic_globbing@hello.txt.snap010064400007650000024000000001621374651470200241560ustar 00000000000000--- source: tests/test_glob.rs expression: "&contents" input_file: tests/inputs/hello.txt --- "Contents of hello" insta-1.3.0/tests/snapshots/test_glob__globs_follow_links@goodbye.txt.snap010064400007650000024000000001661374651470200254530ustar 00000000000000--- source: tests/test_glob.rs expression: "&contents" input_file: tests/inputs/goodbye.txt --- "Contents of goodbye" insta-1.3.0/tests/snapshots/test_glob__globs_follow_links@hello.txt.snap010064400007650000024000000001621374651470200251220ustar 00000000000000--- source: tests/test_glob.rs expression: "&contents" input_file: tests/inputs/hello.txt --- "Contents of hello" insta-1.3.0/tests/snapshots/test_inline__something-2.snap010064400007650000024000000001311374651470200217530ustar 00000000000000--- source: tests/test_inline.rs expression: "\"Testing-thread-2\"" --- Testing-thread-2 insta-1.3.0/tests/snapshots/test_inline__something.snap010064400007650000024000000001251374651470200216170ustar 00000000000000--- source: tests/test_inline.rs expression: "\"Testing-thread\"" --- Testing-thread insta-1.3.0/tests/snapshots/test_inline__unnamed_single_line-2.snap010064400007650000024000000001131374651470200237550ustar 00000000000000--- source: tests/test_inline.rs expression: "\"Testing-2\"" --- Testing-2 insta-1.3.0/tests/snapshots/test_inline__unnamed_single_line.snap010064400007650000024000000001071374651470200236210ustar 00000000000000--- source: tests/test_inline.rs expression: "\"Testing\"" --- Testing insta-1.3.0/tests/snapshots/test_redaction__foo_bar.snap010064400007650000024000000004051374651470200217240ustar 00000000000000--- source: tests/test_redaction.rs expression: "Selector::parse(\".foo.bar\").unwrap()" --- Selector { selectors: [ [ Key( "foo", ), Key( "bar", ), ], ], } insta-1.3.0/tests/snapshots/test_redaction__foo_bar_alt.snap010064400007650000024000000004161374651470200225660ustar 00000000000000--- source: tests/test_redaction.rs expression: "Selector::parse(\".foo[\\\"bar\\\"]\").unwrap()" --- Selector { selectors: [ [ Key( "foo", ), Key( "bar", ), ], ], } insta-1.3.0/tests/snapshots/test_redaction__foo_bar_deep.snap010064400007650000024000000004421374651470200227220ustar 00000000000000--- source: tests/test_redaction.rs expression: "Selector::parse(\".foo.bar.**\").unwrap()" --- Selector { selectors: [ [ Key( "foo", ), Key( "bar", ), DeepWildcard, ], ], } insta-1.3.0/tests/snapshots/test_redaction__foo_bar_full_range.snap010064400007650000024000000005251374651470200241250ustar 00000000000000--- source: tests/test_redaction.rs expression: "Selector::parse(\".foo.bar[]\").unwrap()" --- Selector { selectors: [ [ Key( "foo", ), Key( "bar", ), Range( None, None, ), ], ], } insta-1.3.0/tests/snapshots/test_redaction__foo_bar_range.snap010064400007650000024000000006601374651470200231030ustar 00000000000000--- source: tests/test_redaction.rs expression: "Selector::parse(\".foo.bar[10:20]\").unwrap()" --- Selector { selectors: [ [ Key( "foo", ), Key( "bar", ), Range( Some( 10, ), Some( 20, ), ), ], ], } insta-1.3.0/tests/snapshots/test_redaction__foo_bar_range_from.snap010064400007650000024000000006031374651470200241230ustar 00000000000000--- source: tests/test_redaction.rs expression: "Selector::parse(\".foo.bar[10:]\").unwrap()" --- Selector { selectors: [ [ Key( "foo", ), Key( "bar", ), Range( Some( 10, ), None, ), ], ], } insta-1.3.0/tests/snapshots/test_redaction__foo_bar_range_to.snap010064400007650000024000000006031374651470200236020ustar 00000000000000--- source: tests/test_redaction.rs expression: "Selector::parse(\".foo.bar[:10]\").unwrap()" --- Selector { selectors: [ [ Key( "foo", ), Key( "bar", ), Range( None, Some( 10, ), ), ], ], } insta-1.3.0/tests/snapshots/test_redaction__user.snap010064400007650000024000000004071374651470200212750ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 23,\n username: \"john_doe\".to_string(),\n email: Email(\"john@example.com\".to_string()),\n extra: \"\".to_string(),}" --- id: "[id]" username: john_doe email: john@example.com extra: "" insta-1.3.0/tests/snapshots/test_redaction__user_csv.snap010064400007650000024000000004101374651470200221420ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 44,\n username: \"julius_csv\".to_string(),\n email: Email(\"julius@example.com\".to_string()),\n extra: \"\".to_string(),}" --- id,username,email,extra [id],julius_csv,julius@example.com, insta-1.3.0/tests/snapshots/test_redaction__user_json.snap010064400007650000024000000004741374651470200223320ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 9999,\n username: \"jason_doe\".to_string(),\n email: Email(\"jason@example.com\".to_string()),\n extra: \"ssn goes here\".to_string(),}" --- { "id": "[id]", "username": "jason_doe", "email": "jason@example.com", "extra": "[extra]" } insta-1.3.0/tests/snapshots/test_redaction__user_json_settings.snap010064400007650000024000000004731374651470200242510ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 122,\n username: \"jason_doe\".to_string(),\n email: Email(\"jason@example.com\".to_string()),\n extra: \"ssn goes here\".to_string(),}" --- { "id": "[id]", "username": "jason_doe", "email": "jason@example.com", "extra": "[extra]" } insta-1.3.0/tests/snapshots/test_redaction__user_json_settings_callback.snap010064400007650000024000000004741374651470200260660ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 1234,\n username: \"jason_doe\".to_string(),\n email: Email(\"jason@example.com\".to_string()),\n extra: \"extra here\".to_string(),}" --- { "id": "[id]", "username": "jason_doe", "email": "jason@example.com", "extra": "extra here" } insta-1.3.0/tests/snapshots/test_redaction__user_ron.snap010064400007650000024000000004461374651470200221560ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 53,\n username: \"john_ron\".to_string(),\n email: Email(\"john@example.com\".to_string()),\n extra: \"\".to_string(),}" --- User( id: "[id]", username: "john_ron", email: Email("john@example.com"), extra: "", ) insta-1.3.0/tests/snapshots/test_redaction__user_toml.snap010064400007650000024000000004171374651470200223310ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 53,\n username: \"john_ron\".to_string(),\n email: Email(\"john@example.com\".to_string()),\n extra: \"\".to_string(),}" --- id = '[id]' username = 'john_ron' email = 'john@example.com' extra = '' insta-1.3.0/tests/snapshots/test_redaction__with_random_value_json_settings2.snap010064400007650000024000000004731374651470200270640ustar 00000000000000--- source: tests/test_redaction.rs expression: "&User{id: 975,\n username: \"jason_doe\".to_string(),\n email: Email(\"jason@example.com\".to_string()),\n extra: \"ssn goes here\".to_string(),}" --- { "id": "[id]", "username": "jason_doe", "email": "jason@example.com", "extra": "[extra]" } insta-1.3.0/tests/snapshots/test_suffixes__basic_suffixes@1.snap010064400007650000024000000000761374651470200233630ustar 00000000000000--- source: tests/test_suffixes.rs expression: "&value" --- 1 insta-1.3.0/tests/snapshots/test_suffixes__basic_suffixes@2.snap010064400007650000024000000000761374651470200233640ustar 00000000000000--- source: tests/test_suffixes.rs expression: "&value" --- 2 insta-1.3.0/tests/snapshots/test_suffixes__basic_suffixes@3.snap010064400007650000024000000000761374651470200233650ustar 00000000000000--- source: tests/test_suffixes.rs expression: "&value" --- 3 insta-1.3.0/tests/snapshots2/test_settings__snapshot_path.snap010064400007650000024000000001171374651470200231420ustar 00000000000000--- source: tests/test_settings.rs expression: "vec![1, 2, 3]" --- - 1 - 2 - 3 insta-1.3.0/tests/test_basic.rs010064400007650000024000000027301374651470200146530ustar 00000000000000use insta::{ assert_debug_snapshot, assert_display_snapshot, assert_json_snapshot, assert_yaml_snapshot, }; use std::fmt; #[test] fn test_debug_vector() { assert_debug_snapshot!("debug_vector", vec![1, 2, 3]); } #[test] fn test_unnamed_debug_vector() { assert_debug_snapshot!(vec![1, 2, 3]); assert_debug_snapshot!(vec![1, 2, 3, 4]); assert_debug_snapshot!(vec![1, 2, 3, 4, 5]); } #[test] fn test_yaml_vector() { assert_yaml_snapshot!("yaml_vector", vec![1, 2, 3]); } #[test] fn test_unnamed_yaml_vector() { assert_yaml_snapshot!(vec![1, 2, 3]); assert_yaml_snapshot!(vec![1, 2, 3, 4]); assert_yaml_snapshot!(vec![1, 2, 3, 4, 5]); } #[test] fn test_json_vector() { assert_json_snapshot!("json_vector", vec![1, 2, 3]); } #[test] fn test_unnamed_json_vector() { assert_json_snapshot!(vec![1, 2, 3]); assert_json_snapshot!(vec![1, 2, 3, 4]); assert_json_snapshot!(vec![1, 2, 3, 4, 5]); } mod nested { #[test] fn test_nested_module() { use insta::assert_snapshot; assert_snapshot!("aoeu"); } } struct TestDisplay; impl fmt::Display for TestDisplay { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "TestDisplay struct") } } #[test] fn test_display() { let td = TestDisplay; assert_display_snapshot!("display", td); } #[test] fn test_unnamed_display() { let td = TestDisplay; assert_display_snapshot!(td); assert_display_snapshot!("whatever"); } insta-1.3.0/tests/test_glob.rs010064400007650000024000000006661375421627400145250ustar 00000000000000#![cfg(feature = "glob")] #[test] fn test_basic_globbing() { insta::glob!("inputs/*.txt", |path| { let contents = std::fs::read_to_string(path).unwrap(); insta::assert_json_snapshot!(&contents); }); } #[test] fn test_globs_follow_links() { insta::glob!("link-to-inputs/*.txt", |path| { let contents = std::fs::read_to_string(path).unwrap(); insta::assert_json_snapshot!(&contents); }); } insta-1.3.0/tests/test_inline.rs010064400007650000024000000070551375417561300150600ustar 00000000000000#[cfg(feature = "csv")] use insta::assert_csv_snapshot; #[cfg(feature = "ron")] use insta::assert_ron_snapshot; #[cfg(feature = "toml")] use insta::assert_toml_snapshot; use insta::{assert_debug_snapshot, assert_json_snapshot, assert_snapshot, assert_yaml_snapshot}; use serde::Serialize; use std::thread; #[test] fn test_simple() { assert_debug_snapshot!(vec![1, 2, 3, 4], @r###" [ 1, 2, 3, 4, ] "###); } #[test] fn test_single_line() { assert_snapshot!("Testing", @"Testing"); } #[test] fn test_unnamed_single_line() { assert_snapshot!("Testing"); assert_snapshot!("Testing-2"); } #[test] fn test_unnamed_thread_single_line() { let builder = thread::Builder::new().name("foo::lol::something".into()); let handler = builder .spawn(|| { assert_snapshot!("Testing-thread"); assert_snapshot!("Testing-thread-2"); }) .unwrap(); handler.join().unwrap(); } #[test] fn test_newline() { // https://github.com/mitsuhiko/insta/issues/39 assert_snapshot!("\n", @" "); } #[cfg(feature = "csv")] #[test] fn test_csv_inline() { #[derive(Serialize)] pub struct Email(String); #[derive(Serialize)] pub struct User { id: u32, username: String, email: Email, } assert_csv_snapshot!(User { id: 1453, username: "mehmed-doe".into(), email: Email("mehmed@doe.invalid".into()), }, @r###" id,username,email 1453,mehmed-doe,mehmed@doe.invalid "###); } #[cfg(feature = "ron")] #[test] fn test_ron_inline() { #[derive(Serialize)] pub struct Email(String); #[derive(Serialize)] pub struct User { id: u32, username: String, email: Email, } assert_ron_snapshot!(User { id: 42, username: "peter-doe".into(), email: Email("peter@doe.invalid".into()), }, @r###" User( id: 42, username: "peter-doe", email: Email("peter@doe.invalid"), ) "###); } #[cfg(feature = "toml")] #[test] fn test_toml_inline() { #[derive(Serialize)] pub struct Email(String); #[derive(Serialize)] pub struct User { id: u32, username: String, email: Email, } assert_toml_snapshot!(User { id: 42, username: "peter-doe".into(), email: Email("peter@doe.invalid".into()), }, @r###" id = 42 username = 'peter-doe' email = 'peter@doe.invalid' "###); } #[test] fn test_json_inline() { assert_json_snapshot!(vec!["foo", "bar"], @r###" [ "foo", "bar" ] "###); } #[test] fn test_yaml_inline() { #[derive(Serialize)] pub struct User { id: u32, username: String, email: String, } assert_yaml_snapshot!(User { id: 42, username: "peter-pan".into(), email: "peterpan@wonderland.invalid".into() }, @r###" --- id: 42 username: peter-pan email: peterpan@wonderland.invalid "###); } #[cfg(feature = "redactions")] #[test] fn test_yaml_inline_redacted() { #[derive(Serialize)] pub struct User { id: u32, username: String, email: String, } assert_yaml_snapshot!(User { id: 42, username: "peter-pan".into(), email: "peterpan@wonderland.invalid".into() }, { ".id" => "[user-id]" }, @r###" --- id: "[user-id]" username: peter-pan email: peterpan@wonderland.invalid "###); } #[test] fn test_non_basic_plane() { assert_snapshot!("a 😀oeu", @"a 😀oeu"); } insta-1.3.0/tests/test_redaction.rs010064400007650000024000000146741375417450100155520ustar 00000000000000#![cfg(feature = "redactions")] use insta::_macro_support::Selector; use insta::{ assert_debug_snapshot, assert_json_snapshot, assert_yaml_snapshot, with_settings, Settings, }; use serde::Serialize; #[test] fn test_selector_parser() { macro_rules! assert_selector { ($short:expr, $sel:expr) => { assert_debug_snapshot!($short, Selector::parse($sel).unwrap()); }; } assert_selector!("foo_bar", ".foo.bar"); assert_selector!("foo_bar_alt", ".foo[\"bar\"]"); assert_selector!("foo_bar_full_range", ".foo.bar[]"); assert_selector!("foo_bar_range_to", ".foo.bar[:10]"); assert_selector!("foo_bar_range_from", ".foo.bar[10:]"); assert_selector!("foo_bar_range", ".foo.bar[10:20]"); assert_selector!("foo_bar_deep", ".foo.bar.**"); } #[derive(Serialize)] pub struct Email(String); #[derive(Serialize)] pub struct User { id: u32, username: String, email: Email, extra: String, } #[test] fn test_with_random_value() { assert_yaml_snapshot!("user", &User { id: 42, username: "john_doe".to_string(), email: Email("john@example.com".to_string()), extra: "".to_string(), }, { ".id" => "[id]" }); } #[test] fn test_with_random_value_inline_callback() { assert_yaml_snapshot!("user", &User { id: 23, username: "john_doe".to_string(), email: Email("john@example.com".to_string()), extra: "".to_string(), }, { ".id" => insta::dynamic_redaction(|value, path| { assert_eq!(path.to_string(), ".id"); assert_eq!(value.as_u64().unwrap(), 23); "[id]" }), }); } #[test] fn test_with_random_value_and_trailing_comma() { assert_yaml_snapshot!("user", &User { id: 11, username: "john_doe".to_string(), email: Email("john@example.com".to_string()), extra: "".to_string(), }, { ".id" => "[id]", }); } #[cfg(feature = "csv")] #[test] fn test_with_random_value_csv() { use insta::assert_csv_snapshot; assert_csv_snapshot!("user_csv", &User { id: 44, username: "julius_csv".to_string(), email: Email("julius@example.com".to_string()), extra: "".to_string(), }, { ".id" => "[id]" }); } #[cfg(feature = "ron")] #[test] fn test_with_random_value_ron() { use insta::assert_ron_snapshot; assert_ron_snapshot!("user_ron", &User { id: 53, username: "john_ron".to_string(), email: Email("john@example.com".to_string()), extra: "".to_string(), }, { ".id" => "[id]" }); } #[cfg(feature = "toml")] #[test] fn test_with_random_value_toml() { use insta::assert_toml_snapshot; assert_toml_snapshot!("user_toml", &User { id: 53, username: "john_ron".to_string(), email: Email("john@example.com".to_string()), extra: "".to_string(), }, { ".id" => "[id]" }); } #[test] fn test_with_random_value_json() { assert_json_snapshot!("user_json", &User { id: 9999, username: "jason_doe".to_string(), email: Email("jason@example.com".to_string()), extra: "ssn goes here".to_string(), }, { ".id" => "[id]", ".extra" => "[extra]" }); } #[test] fn test_with_random_value_json_settings() { let mut settings = Settings::new(); settings.add_redaction(".id", "[id]"); settings.add_redaction(".extra", "[extra]"); settings.bind(|| { assert_json_snapshot!( "user_json_settings", &User { id: 122, username: "jason_doe".to_string(), email: Email("jason@example.com".to_string()), extra: "ssn goes here".to_string(), } ); }); } #[test] fn test_with_callbacks() { let mut settings = Settings::new(); settings.add_dynamic_redaction(".id", |value, path| { assert_eq!(path.to_string(), ".id"); assert_eq!(value.as_u64().unwrap(), 1234); "[id]" }); settings.bind(|| { assert_json_snapshot!( "user_json_settings_callback", &User { id: 1234, username: "jason_doe".to_string(), email: Email("jason@example.com".to_string()), extra: "extra here".to_string(), } ); }); } #[test] fn test_with_random_value_json_settings2() { with_settings!({redactions => vec![ (".id", "[id]".into()), (".extra", "[extra]".into()), ]}, { assert_json_snapshot!( &User { id: 975, username: "jason_doe".to_string(), email: Email("jason@example.com".to_string()), extra: "ssn goes here".to_string(), } ); }); } #[test] fn test_redact_newtype_struct() { #[derive(Serialize)] pub struct UserWrapper(User); let wrapper = UserWrapper(User { id: 42, username: "john_doe".to_string(), email: Email("john@example.com".to_string()), extra: "".to_string(), }); assert_json_snapshot!(wrapper, { r#".id"# => "[id]" }, @r###" { "id": "[id]", "username": "john_doe", "email": "john@example.com", "extra": "" } "###); } #[test] fn test_redact_newtype_enum() { #[derive(Serialize)] pub enum Role { Admin(User), Visitor { id: String, name: String }, } let visitor = Role::Visitor { id: "my-id".into(), name: "my-name".into(), }; assert_yaml_snapshot!(visitor, { r#".id"# => "[id]", }, @r###" --- Visitor: id: "[id]" name: my-name "###); let admin = Role::Admin(User { id: 42, username: "john_doe".to_string(), email: Email("john@example.com".to_string()), extra: "".to_string(), }); assert_yaml_snapshot!(admin, { r#".id"# => "[id]", }, @r###" --- Admin: id: "[id]" username: john_doe email: john@example.com extra: "" "###); } #[test] fn test_redact_recursive() { #[derive(Serialize)] pub struct Node { id: u64, next: Option>, } let root = Node { id: 0, next: Some(Box::new(Node { id: 1, next: None })), }; assert_json_snapshot!(root, { ".**.id" => "[id]", }, @r###" { "id": "[id]", "next": { "id": "[id]", "next": null } } "###); } insta-1.3.0/tests/test_settings.rs010064400007650000024000000033701374651470200154330ustar 00000000000000use insta::{assert_yaml_snapshot, with_settings, Settings}; use std::collections::HashMap; #[test] fn test_simple() { let mut map = HashMap::new(); map.insert("a", "first value"); map.insert("b", "second value"); map.insert("c", "third value"); map.insert("d", "fourth value"); let mut settings = Settings::new(); settings.set_sort_maps(true); settings.bind(|| { assert_yaml_snapshot!(&map, @r###" --- a: first value b: second value c: third value d: fourth value "###); }); } #[test] fn test_bound_to_thread() { let mut map = HashMap::new(); map.insert("a", "first value"); map.insert("b", "second value"); map.insert("c", "third value"); map.insert("d", "fourth value"); let mut settings = Settings::new(); settings.set_sort_maps(true); settings.bind_to_thread(); assert_yaml_snapshot!(&map, @r###" --- a: first value b: second value c: third value d: fourth value "###); } #[test] fn test_settings_macro() { let mut map = HashMap::new(); map.insert("a", "first value"); map.insert("b", "second value"); map.insert("c", "third value"); map.insert("d", "fourth value"); with_settings!({sort_maps => true}, { insta::assert_yaml_snapshot!(&map, @r###" --- a: first value b: second value c: third value d: fourth value "###); }); } #[test] fn test_snapshot_path() { with_settings!({snapshot_path => "snapshots2"}, { assert_yaml_snapshot!(vec![1, 2, 3]); }); } #[test] fn test_snapshot_no_module_prepending() { with_settings!({prepend_module_to_snapshot => false}, { assert_yaml_snapshot!(vec![1, 2, 3]); }); } insta-1.3.0/tests/test_suffixes.rs010064400007650000024000000003221374651470200154210ustar 00000000000000#[test] fn test_basic_suffixes() { for value in vec![1, 2, 3] { insta::with_settings!({snapshot_suffix => value.to_string()}, { insta::assert_json_snapshot!(&value); }); } }