Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "7f23d2e4684a789a85c50503004fb38a23c9ea58"
},
"path_in_vcs": "insta"
}
+1125
View File
File diff suppressed because it is too large Load Diff
+204
View File
@@ -0,0 +1,204 @@
# 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 are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.66.0"
name = "insta"
version = "1.48.0"
authors = ["Armin Ronacher <armin.ronacher@active-4.com>"]
build = false
exclude = ["assets/*"]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A snapshot testing library for Rust"
homepage = "https://insta.rs/"
readme = "README.md"
keywords = [
"snapshot",
"testing",
"jest",
"approval",
]
categories = ["development-tools::testing"]
license = "Apache-2.0"
repository = "https://github.com/mitsuhiko/insta"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = [
"--cfg",
"docsrs",
]
[features]
_cargo_insta_internal = ["clap"]
colors = ["console"]
csv = [
"dep:csv",
"serde",
]
default = ["colors"]
filters = [
"regex",
"strip-ansi-escapes",
]
glob = [
"walkdir",
"globset",
]
json = ["serde"]
redactions = [
"pest",
"pest_derive",
"serde",
]
ron = [
"dep:ron",
"serde",
]
toml = [
"dep:toml_edit",
"dep:toml_writer",
"serde",
]
yaml = ["serde"]
[lib]
name = "insta"
path = "src/lib.rs"
[[test]]
name = "test_advanced"
path = "tests/test_advanced.rs"
[[test]]
name = "test_basic"
path = "tests/test_basic.rs"
[[test]]
name = "test_binary"
path = "tests/test_binary.rs"
[[test]]
name = "test_comparator"
path = "tests/test_comparator.rs"
[[test]]
name = "test_glob"
path = "tests/test_glob.rs"
[[test]]
name = "test_inline"
path = "tests/test_inline.rs"
[[test]]
name = "test_redaction"
path = "tests/test_redaction.rs"
[[test]]
name = "test_settings"
path = "tests/test_settings.rs"
[[test]]
name = "test_toml"
path = "tests/test_toml.rs"
[dependencies.clap]
version = "4.1"
features = [
"derive",
"env",
]
optional = true
[dependencies.console]
version = "0.16"
features = ["std"]
optional = true
default-features = false
[dependencies.csv]
version = "1.1.6"
optional = true
[dependencies.globset]
version = "0.4.6"
optional = true
[dependencies.once_cell]
version = "1.20.2"
[dependencies.pest]
version = "2.1.3"
optional = true
[dependencies.pest_derive]
version = "2.1.0"
optional = true
[dependencies.regex]
version = "1.6.0"
features = [
"std",
"unicode",
]
optional = true
default-features = false
[dependencies.ron]
version = "0.12.0"
optional = true
[dependencies.serde]
version = "1.0.117"
optional = true
[dependencies.similar]
version = "2.1.0"
features = ["inline"]
[dependencies.strip-ansi-escapes]
version = "0.2"
optional = true
[dependencies.tempfile]
version = "3"
[dependencies.toml_edit]
version = "0.25.0"
features = [
"serde",
"parse",
"display",
]
optional = true
[dependencies.toml_writer]
version = "1"
optional = true
[dependencies.walkdir]
version = "2.3.1"
optional = true
[dev-dependencies.rustc_version]
version = "0.4.0"
[dev-dependencies.serde]
version = "1.0.117"
features = ["derive"]
[dev-dependencies.similar-asserts]
version = "1.4.2"
+75
View File
@@ -0,0 +1,75 @@
[package]
name = "insta"
version = "1.48.0"
license = "Apache-2.0"
authors = ["Armin Ronacher <armin.ronacher@active-4.com>"]
description = "A snapshot testing library for Rust"
edition = "2021"
rust-version = "1.66.0"
homepage = "https://insta.rs/"
repository = "https://github.com/mitsuhiko/insta"
keywords = ["snapshot", "testing", "jest", "approval"]
categories = ["development-tools::testing"]
readme = "README.md"
exclude = ["assets/*"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["colors"]
# when the redactions feature is enabled values can be redacted in serialized
# snapshots.
redactions = ["pest", "pest_derive", "serde"]
# Enables support for running filters on snapshot
filters = ["regex", "strip-ansi-escapes"]
# Glob support
glob = ["walkdir", "globset"]
# Color support
colors = ["console"]
# Serialization formats
csv = ["dep:csv", "serde"]
json = ["serde"]
ron = ["dep:ron", "serde"]
toml = ["dep:toml_edit", "dep:toml_writer", "serde"]
yaml = ["serde"]
# internal feature exclusive to cargo-insta
_cargo_insta_internal = ["clap"]
[dependencies]
csv = { version = "1.1.6", optional = true }
# Needs pinning in Cargo.lock because of MSRV (0.16.1+ requires 1.71)
console = { version = "0.16", optional = true, default-features = false, features = ["std"] }
pest = { version = "2.1.3", optional = true }
pest_derive = { version = "2.1.0", optional = true }
ron = { version = "0.12.0", optional = true }
toml_edit = { version = "0.25.0", optional = true, features = [
"serde",
"parse",
"display",
] }
toml_writer = { version = "1", optional = true }
globset = { version = "0.4.6", optional = true }
walkdir = { version = "2.3.1", optional = true }
similar = { version = "2.1.0", features = ["inline"] }
regex = { version = "1.6.0", default-features = false, optional = true, features = [
"std",
"unicode",
] }
strip-ansi-escapes = { version = "0.2", optional = true }
serde = { version = "1.0.117", optional = true }
once_cell = "1.20.2"
clap = { workspace = true, optional = true }
tempfile = "3"
[dev-dependencies]
rustc_version = "0.4.0"
serde = { version = "1.0.117", features = ["derive"] }
similar-asserts = "1.4.2"
+201
View File
@@ -0,0 +1,201 @@
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.
+74
View File
@@ -0,0 +1,74 @@
<div align="center">
<img src="https://github.com/mitsuhiko/insta/blob/master/assets/logo.png?raw=true" width="250" height="250">
<p><strong>insta: a snapshot testing library for Rust</strong></p>
</div>
[![Crates.io](https://img.shields.io/crates/d/insta.svg)](https://crates.io/crates/insta)
[![License](https://img.shields.io/github/license/mitsuhiko/insta)](https://github.com/mitsuhiko/insta/blob/master/LICENSE)
[![Documentation](https://docs.rs/insta/badge.svg)](https://docs.rs/insta)
[![VSCode Extension](https://img.shields.io/visual-studio-marketplace/v/mitsuhiko.insta?label=vscode%20extension)](https://marketplace.visualstudio.com/items?itemName=mitsuhiko.insta)
## Introduction
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.
## Example
```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 [5 minute introduction](https://insta.rs/docs/quickstart/).
Insta also supports inline snapshots which are stored right in your source file
instead of separate files. This is accomplished by the companion
[cargo-insta](https://github.com/mitsuhiko/insta/tree/master/cargo-insta) tool.
## Editor Support
For looking at `.snap` files there is a [vscode extension](https://github.com/mitsuhiko/insta/tree/master/vscode-insta)
which can syntax highlight snapshot files, review snapshots and more. It can be installed from the
marketplace: [view on marketplace](https://marketplace.visualstudio.com/items?itemName=mitsuhiko.insta).
![jump to definition](https://raw.githubusercontent.com/mitsuhiko/insta/master/vscode-insta/images/jump-to-definition.gif)
## Diffing
Insta uses [`similar`](https://github.com/mitsuhiko/similar) for all its diffing
operations. You can use it independently of insta. You can use the
[`similar-asserts`](https://github.com/mitsuhiko/similar-asserts) crate to get
inline diffs for the standard `assert_eq!` macro to achieve insta like diffs
for regular comparisons:
```rust
use similar_asserts::assert_eq;
fn main() {
let reference = vec![1, 2, 3, 4];
assert_eq!(reference, (0..4).collect::<Vec<_>>());
}
```
## Sponsor
If you like the project and find it useful you can [become a
sponsor](https://github.com/sponsors/mitsuhiko).
## License and Links
- [Project Website](https://insta.rs/)
- [Documentation](https://docs.rs/insta/)
- [Issue Tracker](https://github.com/mitsuhiko/insta/issues)
- License: [Apache-2.0](https://github.com/mitsuhiko/insta/blob/master/LICENSE)
+147
View File
@@ -0,0 +1,147 @@
//! Provides the [`Comparator`] trait, which provides a mechanism for specifying
//! how [`Snapshot`] data should be compared.
use crate::snapshot::{Snapshot, SnapshotContents, TextSnapshotKind};
/// Allows specific behavior to be invoked when [`Snapshot`]s are compared.
///
/// This is intended for when custom `Snapshot` comparison behavior is
/// desired. For example, two binary files that contain the same logical data
/// but have different representations on disk (as might be the case with
/// compressed images) could be compared with a `Comparator` that decompresses
/// `Snapshot` data before comparing it.
///
/// To make a custom `Comparator` active, pass it to
/// [`crate::settings::Settings::set_comparator`] or call [`with_settings!`] and
/// provide an appropriate `Comparator` instance.
///
/// This trait requires `'static` so that implementing structs can be stored in
/// [`crate::settings::Settings`].
// TODO: `Send + Sync` is required because `Settings` currently uses `Arc`
// internally. Consider removing these bounds if `Settings` switches to `Rc`
// in the next breaking change.
pub trait Comparator: Send + Sync + 'static {
/// Returns `true` if the contents of `reference` and `test` match.
///
/// This is the standard comparison used by [`assert_snapshot!`].
fn matches(&self, reference: &Snapshot, test: &Snapshot) -> bool;
/// Returns `true` if `reference` and `test` match fully, including metadata.
///
/// This is used when `INSTA_REQUIRE_FULL_MATCH` is enabled. The default
/// implementation delegates to [`matches`](Self::matches).
fn matches_fully(&self, reference: &Snapshot, test: &Snapshot) -> bool {
self.matches(reference, test)
}
/// Returns a type-erased clone of `self`.
///
/// This is needed so that [`crate::settings::Settings`] (which provides the
/// usual mechanism for setting a custom `Comparator`) can implement
/// [`Clone`].
fn dyn_clone(&self) -> Box<dyn Comparator>;
}
/// Provides default comparison semantics for [`Snapshot`]s. Binary snapshots
/// are compared on the basis of their contents (including file extension). Text
/// snapshots are compared on the basis of their deserialized representation.
#[derive(Clone)]
pub struct DefaultComparator;
impl Comparator for DefaultComparator {
fn matches(&self, reference: &Snapshot, test: &Snapshot) -> bool {
reference.contents() == test.contents()
// For binary snapshots the extension also needs to be the same:
&& reference.metadata().snapshot_kind == test.metadata().snapshot_kind
}
fn matches_fully(&self, reference: &Snapshot, test: &Snapshot) -> bool {
match (reference.contents(), test.contents()) {
(SnapshotContents::Text(ref_contents), SnapshotContents::Text(test_contents)) => {
// Note that we previously would match the exact values of the
// unnormalized text. But that's too strict — it means we can
// never match a snapshot that has leading/trailing whitespace.
// So instead we check it matches on the latest format.
// Generally those should be the same — latest should be doing
// the minimum normalization; if they diverge we could update
// this to be stricter.
let contents_match_exact = ref_contents.matches_latest(test_contents);
match ref_contents.kind {
TextSnapshotKind::File => {
reference.metadata().trim_for_persistence()
== test.metadata().trim_for_persistence()
&& contents_match_exact
}
TextSnapshotKind::Inline => contents_match_exact,
}
}
_ => self.matches(reference, test),
}
}
fn dyn_clone(&self) -> Box<dyn Comparator> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod test {
use super::DefaultComparator;
use crate::comparator::Comparator;
use crate::snapshot::{
MetaData, Snapshot, SnapshotContents, TextSnapshotContents, TextSnapshotKind,
};
const TEXT: &str =
"The sky above the port was the color of a television, tuned to a dead channel.";
#[test]
fn default_comparator_matches() {
let comparator = DefaultComparator;
let a = Snapshot::from_components(
String::from("test"),
None,
MetaData::default(),
SnapshotContents::Text(TextSnapshotContents::new(
String::from(TEXT),
TextSnapshotKind::Inline,
)),
);
let b = a.clone();
assert!(comparator.matches(&a, &b));
assert!(comparator.matches_fully(&a, &b));
}
#[test]
fn default_comparator_matches_fully() {
let comparator = DefaultComparator;
let a = Snapshot::from_components(
String::from("test"),
None,
MetaData::default(),
SnapshotContents::Text(TextSnapshotContents::new(
String::from(TEXT),
TextSnapshotKind::File,
)),
);
let mut b = Snapshot::from_components(
String::from("test"),
None,
MetaData::default(),
SnapshotContents::Text(TextSnapshotContents::new(
String::from(TEXT),
TextSnapshotKind::Inline,
)),
);
b.metadata.description = Some(String::from("wintermute")); // Differs from None in a.
// Comparing contents alone passes.
assert!(comparator.matches(&a, &b));
// Comparing contents alone still passes.
assert!(comparator.matches_fully(&a, &a));
// Comparing snapshots with differing metadata fails.
assert!(!comparator.matches_fully(&a, &b));
}
}
+522
View File
@@ -0,0 +1,522 @@
use std::fmt::{Display, Write};
use crate::content::Content;
/// The maximum number of characters to print in a single line
/// when [`to_string_pretty`] is used.
const COMPACT_MAX_CHARS: usize = 120;
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum Format {
Condensed,
SingleLine,
Pretty,
}
/// Serializes a serializable to JSON.
pub struct Serializer {
out: String,
format: Format,
indentation: usize,
}
impl Serializer {
/// Creates a new [`Serializer`] that writes into the given writer.
pub fn new() -> Serializer {
Serializer {
out: String::new(),
format: Format::Condensed,
indentation: 0,
}
}
pub fn into_result(self) -> String {
self.out
}
fn write_indentation(&mut self) {
if self.format == Format::Pretty {
write!(self.out, "{: ^1$}", "", self.indentation * 2).unwrap();
}
}
fn start_container(&mut self, c: char) {
self.write_char(c);
self.indentation += 1;
}
fn end_container(&mut self, c: char, empty: bool) {
self.indentation -= 1;
if self.format == Format::Pretty && !empty {
self.write_char('\n');
self.write_indentation();
}
self.write_char(c);
}
fn write_comma(&mut self, first: bool) {
match self.format {
Format::Pretty => {
if first {
self.write_char('\n');
} else {
self.write_str(",\n");
}
self.write_indentation();
}
Format::Condensed => {
if !first {
self.write_char(',');
}
}
Format::SingleLine => {
if !first {
self.write_str(", ");
}
}
}
}
fn write_colon(&mut self) {
match self.format {
Format::Pretty | Format::SingleLine => self.write_str(": "),
Format::Condensed => self.write_char(':'),
}
}
fn serialize_array(&mut self, items: &[Content]) {
self.start_container('[');
for (idx, item) in items.iter().enumerate() {
self.write_comma(idx == 0);
self.serialize(item);
}
self.end_container(']', items.is_empty());
}
fn serialize_object(&mut self, fields: &[(&str, Content)]) {
self.start_container('{');
for (idx, (key, value)) in fields.iter().enumerate() {
self.write_comma(idx == 0);
self.write_escaped_str(key);
self.write_colon();
self.serialize(value);
}
self.end_container('}', fields.is_empty());
}
pub fn serialize(&mut self, value: &Content) {
match value {
Content::Bool(true) => self.write_str("true"),
Content::Bool(false) => self.write_str("false"),
Content::U8(n) => write!(self.out, "{n}").unwrap(),
Content::U16(n) => write!(self.out, "{n}").unwrap(),
Content::U32(n) => write!(self.out, "{n}").unwrap(),
Content::U64(n) => write!(self.out, "{n}").unwrap(),
Content::U128(n) => write!(self.out, "{n}").unwrap(),
Content::I8(n) => write!(self.out, "{n}").unwrap(),
Content::I16(n) => write!(self.out, "{n}").unwrap(),
Content::I32(n) => write!(self.out, "{n}").unwrap(),
Content::I64(n) => write!(self.out, "{n}").unwrap(),
Content::I128(n) => write!(self.out, "{n}").unwrap(),
Content::F32(f) => self.write_float(f, f.is_finite()),
Content::F64(f) => self.write_float(f, f.is_finite()),
Content::Char(c) => self.write_escaped_str(&(*c).to_string()),
Content::String(s) => self.write_escaped_str(s),
Content::Bytes(bytes) => {
self.start_container('[');
for (idx, byte) in bytes.iter().enumerate() {
self.write_comma(idx == 0);
self.write_str(&byte.to_string());
}
self.end_container(']', bytes.is_empty());
}
Content::None | Content::Unit | Content::UnitStruct(_) => self.write_str("null"),
Content::Some(content) => self.serialize(content),
Content::UnitVariant(_, _, variant) => self.write_escaped_str(variant),
Content::NewtypeStruct(_, content) => self.serialize(content),
Content::NewtypeVariant(_, _, variant, content) => {
self.start_container('{');
self.write_comma(true);
self.write_escaped_str(variant);
self.write_colon();
self.serialize(content);
self.end_container('}', false);
}
Content::Seq(seq) | Content::Tuple(seq) | Content::TupleStruct(_, seq) => {
self.serialize_array(seq);
}
Content::TupleVariant(_, _, variant, seq) => {
self.start_container('{');
self.write_comma(true);
self.write_escaped_str(variant);
self.write_colon();
self.serialize_array(seq);
self.end_container('}', false);
}
Content::Map(map) => {
self.start_container('{');
for (idx, (key, value)) in map.iter().enumerate() {
self.write_comma(idx == 0);
let real_key = key.resolve_inner();
if let Content::String(ref s) = real_key {
self.write_escaped_str(s);
} else if let Some(num) = real_key.as_i64() {
self.write_escaped_str(&num.to_string());
} else if let Some(num) = real_key.as_i128() {
self.write_escaped_str(&num.to_string());
} else {
panic!("cannot serialize maps without string keys to JSON");
}
self.write_colon();
self.serialize(value);
}
self.end_container('}', map.is_empty());
}
Content::Struct(_, fields) => {
self.serialize_object(fields);
}
Content::StructVariant(_, _, variant, fields) => {
self.start_container('{');
self.write_comma(true);
self.write_escaped_str(variant);
self.write_colon();
self.serialize_object(fields);
self.end_container('}', false);
}
}
}
fn write_float(&mut self, n: impl Display, is_finite: bool) {
if is_finite {
let start = self.out.len();
write!(self.out, "{n}").unwrap();
// ensure the result has .0 for whole numbers to be round-trip safe
if !self.out[start..].contains('.') {
self.out.push_str(".0");
}
} else {
self.write_str("null");
}
}
fn write_str(&mut self, s: &str) {
self.out.push_str(s);
}
fn write_char(&mut self, c: char) {
self.out.push(c);
}
fn write_escaped_str(&mut self, value: &str) {
self.write_char('"');
let bytes = value.as_bytes();
let mut start = 0;
for (i, &byte) in bytes.iter().enumerate() {
let escape = ESCAPE[byte as usize];
if escape == 0 {
continue;
}
if start < i {
self.write_str(&value[start..i]);
}
match escape {
self::BB => self.write_str("\\b"),
self::TT => self.write_str("\\t"),
self::NN => self.write_str("\\n"),
self::FF => self.write_str("\\f"),
self::RR => self.write_str("\\r"),
self::QU => self.write_str("\\\""),
self::BS => self.write_str("\\\\"),
self::U => {
static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
self.write_str("\\u00");
self.write_char(HEX_DIGITS[(byte >> 4) as usize] as char);
self.write_char(HEX_DIGITS[(byte & 0xF) as usize] as char);
}
_ => unreachable!(),
}
start = i + 1;
}
if start != bytes.len() {
self.write_str(&value[start..]);
}
self.write_char('"');
}
}
const BB: u8 = b'b'; // \x08
const TT: u8 = b't'; // \x09
const NN: u8 = b'n'; // \x0A
const FF: u8 = b'f'; // \x0C
const RR: u8 = b'r'; // \x0D
const QU: u8 = b'"'; // \x22
const BS: u8 = b'\\'; // \x5C
const U: u8 = b'u'; // \x00...\x1F except the ones above
// Lookup table of escape sequences. A value of b'x' at index i means that byte
// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped.
#[rustfmt::skip]
static ESCAPE: [u8; 256] = [
// 1 2 3 4 5 6 7 8 9 A B C D E F
U, U, U, U, U, U, U, U, BB, TT, NN, U, FF, RR, U, U, // 0
U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, // 1
0, 0, QU, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, BS, 0, 0, 0, // 5
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F
];
/// Serializes a value to JSON.
pub fn to_string(value: &Content) -> String {
let mut ser = Serializer::new();
ser.serialize(value);
ser.into_result()
}
/// Serializes a value to JSON in single-line format.
#[allow(unused)]
pub fn to_string_compact(value: &Content) -> String {
let mut ser = Serializer::new();
ser.format = Format::SingleLine;
ser.serialize(value);
let rv = ser.into_result();
// this is pretty wasteful as we just format twice
// but it's acceptable for the way this is used in
// insta.
if rv.chars().count() > COMPACT_MAX_CHARS {
to_string_pretty(value)
} else {
rv
}
}
/// Serializes a value to JSON pretty
#[allow(unused)]
pub fn to_string_pretty(value: &Content) -> String {
let mut ser = Serializer::new();
ser.format = Format::Pretty;
ser.serialize(value);
ser.into_result()
}
#[test]
fn test_to_string() {
let json = to_string(&Content::Map(vec![
(
Content::from("environments"),
Content::Seq(vec![
Content::from("development"),
Content::from("production"),
]),
),
(Content::from("cmdline"), Content::Seq(vec![])),
(Content::from("extra"), Content::Map(vec![])),
]));
crate::assert_snapshot!(&json, @r#"{"environments":["development","production"],"cmdline":[],"extra":{}}"#);
}
#[test]
fn test_to_string_pretty() {
let json = to_string_pretty(&Content::Map(vec![
(
Content::from("environments"),
Content::Seq(vec![
Content::from("development"),
Content::from("production"),
]),
),
(Content::from("cmdline"), Content::Seq(vec![])),
(Content::from("extra"), Content::Map(vec![])),
]));
crate::assert_snapshot!(&json, @r#"
{
"environments": [
"development",
"production"
],
"cmdline": [],
"extra": {}
}
"#);
}
#[test]
fn test_to_string_num_keys() {
let content = Content::Map(vec![
(Content::from(42u32), Content::from(true)),
(Content::from(-23i32), Content::from(false)),
]);
let json = to_string_pretty(&content);
crate::assert_snapshot!(&json, @r#"
{
"42": true,
"-23": false
}
"#);
}
#[test]
fn test_to_string_pretty_complex() {
let content = Content::Map(vec![
(
Content::from("is_alive"),
Content::NewtypeStruct("Some", Content::from(true).into()),
),
(
Content::from("newtype_variant"),
Content::NewtypeVariant(
"Foo",
0,
"variant_a",
Box::new(Content::Struct(
"VariantA",
vec![
("field_a", Content::String("value_a".into())),
("field_b", 42u32.into()),
],
)),
),
),
(
Content::from("struct_variant"),
Content::StructVariant(
"Foo",
0,
"variant_b",
vec![
("field_a", Content::String("value_a".into())),
("field_b", 42u32.into()),
],
),
),
(
Content::from("tuple_variant"),
Content::TupleVariant(
"Foo",
0,
"variant_c",
vec![(Content::String("value_a".into())), (42u32.into())],
),
),
(Content::from("empty_array"), Content::Seq(vec![])),
(Content::from("empty_object"), Content::Map(vec![])),
(Content::from("array"), Content::Seq(vec![true.into()])),
(
Content::from("object"),
Content::Map(vec![("foo".into(), true.into())]),
),
(
Content::from("array_of_objects"),
Content::Seq(vec![Content::Struct(
"MyType",
vec![
("foo", Content::from("bar".to_string())),
("bar", Content::from("xxx".to_string())),
],
)]),
),
(
Content::from("unit_variant"),
Content::UnitVariant("Stuff", 0, "value"),
),
(Content::from("u8"), Content::U8(8)),
(Content::from("u16"), Content::U16(16)),
(Content::from("u32"), Content::U32(32)),
(Content::from("u64"), Content::U64(64)),
(Content::from("u128"), Content::U128(128)),
(Content::from("i8"), Content::I8(8)),
(Content::from("i16"), Content::I16(16)),
(Content::from("i32"), Content::I32(32)),
(Content::from("i64"), Content::I64(64)),
(Content::from("i128"), Content::I128(128)),
(Content::from("f32"), Content::F32(32.0)),
(Content::from("f64"), Content::F64(64.0)),
(Content::from("char"), Content::Char('A')),
(Content::from("bytes"), Content::Bytes(b"hehe".to_vec())),
(Content::from("null"), Content::None),
(Content::from("unit"), Content::Unit),
(
Content::from("crazy_string"),
Content::String((0u8..=126).map(|x| x as char).collect()),
),
]);
let json = to_string_pretty(&content);
crate::assert_snapshot!(&json, @r##"
{
"is_alive": true,
"newtype_variant": {
"variant_a": {
"field_a": "value_a",
"field_b": 42
}
},
"struct_variant": {
"variant_b": {
"field_a": "value_a",
"field_b": 42
}
},
"tuple_variant": {
"variant_c": [
"value_a",
42
]
},
"empty_array": [],
"empty_object": {},
"array": [
true
],
"object": {
"foo": true
},
"array_of_objects": [
{
"foo": "bar",
"bar": "xxx"
}
],
"unit_variant": "value",
"u8": 8,
"u16": 16,
"u32": 32,
"u64": 64,
"u128": 128,
"i8": 8,
"i16": 16,
"i32": 32,
"i64": 64,
"i128": 128,
"f32": 32.0,
"f64": 64.0,
"char": "A",
"bytes": [
104,
101,
104,
101
],
"null": null,
"unit": null,
"crazy_string": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
}
"##);
}
+404
View File
@@ -0,0 +1,404 @@
//! This module implements a generic `Content` type that can hold
//! runtime typed data.
//!
//! It's modelled after serde's data format but it's in fact possible to use
//! this independently of serde. The `yaml` and `json` support implemented
//! here works without serde. Only `yaml` has an implemented parser but since
//! YAML is a superset of JSON insta instead currently parses JSON via the
//! YAML implementation.
pub mod json;
#[cfg(feature = "serde")]
mod serialization;
pub mod yaml;
#[cfg(feature = "serde")]
pub use serialization::*;
use std::fmt;
/// An internal error type for content related errors.
#[derive(Debug)]
pub enum Error {
FailedParsingYaml(std::path::PathBuf),
UnexpectedDataType,
MissingField,
FileIo(std::io::Error, std::path::PathBuf),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::FailedParsingYaml(p) => {
f.write_str(format!("Failed parsing the YAML from {:?}", p.display()).as_str())
}
Error::UnexpectedDataType => {
f.write_str("The present data type wasn't what was expected")
}
Error::MissingField => f.write_str("A required field was missing"),
Error::FileIo(e, p) => {
f.write_str(format!("File error for {:?}: {}", p.display(), e).as_str())
}
}
}
}
impl std::error::Error for Error {}
/// 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`] 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<String>`
/// 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 [`Content::as_str`] method will
/// automatically resolve such internal wrappers.
///
/// If you do need to pattern match you should use the
/// [`Content::resolve_inner`] method to resolve such internal wrappers.
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Content {
Bool(bool),
U8(u8),
U16(u16),
U32(u32),
U64(u64),
U128(u128),
I8(i8),
I16(i16),
I32(i32),
I64(i64),
I128(i128),
F32(f32),
F64(f64),
Char(char),
String(String),
Bytes(Vec<u8>),
#[doc(hidden)]
None,
#[doc(hidden)]
Some(Box<Content>),
#[doc(hidden)]
Unit,
#[doc(hidden)]
UnitStruct(&'static str),
#[doc(hidden)]
UnitVariant(&'static str, u32, &'static str),
#[doc(hidden)]
NewtypeStruct(&'static str, Box<Content>),
#[doc(hidden)]
NewtypeVariant(&'static str, u32, &'static str, Box<Content>),
Seq(Vec<Content>),
#[doc(hidden)]
Tuple(Vec<Content>),
#[doc(hidden)]
TupleStruct(&'static str, Vec<Content>),
#[doc(hidden)]
TupleVariant(&'static str, u32, &'static str, Vec<Content>),
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)>,
),
}
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!(u128, U128);
impl_from!(i8, I8);
impl_from!(i16, I16);
impl_from!(i32, I32);
impl_from!(i64, I64);
impl_from!(i128, I128);
impl_from!(f32, F32);
impl_from!(f64, F64);
impl_from!(char, Char);
impl_from!(String, String);
impl_from!(Vec<u8>, 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<Option<String>>`
/// 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,
}
}
/// Mutable version of [`Self::resolve_inner`].
pub fn resolve_inner_mut(&mut self) -> &mut Content {
match *self {
Content::Some(ref mut v)
| Content::NewtypeStruct(_, ref mut v)
| Content::NewtypeVariant(_, _, _, ref mut v) => v.resolve_inner_mut(),
ref mut 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 {
matches!(self.resolve_inner(), Content::None | Content::Unit)
}
/// Returns the value as bool
pub fn as_bool(&self) -> Option<bool> {
match *self.resolve_inner() {
Content::Bool(val) => Some(val),
_ => None,
}
}
/// Returns the value as u64
pub fn as_u64(&self) -> Option<u64> {
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::U128(v) => {
let rv = v as u64;
if rv as u128 == v {
Some(rv)
} else {
None
}
}
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),
Content::I128(v) => {
let rv = v as u64;
if rv as i128 == v {
Some(rv)
} else {
None
}
}
_ => None,
}
}
/// Returns the value as u128
pub fn as_u128(&self) -> Option<u128> {
match *self.resolve_inner() {
Content::U128(v) => Some(v),
Content::I128(v) if v >= 0 => Some(v as u128),
_ => self.as_u64().map(u128::from),
}
}
/// Returns the value as i64
pub fn as_i64(&self) -> Option<i64> {
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::U128(v) => {
let rv = v as i64;
if rv as u128 == 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),
Content::I128(v) => {
let rv = v as i64;
if rv as i128 == v {
Some(rv)
} else {
None
}
}
_ => None,
}
}
/// Returns the value as i128
pub fn as_i128(&self) -> Option<i128> {
match *self.resolve_inner() {
Content::U128(v) => {
let rv = v as i128;
if rv as u128 == v {
Some(rv)
} else {
None
}
}
Content::I128(v) => Some(v),
_ => self.as_i64().map(i128::from),
}
}
/// Returns the value as f64
pub fn as_f64(&self) -> Option<f64> {
match *self.resolve_inner() {
Content::F32(v) => Some(f64::from(v)),
Content::F64(v) => Some(v),
_ => None,
}
}
/// Recursively walks the content structure mutably.
///
/// The callback is invoked for every content in the tree.
pub fn walk<F: FnMut(&mut Content) -> 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);
}
}
_ => {}
}
}
}
+643
View File
@@ -0,0 +1,643 @@
use std::cmp::Ordering;
use std::marker::PhantomData;
use crate::content::Content;
use serde::{ser, Serialize, Serializer};
#[derive(PartialEq, Debug)]
pub enum Key<'a> {
Bool(bool),
U64(u64),
I64(i64),
F64(f64),
U128(u128),
I128(i128),
Str(&'a str),
Bytes(&'a [u8]),
Other,
}
impl Key<'_> {
/// Needed because [`std::mem::discriminant`] is not [`Ord`]
fn discriminant(&self) -> usize {
match self {
Key::Bool(_) => 1,
Key::U64(_) => 2,
Key::I64(_) => 3,
Key::F64(_) => 4,
Key::U128(_) => 5,
Key::I128(_) => 6,
Key::Str(_) => 7,
Key::Bytes(_) => 8,
Key::Other => 9,
}
}
}
impl Eq for Key<'_> {}
impl Ord for Key<'_> {
fn cmp(&self, other: &Self) -> Ordering {
let self_discriminant = self.discriminant();
let other_discriminant = other.discriminant();
match Ord::cmp(&self_discriminant, &other_discriminant) {
Ordering::Equal => match (self, other) {
(Key::Bool(a), Key::Bool(b)) => Ord::cmp(a, b),
(Key::U64(a), Key::U64(b)) => Ord::cmp(a, b),
(Key::I64(a), Key::I64(b)) => Ord::cmp(a, b),
(Key::F64(a), Key::F64(b)) => f64_total_cmp(*a, *b),
(Key::U128(a), Key::U128(b)) => Ord::cmp(a, b),
(Key::I128(a), Key::I128(b)) => Ord::cmp(a, b),
(Key::Str(a), Key::Str(b)) => Ord::cmp(a, b),
(Key::Bytes(a), Key::Bytes(b)) => Ord::cmp(a, b),
_ => Ordering::Equal,
},
cmp => cmp,
}
}
}
impl PartialOrd for Key<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn f64_total_cmp(left: f64, right: f64) -> Ordering {
// this is taken from f64::total_cmp on newer rust versions
let mut left = left.to_bits() as i64;
let mut right = right.to_bits() as i64;
left ^= (((left >> 63) as u64) >> 1) as i64;
right ^= (((right >> 63) as u64) >> 1) as i64;
left.cmp(&right)
}
impl Content {
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::U128(val) => Key::U128(val),
Content::I16(val) => Key::I64(val.into()),
Content::I32(val) => Key::I64(val.into()),
Content::I64(val) => Key::I64(val),
Content::I128(val) => Key::I128(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,
}
}
pub(crate) fn sort_maps(&mut self) {
self.walk(&mut |content| {
if let Content::Map(ref mut items) = content {
// try to compare by key first, if that fails compare by the
// object value. That way some values normalize, and if we
// can't normalize we still have a stable order.
items.sort_by(|a, b| match (a.0.as_key(), b.0.as_key()) {
(Key::Other, _) | (_, Key::Other) => {
a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)
}
(ref a, ref b) => a.cmp(b),
})
}
true
})
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl Serialize for Content {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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::U128(u) => serializer.serialize_u128(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::I128(i) => serializer.serialize_i128(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 (k, 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<E> {
error: PhantomData<E>,
}
impl<E> ContentSerializer<E> {
pub fn new() -> Self {
ContentSerializer { error: PhantomData }
}
}
impl<E> Serializer for ContentSerializer<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
type SerializeSeq = SerializeSeq<E>;
type SerializeTuple = SerializeTuple<E>;
type SerializeTupleStruct = SerializeTupleStruct<E>;
type SerializeTupleVariant = SerializeTupleVariant<E>;
type SerializeMap = SerializeMap<E>;
type SerializeStruct = SerializeStruct<E>;
type SerializeStructVariant = SerializeStructVariant<E>;
fn serialize_bool(self, v: bool) -> Result<Content, E> {
Ok(Content::Bool(v))
}
fn serialize_i8(self, v: i8) -> Result<Content, E> {
Ok(Content::I8(v))
}
fn serialize_i16(self, v: i16) -> Result<Content, E> {
Ok(Content::I16(v))
}
fn serialize_i32(self, v: i32) -> Result<Content, E> {
Ok(Content::I32(v))
}
fn serialize_i64(self, v: i64) -> Result<Content, E> {
Ok(Content::I64(v))
}
fn serialize_i128(self, v: i128) -> Result<Content, E> {
Ok(Content::I128(v))
}
fn serialize_u8(self, v: u8) -> Result<Content, E> {
Ok(Content::U8(v))
}
fn serialize_u16(self, v: u16) -> Result<Content, E> {
Ok(Content::U16(v))
}
fn serialize_u32(self, v: u32) -> Result<Content, E> {
Ok(Content::U32(v))
}
fn serialize_u64(self, v: u64) -> Result<Content, E> {
Ok(Content::U64(v))
}
fn serialize_u128(self, v: u128) -> Result<Content, E> {
Ok(Content::U128(v))
}
fn serialize_f32(self, v: f32) -> Result<Content, E> {
Ok(Content::F32(v))
}
fn serialize_f64(self, v: f64) -> Result<Content, E> {
Ok(Content::F64(v))
}
fn serialize_char(self, v: char) -> Result<Content, E> {
Ok(Content::Char(v))
}
fn serialize_str(self, value: &str) -> Result<Content, E> {
Ok(Content::String(value.to_owned()))
}
fn serialize_bytes(self, value: &[u8]) -> Result<Content, E> {
Ok(Content::Bytes(value.to_owned()))
}
fn serialize_none(self) -> Result<Content, E> {
Ok(Content::None)
}
fn serialize_some<T>(self, value: &T) -> Result<Content, E>
where
T: Serialize + ?Sized,
{
Ok(Content::Some(Box::new(value.serialize(self)?)))
}
fn serialize_unit(self) -> Result<Content, E> {
Ok(Content::Unit)
}
fn serialize_unit_struct(self, name: &'static str) -> Result<Content, E> {
Ok(Content::UnitStruct(name))
}
fn serialize_unit_variant(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
) -> Result<Content, E> {
Ok(Content::UnitVariant(name, variant_index, variant))
}
fn serialize_newtype_struct<T>(self, name: &'static str, value: &T) -> Result<Content, E>
where
T: Serialize + ?Sized,
{
Ok(Content::NewtypeStruct(
name,
Box::new(value.serialize(self)?),
))
}
fn serialize_newtype_variant<T>(
self,
name: &'static str,
variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Content, E>
where
T: Serialize + ?Sized,
{
Ok(Content::NewtypeVariant(
name,
variant_index,
variant,
Box::new(value.serialize(self)?),
))
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, E> {
Ok(SerializeSeq {
elements: Vec::with_capacity(len.unwrap_or(0)),
error: PhantomData,
})
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, E> {
Ok(SerializeTuple {
elements: Vec::with_capacity(len),
error: PhantomData,
})
}
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, E> {
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<Self::SerializeTupleVariant, E> {
Ok(SerializeTupleVariant {
name,
variant_index,
variant,
fields: Vec::with_capacity(len),
error: PhantomData,
})
}
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, E> {
Ok(SerializeMap {
entries: Vec::with_capacity(len.unwrap_or(0)),
key: None,
error: PhantomData,
})
}
fn serialize_struct(self, name: &'static str, len: usize) -> Result<Self::SerializeStruct, E> {
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<Self::SerializeStructVariant, E> {
Ok(SerializeStructVariant {
name,
variant_index,
variant,
fields: Vec::with_capacity(len),
error: PhantomData,
})
}
}
pub struct SerializeSeq<E> {
elements: Vec<Content>,
error: PhantomData<E>,
}
impl<E> ser::SerializeSeq for SerializeSeq<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let value = value.serialize(ContentSerializer::<E>::new())?;
self.elements.push(value);
Ok(())
}
fn end(self) -> Result<Content, E> {
Ok(Content::Seq(self.elements))
}
}
pub struct SerializeTuple<E> {
elements: Vec<Content>,
error: PhantomData<E>,
}
impl<E> ser::SerializeTuple for SerializeTuple<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let value = value.serialize(ContentSerializer::<E>::new())?;
self.elements.push(value);
Ok(())
}
fn end(self) -> Result<Content, E> {
Ok(Content::Tuple(self.elements))
}
}
pub struct SerializeTupleStruct<E> {
name: &'static str,
fields: Vec<Content>,
error: PhantomData<E>,
}
impl<E> ser::SerializeTupleStruct for SerializeTupleStruct<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let value = value.serialize(ContentSerializer::<E>::new())?;
self.fields.push(value);
Ok(())
}
fn end(self) -> Result<Content, E> {
Ok(Content::TupleStruct(self.name, self.fields))
}
}
pub struct SerializeTupleVariant<E> {
name: &'static str,
variant_index: u32,
variant: &'static str,
fields: Vec<Content>,
error: PhantomData<E>,
}
impl<E> ser::SerializeTupleVariant for SerializeTupleVariant<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let value = value.serialize(ContentSerializer::<E>::new())?;
self.fields.push(value);
Ok(())
}
fn end(self) -> Result<Content, E> {
Ok(Content::TupleVariant(
self.name,
self.variant_index,
self.variant,
self.fields,
))
}
}
pub struct SerializeMap<E> {
entries: Vec<(Content, Content)>,
key: Option<Content>,
error: PhantomData<E>,
}
impl<E> ser::SerializeMap for SerializeMap<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
fn serialize_key<T>(&mut self, key: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let key = key.serialize(ContentSerializer::<E>::new())?;
self.key = Some(key);
Ok(())
}
fn serialize_value<T>(&mut self, value: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let key = self
.key
.take()
.expect("serialize_value called before serialize_key");
let value = value.serialize(ContentSerializer::<E>::new())?;
self.entries.push((key, value));
Ok(())
}
fn end(self) -> Result<Content, E> {
Ok(Content::Map(self.entries))
}
fn serialize_entry<K, V>(&mut self, key: &K, value: &V) -> Result<(), E>
where
K: Serialize + ?Sized,
V: Serialize + ?Sized,
{
let key = key.serialize(ContentSerializer::<E>::new())?;
let value = value.serialize(ContentSerializer::<E>::new())?;
self.entries.push((key, value));
Ok(())
}
}
pub struct SerializeStruct<E> {
name: &'static str,
fields: Vec<(&'static str, Content)>,
error: PhantomData<E>,
}
impl<E> ser::SerializeStruct for SerializeStruct<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let value = value.serialize(ContentSerializer::<E>::new())?;
self.fields.push((key, value));
Ok(())
}
fn end(self) -> Result<Content, E> {
Ok(Content::Struct(self.name, self.fields))
}
}
pub struct SerializeStructVariant<E> {
name: &'static str,
variant_index: u32,
variant: &'static str,
fields: Vec<(&'static str, Content)>,
error: PhantomData<E>,
}
impl<E> ser::SerializeStructVariant for SerializeStructVariant<E>
where
E: ser::Error,
{
type Ok = Content;
type Error = E;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), E>
where
T: Serialize + ?Sized,
{
let value = value.serialize(ContentSerializer::<E>::new())?;
self.fields.push((key, value));
Ok(())
}
fn end(self) -> Result<Content, E> {
Ok(Content::StructVariant(
self.name,
self.variant_index,
self.variant,
self.fields,
))
}
}
+129
View File
@@ -0,0 +1,129 @@
pub mod vendored;
use std::path::Path;
use crate::content::{Content, Error};
use crate::content::yaml::vendored::Yaml as YamlValue;
pub fn parse_str(s: &str, filename: &Path) -> Result<Content, Error> {
let mut blobs = crate::content::yaml::vendored::yaml::YamlLoader::load_from_str(s)
.map_err(|_| Error::FailedParsingYaml(filename.to_path_buf()))?;
match (blobs.pop(), blobs.pop()) {
(Some(blob), None) => from_yaml_blob(blob, filename),
_ => Err(Error::FailedParsingYaml(filename.to_path_buf())),
}
}
fn from_yaml_blob(blob: YamlValue, filename: &Path) -> Result<Content, Error> {
match blob {
YamlValue::Null => Ok(Content::None),
YamlValue::Boolean(b) => Ok(Content::from(b)),
YamlValue::Integer(num) => Ok(Content::from(num)),
YamlValue::Real(real_str) => {
let real: f64 = real_str.parse().unwrap();
Ok(Content::from(real))
}
YamlValue::String(s) => Ok(Content::from(s)),
YamlValue::Array(seq) => {
let seq = seq
.into_iter()
.map(|x| from_yaml_blob(x, filename))
.collect::<Result<_, Error>>()?;
Ok(Content::Seq(seq))
}
YamlValue::Hash(obj) => {
let obj = obj
.into_iter()
.map(|(k, v)| Ok((from_yaml_blob(k, filename)?, from_yaml_blob(v, filename)?)))
.collect::<Result<_, Error>>()?;
Ok(Content::Map(obj))
}
YamlValue::BadValue => Err(Error::FailedParsingYaml(filename.to_path_buf())),
}
}
pub fn to_string(content: &Content) -> String {
let yaml_blob = to_yaml_value(content);
let mut buf = String::new();
let mut emitter = crate::content::yaml::vendored::emitter::YamlEmitter::new(&mut buf);
if std::env::var("INSTA_YAML_BLOCK_STYLE").as_deref() == Ok("1") {
emitter.use_literal_blocks(true);
}
emitter.dump(&yaml_blob).unwrap();
if !buf.ends_with('\n') {
buf.push('\n');
}
buf
}
fn to_yaml_value(content: &Content) -> YamlValue {
fn translate_seq(seq: &[Content]) -> YamlValue {
let seq = seq.iter().map(to_yaml_value).collect();
YamlValue::Array(seq)
}
fn translate_fields(fields: &[(&str, Content)]) -> YamlValue {
let fields = fields
.iter()
.map(|(k, v)| (YamlValue::String(k.to_string()), to_yaml_value(v)))
.collect();
YamlValue::Hash(fields)
}
match content {
Content::Bool(b) => YamlValue::Boolean(*b),
Content::U8(n) => YamlValue::Integer(i64::from(*n)),
Content::U16(n) => YamlValue::Integer(i64::from(*n)),
Content::U32(n) => YamlValue::Integer(i64::from(*n)),
Content::U64(n) => YamlValue::Real(n.to_string()),
Content::U128(n) => YamlValue::Real(n.to_string()),
Content::I8(n) => YamlValue::Integer(i64::from(*n)),
Content::I16(n) => YamlValue::Integer(i64::from(*n)),
Content::I32(n) => YamlValue::Integer(i64::from(*n)),
Content::I64(n) => YamlValue::Integer(*n),
Content::I128(n) => YamlValue::Real(n.to_string()),
Content::F32(f) => YamlValue::Real(f.to_string()),
Content::F64(f) => YamlValue::Real(f.to_string()),
Content::Char(c) => YamlValue::String(c.to_string()),
Content::String(s) => YamlValue::String(s.to_owned()),
Content::Bytes(bytes) => {
let bytes = bytes
.iter()
.map(|b| YamlValue::Integer(i64::from(*b)))
.collect();
YamlValue::Array(bytes)
}
Content::None | Content::Unit | Content::UnitStruct(_) => YamlValue::Null,
Content::Some(content) => to_yaml_value(content),
Content::UnitVariant(_, _, variant) => YamlValue::String(variant.to_string()),
Content::NewtypeStruct(_, content) => to_yaml_value(content),
Content::NewtypeVariant(_, _, variant, content) => YamlValue::Hash(vec![(
YamlValue::String(variant.to_string()),
to_yaml_value(content),
)]),
Content::Seq(seq) => translate_seq(seq),
Content::Tuple(seq) => translate_seq(seq),
Content::TupleStruct(_, seq) => translate_seq(seq),
Content::TupleVariant(_, _, variant, seq) => YamlValue::Hash(vec![(
YamlValue::String(variant.to_string()),
translate_seq(seq),
)]),
Content::Map(map) => {
let map = map
.iter()
.map(|(k, v)| (to_yaml_value(k), to_yaml_value(v)))
.collect();
YamlValue::Hash(map)
}
Content::Struct(_name, fields) => translate_fields(fields),
Content::StructVariant(_, _, variant, fields) => YamlValue::Hash(vec![(
YamlValue::String(variant.to_string()),
translate_fields(fields),
)]),
}
}
@@ -0,0 +1,743 @@
use crate::content::yaml::vendored::yaml::{Hash, Yaml};
use std::error::Error;
use std::fmt::{self, Display};
#[derive(Copy, Clone, Debug)]
pub enum EmitError {
FmtError(fmt::Error),
}
impl Error for EmitError {
fn cause(&self) -> Option<&dyn Error> {
None
}
}
impl Display for EmitError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match *self {
EmitError::FmtError(ref err) => Display::fmt(err, formatter),
}
}
}
impl From<fmt::Error> for EmitError {
fn from(f: fmt::Error) -> Self {
EmitError::FmtError(f)
}
}
pub struct YamlEmitter<'a> {
writer: &'a mut dyn fmt::Write,
best_indent: usize,
compact: bool,
use_literal_blocks: bool,
level: isize,
}
pub type EmitResult = Result<(), EmitError>;
/// From [`serialize::json`]
fn escape_str(wr: &mut dyn fmt::Write, v: &str) -> Result<(), fmt::Error> {
wr.write_str("\"")?;
let mut start = 0;
for (i, byte) in v.bytes().enumerate() {
let escaped = match byte {
b'"' => "\\\"",
b'\\' => "\\\\",
b'\x00' => "\\u0000",
b'\x01' => "\\u0001",
b'\x02' => "\\u0002",
b'\x03' => "\\u0003",
b'\x04' => "\\u0004",
b'\x05' => "\\u0005",
b'\x06' => "\\u0006",
b'\x07' => "\\u0007",
b'\x08' => "\\b",
b'\t' => "\\t",
b'\n' => "\\n",
b'\x0b' => "\\u000b",
b'\x0c' => "\\f",
b'\r' => "\\r",
b'\x0e' => "\\u000e",
b'\x0f' => "\\u000f",
b'\x10' => "\\u0010",
b'\x11' => "\\u0011",
b'\x12' => "\\u0012",
b'\x13' => "\\u0013",
b'\x14' => "\\u0014",
b'\x15' => "\\u0015",
b'\x16' => "\\u0016",
b'\x17' => "\\u0017",
b'\x18' => "\\u0018",
b'\x19' => "\\u0019",
b'\x1a' => "\\u001a",
b'\x1b' => "\\u001b",
b'\x1c' => "\\u001c",
b'\x1d' => "\\u001d",
b'\x1e' => "\\u001e",
b'\x1f' => "\\u001f",
b'\x7f' => "\\u007f",
_ => continue,
};
if start < i {
wr.write_str(&v[start..i])?;
}
wr.write_str(escaped)?;
start = i + 1;
}
if start != v.len() {
wr.write_str(&v[start..])?;
}
wr.write_str("\"")?;
Ok(())
}
impl<'a> YamlEmitter<'a> {
pub fn new(writer: &'a mut dyn fmt::Write) -> YamlEmitter<'a> {
YamlEmitter {
writer,
best_indent: 2,
compact: true,
level: -1,
use_literal_blocks: false,
}
}
pub fn use_literal_blocks(&mut self, value: bool) {
self.use_literal_blocks = value;
}
pub fn dump(&mut self, doc: &Yaml) -> EmitResult {
// write DocumentStart
writeln!(self.writer, "---")?;
self.level = -1;
self.emit_node(doc)
}
fn write_indent(&mut self) -> EmitResult {
if self.level <= 0 {
return Ok(());
}
for _ in 0..self.level {
for _ in 0..self.best_indent {
write!(self.writer, " ")?;
}
}
Ok(())
}
fn emit_node(&mut self, node: &Yaml) -> EmitResult {
match *node {
Yaml::Array(ref v) => self.emit_array(v),
Yaml::Hash(ref h) => self.emit_hash(h),
Yaml::String(ref v) => {
if need_quotes(v) {
escape_str(self.writer, v)?;
} else {
write!(self.writer, "{v}")?;
}
Ok(())
}
Yaml::Boolean(v) => {
if v {
self.writer.write_str("true")?;
} else {
self.writer.write_str("false")?;
}
Ok(())
}
Yaml::Integer(v) => {
write!(self.writer, "{v}")?;
Ok(())
}
Yaml::Real(ref v) => {
write!(self.writer, "{v}")?;
Ok(())
}
Yaml::Null | Yaml::BadValue => {
write!(self.writer, "~")?;
Ok(())
}
}
}
fn emit_array(&mut self, v: &[Yaml]) -> EmitResult {
if v.is_empty() {
write!(self.writer, "[]")?;
} else {
self.level += 1;
for (cnt, x) in v.iter().enumerate() {
if cnt > 0 {
writeln!(self.writer)?;
self.write_indent()?;
}
write!(self.writer, "-")?;
self.emit_val(true, x)?;
}
self.level -= 1;
}
Ok(())
}
fn emit_hash(&mut self, h: &Hash) -> EmitResult {
if h.is_empty() {
self.writer.write_str("{}")?;
} else {
self.level += 1;
for (cnt, (k, v)) in h.iter().enumerate() {
let complex_key = matches!(*k, Yaml::Hash(_) | Yaml::Array(_));
if cnt > 0 {
writeln!(self.writer)?;
self.write_indent()?;
}
if complex_key {
write!(self.writer, "?")?;
self.emit_val(true, k)?;
writeln!(self.writer)?;
self.write_indent()?;
write!(self.writer, ":")?;
self.emit_val(true, v)?;
} else {
self.emit_node(k)?;
write!(self.writer, ":")?;
self.emit_val(false, v)?;
}
}
self.level -= 1;
}
Ok(())
}
/// Emit a yaml as a hash or array value: i.e., which should appear
/// following a ":" or "-", either after a space, or on a new line.
/// If `inline` is true, then the preceding characters are distinct
/// and short enough to respect the compact flag.
fn emit_val(&mut self, inline: bool, val: &Yaml) -> EmitResult {
match *val {
Yaml::Array(ref v) => {
if (inline && self.compact) || v.is_empty() {
write!(self.writer, " ")?;
} else {
writeln!(self.writer)?;
self.level += 1;
self.write_indent()?;
self.level -= 1;
}
self.emit_array(v)
}
Yaml::Hash(ref h) => {
if (inline && self.compact) || h.is_empty() {
write!(self.writer, " ")?;
} else {
writeln!(self.writer)?;
self.level += 1;
self.write_indent()?;
self.level -= 1;
}
self.emit_hash(h)
}
Yaml::String(ref s) => {
if let Some(indicator) = literal_block_style(s, self.use_literal_blocks) {
self.emit_literal_block(s, indicator)
} else {
write!(self.writer, " ")?;
self.emit_node(val)
}
}
_ => {
write!(self.writer, " ")?;
self.emit_node(val)
}
}
}
/// Emits a string as a YAML literal block scalar.
///
/// Example:
/// ```yaml
/// key: |
/// first line
/// second line
/// ```
fn emit_literal_block(&mut self, s: &str, indicator: ChompingIndicator) -> EmitResult {
match indicator {
ChompingIndicator::Strip => writeln!(self.writer, " |-")?,
ChompingIndicator::Clip => writeln!(self.writer, " |")?,
ChompingIndicator::Keep => writeln!(self.writer, " |+")?,
}
let content_indent =
((self.level + 1) * self.best_indent as isize).max(self.best_indent as isize) as usize;
let trailing_newlines = s.len() - s.trim_end_matches('\n').len();
let content = s.trim_end_matches('\n');
for line in content.split('\n') {
for _ in 0..content_indent {
write!(self.writer, " ")?;
}
writeln!(self.writer, "{}", line)?;
}
if matches!(indicator, ChompingIndicator::Keep) && trailing_newlines > 1 {
for _ in 1..trailing_newlines {
writeln!(self.writer)?;
}
}
Ok(())
}
}
/// [Block chomping indicator](https://yaml.org/spec/1.2.2/#8112-block-chomping-indicator) for YAML literal block.
#[derive(Clone, Copy)]
enum ChompingIndicator {
Strip,
Clip,
Keep,
}
/// Determines whether a string can use literal block style or not.
fn literal_block_style(s: &str, use_literal_blocks: bool) -> Option<ChompingIndicator> {
if !use_literal_blocks || !s.contains('\n') {
return None;
}
let trailing_newlines = s.len() - s.trim_end_matches('\n').len();
Some(match trailing_newlines {
0 => ChompingIndicator::Strip,
1 => ChompingIndicator::Clip,
_ => ChompingIndicator::Keep,
})
}
#[allow(clippy::doc_markdown)] // \` is recognised as unbalanced backticks
/// Check if the string requires quoting.
///
/// Strings starting with any of the following characters must be quoted.
/// `:`, `&`, `*`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`
/// Strings containing any of the following characters must be quoted.
/// `{`, `}`, `\[`, `\]`, `,`, `#`, `\``
///
/// If the string contains any of the following control characters, it must be escaped with double quotes:
/// `\0`, `\x01`, `\x02`, `\x03`, `\x04`, `\x05`, `\x06`, `\a`, `\b`, `\t`, `\n, `\v, `\f`, `\r`, `\x0e`, `\x0f`, `\x10`, `\x11`, `\x12`, `\x13`, `\x14`, `\x15`, `\x16`, `\x17`, `\x18`, `\x19`, `\x1a`, `\e`, `\x1c`, `\x1d`, `\x1e`, `\x1f`, `\N`, `\_`, `\L`, `\P`
///
/// Finally, there are other cases when the strings must be quoted, no matter if you're using single or double quotes:
/// * When the string is `true` or `false` (otherwise, it would be treated as a boolean value);
/// * When the string is `null` or `~` (otherwise, it would be considered as a null value);
/// * When the string looks like a number, such as integers (e.g. `2`, `14`, etc.), floats (e.g. `2.6`, `14.9`) and exponential numbers (e.g. `12e7`, etc.) (otherwise, it would be treated as a numeric value);
/// * When the string looks like a date (e.g. `2014-12-31`) (otherwise it would be automatically converted into a Unix timestamp).
fn need_quotes(string: &str) -> bool {
fn need_quotes_spaces(string: &str) -> bool {
string.starts_with(' ') || string.ends_with(' ')
}
string.is_empty()
|| need_quotes_spaces(string)
|| string.starts_with(|character: char| {
matches!(
character,
'&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@'
)
})
|| string.contains(|character: char| {
matches!(character, ':'
| '{'
| '}'
| '['
| ']'
| ','
| '#'
| '`'
| '\"'
| '\''
| '\\'
| '\0'..='\x06'
| '\t'
| '\n'
| '\r'
| '\x0e'..='\x1a'
| '\x1c'..='\x1f')
})
|| [
// http://yaml.org/type/bool.html
// Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse
// them as string, not booleans, although it is violating the YAML 1.1 specification.
// See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088.
"yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE",
"false", "on", "On", "ON", "off", "Off", "OFF",
// http://yaml.org/type/null.html
"null", "Null", "NULL", "~",
]
.contains(&string)
|| string.starts_with('.')
|| string.starts_with("0x")
|| string.parse::<i64>().is_ok()
|| string.parse::<f64>().is_ok()
}
#[cfg(test)]
mod test {
use super::*;
use crate::content::yaml::vendored::yaml::YamlLoader;
#[test]
fn test_emit_simple() {
let s = "
# comment
a0 bb: val
a1:
b1: 4
b2: d
a2: 4 # i'm comment
a3: [1, 2, 3]
a4:
- [a1, a2]
- 2
";
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
println!("original:\n{s}");
println!("emitted:\n{writer}");
let docs_new = match YamlLoader::load_from_str(&writer) {
Ok(y) => y,
Err(e) => panic!("{}", e),
};
let doc_new = &docs_new[0];
assert_eq!(doc, doc_new);
}
#[test]
fn test_emit_complex() {
let s = r#"
catalogue:
product: &coffee { name: Coffee, price: 2.5 , unit: 1l }
product: &cookies { name: Cookies!, price: 3.40 , unit: 400g}
products:
*coffee:
amount: 4
*cookies:
amount: 4
[1,2,3,4]:
array key
2.4:
real key
true:
bool key
{}:
empty hash key
"#;
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
let docs_new = match YamlLoader::load_from_str(&writer) {
Ok(y) => y,
Err(e) => panic!("{}", e),
};
let doc_new = &docs_new[0];
assert_eq!(doc, doc_new);
}
#[test]
fn test_emit_avoid_quotes() {
let s = r#"---
a7: 你好
boolean: "true"
boolean2: "false"
date: 2014-12-31
empty_string: ""
empty_string1: " "
empty_string2: " a"
empty_string3: " a "
exp: "12e7"
field: ":"
field2: "{"
field3: "\\"
field4: "\n"
field5: "can't avoid quote"
float: "2.6"
int: "4"
nullable: "null"
nullable2: "~"
products:
"*coffee":
amount: 4
"*cookies":
amount: 4
".milk":
amount: 1
"2.4": real key
"[1,2,3,4]": array key
"true": bool key
"{}": empty hash key
x: test
y: avoid quoting here
z: string with spaces"#;
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
assert_eq!(s, writer, "actual:\n\n{writer}\n");
}
#[test]
fn emit_quoted_bools() {
let input = r#"---
string0: yes
string1: no
string2: "true"
string3: "false"
string4: "~"
null0: ~
[true, false]: real_bools
[True, TRUE, False, FALSE, y,Y,yes,Yes,YES,n,N,no,No,NO,on,On,ON,off,Off,OFF]: false_bools
bool0: true
bool1: false"#;
let expected = r#"---
string0: "yes"
string1: "no"
string2: "true"
string3: "false"
string4: "~"
null0: ~
? - true
- false
: real_bools
? - "True"
- "TRUE"
- "False"
- "FALSE"
- y
- Y
- "yes"
- "Yes"
- "YES"
- n
- N
- "no"
- "No"
- "NO"
- "on"
- "On"
- "ON"
- "off"
- "Off"
- "OFF"
: false_bools
bool0: true
bool1: false"#;
let docs = YamlLoader::load_from_str(input).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
assert_eq!(
expected, writer,
"expected:\n{expected}\nactual:\n{writer}\n"
);
}
#[test]
fn test_empty_and_nested_compact() {
let s = r#"---
a:
b:
c: hello
d: {}
e:
- f
- g
- h: []"#;
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
assert_eq!(s, writer);
}
#[test]
fn test_nested_arrays() {
let s = r#"---
a:
- b
- - c
- d
- - e
- f"#;
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
println!("original:\n{s}");
println!("emitted:\n{writer}");
assert_eq!(s, writer);
}
#[test]
fn test_deeply_nested_arrays() {
let s = r#"---
a:
- b
- - c
- d
- - e
- - f
- - e"#;
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
println!("original:\n{s}");
println!("emitted:\n{writer}");
assert_eq!(s, writer);
}
#[test]
fn test_nested_hashes() {
let s = r#"---
a:
b:
c:
d:
e: f"#;
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
println!("original:\n{s}");
println!("emitted:\n{writer}");
assert_eq!(s, writer);
}
#[test]
// Multiline string without trailing newline should use `|-`
fn test_literal_block_multiline_no_trailing_newline() {
let h = vec![(
Yaml::String("description".into()),
Yaml::String("Line 1\nLine 2\nLine 3".into()),
)];
let doc = Yaml::Hash(h);
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.use_literal_blocks(true);
emitter.dump(&doc).unwrap();
}
let expected = "---\ndescription: |-\n Line 1\n Line 2\n Line 3\n";
assert_eq!(expected, writer, "actual:\n{writer}");
}
#[test]
// Multiline string with single trailing newline should use `|`
fn test_literal_block_with_single_trailing_newline() {
let h = vec![(
Yaml::String("content".into()),
Yaml::String("Line 1\nLine 2\n".into()),
)];
let doc = Yaml::Hash(h);
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.use_literal_blocks(true);
emitter.dump(&doc).unwrap();
}
let expected = "---\ncontent: |\n Line 1\n Line 2\n";
assert_eq!(expected, writer, "actual:\n{writer}");
}
#[test]
// Multiline string with multiple trailing newlines should use `|+`
fn test_literal_block_with_multiple_trailing_newlines() {
let h = vec![(
Yaml::String("content".into()),
Yaml::String("Line 1\n\n\n".into()),
)];
let doc = Yaml::Hash(h);
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.use_literal_blocks(true);
emitter.dump(&doc).unwrap();
}
// With |+, trailing newlines are preserved
assert!(writer.contains("|+"), "should use |+ chomping: {writer}");
}
#[test]
// Single-line string should not use literal block
fn test_no_literal_block_for_single_line() {
let h = vec![(
Yaml::String("content".into()),
Yaml::String("Just a single line".into()),
)];
let doc = Yaml::Hash(h);
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.use_literal_blocks(true);
emitter.dump(&doc).unwrap();
}
let expected = "---\ncontent: Just a single line";
assert_eq!(expected, writer, "actual:\n{writer}");
}
}
@@ -0,0 +1,82 @@
//! Copyright 2015, Yuheng Chen. Apache 2 licensed.
//!
//! This vendored code used to be yaml-rust. It's intended to be replaced in
//! the next major version with a yaml-rust2 which is an actively maintained
//! version of this. Is it has different snapshot formats and different
//! MSRV requirements, we vendor it temporarily.
#![allow(unused)]
#![allow(clippy::needless_raw_strings)]
pub mod emitter;
pub mod parser;
pub mod scanner;
pub mod yaml;
pub use self::yaml::Yaml;
#[cfg(test)]
mod tests {
use super::*;
use crate::content::yaml::vendored::emitter::YamlEmitter;
use crate::content::yaml::vendored::scanner::ScanError;
use crate::content::yaml::vendored::yaml::YamlLoader;
#[test]
fn test_api() {
let s = "
# from yaml-cpp example
- name: Ogre
position: [0, 5, 0]
powers:
- name: Club
damage: 10
- name: Fist
damage: 8
- name: Dragon
position: [1, 0, 10]
powers:
- name: Fire Breath
damage: 25
- name: Claws
damage: 15
- name: Wizard
position: [5, -3, 0]
powers:
- name: Acid Rain
damage: 50
- name: Staff
damage: 3
";
let docs = YamlLoader::load_from_str(s).unwrap();
let doc = &docs[0];
assert_eq!(doc[0]["name"].as_str().unwrap(), "Ogre");
let mut writer = String::new();
{
let mut emitter = YamlEmitter::new(&mut writer);
emitter.dump(doc).unwrap();
}
assert!(!writer.is_empty());
}
fn try_fail(s: &str) -> Result<Vec<Yaml>, ScanError> {
let t = YamlLoader::load_from_str(s)?;
Ok(t)
}
#[test]
fn test_fail() {
let s = "
# syntax error
scalar
key: [1, 2]]
key1:a2
";
assert!(YamlLoader::load_from_str(s).is_err());
assert!(try_fail(s).is_err());
}
}
@@ -0,0 +1,817 @@
use crate::content::yaml::vendored::scanner::*;
use std::collections::HashMap;
#[derive(Clone, Copy, PartialEq, Debug, Eq)]
enum State {
StreamStart,
ImplicitDocumentStart,
DocumentStart,
DocumentContent,
DocumentEnd,
BlockNode,
// BlockNodeOrIndentlessSequence,
// FlowNode,
BlockSequenceFirstEntry,
BlockSequenceEntry,
IndentlessSequenceEntry,
BlockMappingFirstKey,
BlockMappingKey,
BlockMappingValue,
FlowSequenceFirstEntry,
FlowSequenceEntry,
FlowSequenceEntryMappingKey,
FlowSequenceEntryMappingValue,
FlowSequenceEntryMappingEnd,
FlowMappingFirstKey,
FlowMappingKey,
FlowMappingValue,
FlowMappingEmptyValue,
End,
}
/// [`Event`] is used with the low-level event base parsing API,
/// see [`EventReceiver`] trait.
#[derive(Clone, PartialEq, Debug, Eq)]
pub enum Event {
/// Reserved for internal use
StreamStart,
StreamEnd,
DocumentStart,
DocumentEnd,
/// Refer to an anchor ID
Alias(usize),
/// Value, style, anchor ID, tag
Scalar(String, TScalarStyle, usize, Option<TokenType>),
/// Anchor ID
SequenceStart(usize),
SequenceEnd,
/// Anchor ID
MappingStart(usize),
MappingEnd,
}
impl Event {
fn empty_scalar() -> Event {
// a null scalar
Event::Scalar("~".to_owned(), TScalarStyle::Plain, 0, None)
}
fn empty_scalar_with_anchor(anchor: usize, tag: Option<TokenType>) -> Event {
Event::Scalar("".to_owned(), TScalarStyle::Plain, anchor, tag)
}
}
#[derive(Debug)]
pub struct Parser<T> {
scanner: Scanner<T>,
states: Vec<State>,
state: State,
token: Option<Token>,
current: Option<(Event, Marker)>,
anchors: HashMap<String, usize>,
anchor_id: usize,
}
pub trait EventReceiver {
fn on_event(&mut self, ev: Event);
}
pub trait MarkedEventReceiver {
fn on_event(&mut self, ev: Event, _mark: Marker);
}
impl<R: EventReceiver> MarkedEventReceiver for R {
fn on_event(&mut self, ev: Event, _mark: Marker) {
self.on_event(ev)
}
}
pub type ParseResult = Result<(Event, Marker), ScanError>;
impl<T: Iterator<Item = char>> Parser<T> {
pub fn new(src: T) -> Parser<T> {
Parser {
scanner: Scanner::new(src),
states: Vec::new(),
state: State::StreamStart,
token: None,
current: None,
anchors: HashMap::new(),
// valid anchor_id starts from 1
anchor_id: 1,
}
}
pub fn next(&mut self) -> ParseResult {
match self.current {
None => self.parse(),
Some(_) => Ok(self.current.take().unwrap()),
}
}
fn peek_token(&mut self) -> Result<&Token, ScanError> {
match self.token {
None => {
self.token = Some(self.scan_next_token()?);
Ok(self.token.as_ref().unwrap())
}
Some(ref tok) => Ok(tok),
}
}
fn scan_next_token(&mut self) -> Result<Token, ScanError> {
let token = self.scanner.next();
match token {
None => match self.scanner.get_error() {
None => Err(ScanError::new(self.scanner.mark(), "unexpected eof")),
Some(e) => Err(e),
},
Some(tok) => Ok(tok),
}
}
fn fetch_token(&mut self) -> Token {
self.token
.take()
.expect("fetch_token needs to be preceded by peek_token")
}
fn skip(&mut self) {
self.token = None;
//self.peek_token();
}
fn pop_state(&mut self) {
self.state = self.states.pop().unwrap()
}
fn push_state(&mut self, state: State) {
self.states.push(state);
}
fn parse(&mut self) -> ParseResult {
if self.state == State::End {
return Ok((Event::StreamEnd, self.scanner.mark()));
}
let (ev, mark) = self.state_machine()?;
// println!("EV {:?}", ev);
Ok((ev, mark))
}
pub fn load<R: MarkedEventReceiver>(
&mut self,
recv: &mut R,
multi: bool,
) -> Result<(), ScanError> {
if !self.scanner.stream_started() {
let (ev, mark) = self.next()?;
assert_eq!(ev, Event::StreamStart);
recv.on_event(ev, mark);
}
if self.scanner.stream_ended() {
// XXX has parsed?
recv.on_event(Event::StreamEnd, self.scanner.mark());
return Ok(());
}
loop {
let (ev, mark) = self.next()?;
if ev == Event::StreamEnd {
recv.on_event(ev, mark);
return Ok(());
}
// clear anchors before a new document
self.anchors.clear();
self.load_document(ev, mark, recv)?;
if !multi {
break;
}
}
Ok(())
}
fn load_document<R: MarkedEventReceiver>(
&mut self,
first_ev: Event,
mark: Marker,
recv: &mut R,
) -> Result<(), ScanError> {
assert_eq!(first_ev, Event::DocumentStart);
recv.on_event(first_ev, mark);
let (ev, mark) = self.next()?;
self.load_node(ev, mark, recv)?;
// DOCUMENT-END is expected.
let (ev, mark) = self.next()?;
assert_eq!(ev, Event::DocumentEnd);
recv.on_event(ev, mark);
Ok(())
}
fn load_node<R: MarkedEventReceiver>(
&mut self,
first_ev: Event,
mark: Marker,
recv: &mut R,
) -> Result<(), ScanError> {
match first_ev {
Event::Alias(..) | Event::Scalar(..) => {
recv.on_event(first_ev, mark);
Ok(())
}
Event::SequenceStart(_) => {
recv.on_event(first_ev, mark);
self.load_sequence(recv)
}
Event::MappingStart(_) => {
recv.on_event(first_ev, mark);
self.load_mapping(recv)
}
_ => {
println!("UNREACHABLE EVENT: {first_ev:?}");
unreachable!();
}
}
}
fn load_mapping<R: MarkedEventReceiver>(&mut self, recv: &mut R) -> Result<(), ScanError> {
let (mut key_ev, mut key_mark) = self.next()?;
while key_ev != Event::MappingEnd {
// key
self.load_node(key_ev, key_mark, recv)?;
// value
let (ev, mark) = self.next()?;
self.load_node(ev, mark, recv)?;
// next event
let (ev, mark) = self.next()?;
key_ev = ev;
key_mark = mark;
}
recv.on_event(key_ev, key_mark);
Ok(())
}
fn load_sequence<R: MarkedEventReceiver>(&mut self, recv: &mut R) -> Result<(), ScanError> {
let (mut ev, mut mark) = self.next()?;
while ev != Event::SequenceEnd {
self.load_node(ev, mark, recv)?;
// next event
let (next_ev, next_mark) = self.next()?;
ev = next_ev;
mark = next_mark;
}
recv.on_event(ev, mark);
Ok(())
}
fn state_machine(&mut self) -> ParseResult {
// let next_tok = self.peek_token()?;
// println!("cur_state {:?}, next tok: {:?}", self.state, next_tok);
match self.state {
State::StreamStart => self.stream_start(),
State::ImplicitDocumentStart => self.document_start(true),
State::DocumentStart => self.document_start(false),
State::DocumentContent => self.document_content(),
State::DocumentEnd => self.document_end(),
State::BlockNode => self.parse_node(true, false),
// State::BlockNodeOrIndentlessSequence => self.parse_node(true, true),
// State::FlowNode => self.parse_node(false, false),
State::BlockMappingFirstKey => self.block_mapping_key(true),
State::BlockMappingKey => self.block_mapping_key(false),
State::BlockMappingValue => self.block_mapping_value(),
State::BlockSequenceFirstEntry => self.block_sequence_entry(true),
State::BlockSequenceEntry => self.block_sequence_entry(false),
State::FlowSequenceFirstEntry => self.flow_sequence_entry(true),
State::FlowSequenceEntry => self.flow_sequence_entry(false),
State::FlowMappingFirstKey => self.flow_mapping_key(true),
State::FlowMappingKey => self.flow_mapping_key(false),
State::FlowMappingValue => self.flow_mapping_value(false),
State::IndentlessSequenceEntry => self.indentless_sequence_entry(),
State::FlowSequenceEntryMappingKey => self.flow_sequence_entry_mapping_key(),
State::FlowSequenceEntryMappingValue => self.flow_sequence_entry_mapping_value(),
State::FlowSequenceEntryMappingEnd => self.flow_sequence_entry_mapping_end(),
State::FlowMappingEmptyValue => self.flow_mapping_value(true),
/* impossible */
State::End => unreachable!(),
}
}
fn stream_start(&mut self) -> ParseResult {
match *self.peek_token()? {
Token(mark, TokenType::StreamStart(_)) => {
self.state = State::ImplicitDocumentStart;
self.skip();
Ok((Event::StreamStart, mark))
}
Token(mark, _) => Err(ScanError::new(mark, "did not find expected <stream-start>")),
}
}
fn document_start(&mut self, implicit: bool) -> ParseResult {
if !implicit {
while let TokenType::DocumentEnd = self.peek_token()?.1 {
self.skip();
}
}
match *self.peek_token()? {
Token(mark, TokenType::StreamEnd) => {
self.state = State::End;
self.skip();
Ok((Event::StreamEnd, mark))
}
Token(_, TokenType::VersionDirective(..))
| Token(_, TokenType::TagDirective(..))
| Token(_, TokenType::DocumentStart) => {
// explicit document
self._explicit_document_start()
}
Token(mark, _) if implicit => {
self.parser_process_directives()?;
self.push_state(State::DocumentEnd);
self.state = State::BlockNode;
Ok((Event::DocumentStart, mark))
}
_ => {
// explicit document
self._explicit_document_start()
}
}
}
fn parser_process_directives(&mut self) -> Result<(), ScanError> {
loop {
match self.peek_token()?.1 {
TokenType::VersionDirective(_, _) => {
// XXX parsing with warning according to spec
//if major != 1 || minor > 2 {
// return Err(ScanError::new(tok.0,
// "found incompatible YAML document"));
//}
}
TokenType::TagDirective(..) => {
// TODO add tag directive
}
_ => break,
}
self.skip();
}
// TODO tag directive
Ok(())
}
fn _explicit_document_start(&mut self) -> ParseResult {
self.parser_process_directives()?;
match *self.peek_token()? {
Token(mark, TokenType::DocumentStart) => {
self.push_state(State::DocumentEnd);
self.state = State::DocumentContent;
self.skip();
Ok((Event::DocumentStart, mark))
}
Token(mark, _) => Err(ScanError::new(
mark,
"did not find expected <document start>",
)),
}
}
fn document_content(&mut self) -> ParseResult {
match *self.peek_token()? {
Token(mark, TokenType::VersionDirective(..))
| Token(mark, TokenType::TagDirective(..))
| Token(mark, TokenType::DocumentStart)
| Token(mark, TokenType::DocumentEnd)
| Token(mark, TokenType::StreamEnd) => {
self.pop_state();
// empty scalar
Ok((Event::empty_scalar(), mark))
}
_ => self.parse_node(true, false),
}
}
fn document_end(&mut self) -> ParseResult {
let mut _implicit = true;
let marker: Marker = match *self.peek_token()? {
Token(mark, TokenType::DocumentEnd) => {
self.skip();
_implicit = false;
mark
}
Token(mark, _) => mark,
};
// TODO tag handling
self.state = State::DocumentStart;
Ok((Event::DocumentEnd, marker))
}
fn register_anchor(&mut self, name: String, _: &Marker) -> Result<usize, ScanError> {
// anchors can be overridden/reused
// if self.anchors.contains_key(name) {
// return Err(ScanError::new(*mark,
// "while parsing anchor, found duplicated anchor"));
// }
let new_id = self.anchor_id;
self.anchor_id += 1;
self.anchors.insert(name, new_id);
Ok(new_id)
}
fn parse_node(&mut self, block: bool, indentless_sequence: bool) -> ParseResult {
let mut anchor_id = 0;
let mut tag = None;
match *self.peek_token()? {
Token(_, TokenType::Alias(_)) => {
self.pop_state();
if let Token(mark, TokenType::Alias(name)) = self.fetch_token() {
match self.anchors.get(&name) {
None => {
return Err(ScanError::new(
mark,
"while parsing node, found unknown anchor",
))
}
Some(id) => return Ok((Event::Alias(*id), mark)),
}
} else {
unreachable!()
}
}
Token(_, TokenType::Anchor(_)) => {
if let Token(mark, TokenType::Anchor(name)) = self.fetch_token() {
anchor_id = self.register_anchor(name, &mark)?;
if let TokenType::Tag(..) = self.peek_token()?.1 {
if let tg @ TokenType::Tag(..) = self.fetch_token().1 {
tag = Some(tg);
} else {
unreachable!()
}
}
} else {
unreachable!()
}
}
Token(_, TokenType::Tag(..)) => {
if let tg @ TokenType::Tag(..) = self.fetch_token().1 {
tag = Some(tg);
if let TokenType::Anchor(_) = self.peek_token()?.1 {
if let Token(mark, TokenType::Anchor(name)) = self.fetch_token() {
anchor_id = self.register_anchor(name, &mark)?;
} else {
unreachable!()
}
}
} else {
unreachable!()
}
}
_ => {}
}
match *self.peek_token()? {
Token(mark, TokenType::BlockEntry) if indentless_sequence => {
self.state = State::IndentlessSequenceEntry;
Ok((Event::SequenceStart(anchor_id), mark))
}
Token(_, TokenType::Scalar(..)) => {
self.pop_state();
if let Token(mark, TokenType::Scalar(style, v)) = self.fetch_token() {
Ok((Event::Scalar(v, style, anchor_id, tag), mark))
} else {
unreachable!()
}
}
Token(mark, TokenType::FlowSequenceStart) => {
self.state = State::FlowSequenceFirstEntry;
Ok((Event::SequenceStart(anchor_id), mark))
}
Token(mark, TokenType::FlowMappingStart) => {
self.state = State::FlowMappingFirstKey;
Ok((Event::MappingStart(anchor_id), mark))
}
Token(mark, TokenType::BlockSequenceStart) if block => {
self.state = State::BlockSequenceFirstEntry;
Ok((Event::SequenceStart(anchor_id), mark))
}
Token(mark, TokenType::BlockMappingStart) if block => {
self.state = State::BlockMappingFirstKey;
Ok((Event::MappingStart(anchor_id), mark))
}
// ex 7.2, an empty scalar can follow a secondary tag
Token(mark, _) if tag.is_some() || anchor_id > 0 => {
self.pop_state();
Ok((Event::empty_scalar_with_anchor(anchor_id, tag), mark))
}
Token(mark, _) => Err(ScanError::new(
mark,
"while parsing a node, did not find expected node content",
)),
}
}
fn block_mapping_key(&mut self, first: bool) -> ParseResult {
// skip BlockMappingStart
if first {
let _ = self.peek_token()?;
//self.marks.push(tok.0);
self.skip();
}
match *self.peek_token()? {
Token(_, TokenType::Key) => {
self.skip();
match *self.peek_token()? {
Token(mark, TokenType::Key)
| Token(mark, TokenType::Value)
| Token(mark, TokenType::BlockEnd) => {
self.state = State::BlockMappingValue;
// empty scalar
Ok((Event::empty_scalar(), mark))
}
_ => {
self.push_state(State::BlockMappingValue);
self.parse_node(true, true)
}
}
}
// XXX(chenyh): libyaml failed to parse spec 1.2, ex8.18
Token(mark, TokenType::Value) => {
self.state = State::BlockMappingValue;
Ok((Event::empty_scalar(), mark))
}
Token(mark, TokenType::BlockEnd) => {
self.pop_state();
self.skip();
Ok((Event::MappingEnd, mark))
}
Token(mark, _) => Err(ScanError::new(
mark,
"while parsing a block mapping, did not find expected key",
)),
}
}
fn block_mapping_value(&mut self) -> ParseResult {
match *self.peek_token()? {
Token(_, TokenType::Value) => {
self.skip();
match *self.peek_token()? {
Token(mark, TokenType::Key)
| Token(mark, TokenType::Value)
| Token(mark, TokenType::BlockEnd) => {
self.state = State::BlockMappingKey;
// empty scalar
Ok((Event::empty_scalar(), mark))
}
_ => {
self.push_state(State::BlockMappingKey);
self.parse_node(true, true)
}
}
}
Token(mark, _) => {
self.state = State::BlockMappingKey;
// empty scalar
Ok((Event::empty_scalar(), mark))
}
}
}
fn flow_mapping_key(&mut self, first: bool) -> ParseResult {
if first {
let _ = self.peek_token()?;
self.skip();
}
let marker: Marker =
{
match *self.peek_token()? {
Token(mark, TokenType::FlowMappingEnd) => mark,
Token(mark, _) => {
if !first {
match *self.peek_token()? {
Token(_, TokenType::FlowEntry) => self.skip(),
Token(mark, _) => return Err(ScanError::new(mark,
"while parsing a flow mapping, did not find expected ',' or '}'"))
}
}
match *self.peek_token()? {
Token(_, TokenType::Key) => {
self.skip();
match *self.peek_token()? {
Token(mark, TokenType::Value)
| Token(mark, TokenType::FlowEntry)
| Token(mark, TokenType::FlowMappingEnd) => {
self.state = State::FlowMappingValue;
return Ok((Event::empty_scalar(), mark));
}
_ => {
self.push_state(State::FlowMappingValue);
return self.parse_node(false, false);
}
}
}
Token(marker, TokenType::Value) => {
self.state = State::FlowMappingValue;
return Ok((Event::empty_scalar(), marker));
}
Token(_, TokenType::FlowMappingEnd) => (),
_ => {
self.push_state(State::FlowMappingEmptyValue);
return self.parse_node(false, false);
}
}
mark
}
}
};
self.pop_state();
self.skip();
Ok((Event::MappingEnd, marker))
}
fn flow_mapping_value(&mut self, empty: bool) -> ParseResult {
let mark: Marker = {
if empty {
let Token(mark, _) = *self.peek_token()?;
self.state = State::FlowMappingKey;
return Ok((Event::empty_scalar(), mark));
} else {
match *self.peek_token()? {
Token(marker, TokenType::Value) => {
self.skip();
match self.peek_token()?.1 {
TokenType::FlowEntry | TokenType::FlowMappingEnd => {}
_ => {
self.push_state(State::FlowMappingKey);
return self.parse_node(false, false);
}
}
marker
}
Token(marker, _) => marker,
}
}
};
self.state = State::FlowMappingKey;
Ok((Event::empty_scalar(), mark))
}
fn flow_sequence_entry(&mut self, first: bool) -> ParseResult {
// skip FlowMappingStart
if first {
let _ = self.peek_token()?;
//self.marks.push(tok.0);
self.skip();
}
match *self.peek_token()? {
Token(mark, TokenType::FlowSequenceEnd) => {
self.pop_state();
self.skip();
return Ok((Event::SequenceEnd, mark));
}
Token(_, TokenType::FlowEntry) if !first => {
self.skip();
}
Token(mark, _) if !first => {
return Err(ScanError::new(
mark,
"while parsing a flow sequence, expected ',' or ']'",
));
}
_ => { /* next */ }
}
match *self.peek_token()? {
Token(mark, TokenType::FlowSequenceEnd) => {
self.pop_state();
self.skip();
Ok((Event::SequenceEnd, mark))
}
Token(mark, TokenType::Key) => {
self.state = State::FlowSequenceEntryMappingKey;
self.skip();
Ok((Event::MappingStart(0), mark))
}
_ => {
self.push_state(State::FlowSequenceEntry);
self.parse_node(false, false)
}
}
}
fn indentless_sequence_entry(&mut self) -> ParseResult {
match *self.peek_token()? {
Token(_, TokenType::BlockEntry) => (),
Token(mark, _) => {
self.pop_state();
return Ok((Event::SequenceEnd, mark));
}
}
self.skip();
match *self.peek_token()? {
Token(mark, TokenType::BlockEntry)
| Token(mark, TokenType::Key)
| Token(mark, TokenType::Value)
| Token(mark, TokenType::BlockEnd) => {
self.state = State::IndentlessSequenceEntry;
Ok((Event::empty_scalar(), mark))
}
_ => {
self.push_state(State::IndentlessSequenceEntry);
self.parse_node(true, false)
}
}
}
fn block_sequence_entry(&mut self, first: bool) -> ParseResult {
// BLOCK-SEQUENCE-START
if first {
let _ = self.peek_token()?;
//self.marks.push(tok.0);
self.skip();
}
match *self.peek_token()? {
Token(mark, TokenType::BlockEnd) => {
self.pop_state();
self.skip();
Ok((Event::SequenceEnd, mark))
}
Token(_, TokenType::BlockEntry) => {
self.skip();
match *self.peek_token()? {
Token(mark, TokenType::BlockEntry) | Token(mark, TokenType::BlockEnd) => {
self.state = State::BlockSequenceEntry;
Ok((Event::empty_scalar(), mark))
}
_ => {
self.push_state(State::BlockSequenceEntry);
self.parse_node(true, false)
}
}
}
Token(mark, _) => Err(ScanError::new(
mark,
"while parsing a block collection, did not find expected '-' indicator",
)),
}
}
fn flow_sequence_entry_mapping_key(&mut self) -> ParseResult {
match *self.peek_token()? {
Token(mark, TokenType::Value)
| Token(mark, TokenType::FlowEntry)
| Token(mark, TokenType::FlowSequenceEnd) => {
self.skip();
self.state = State::FlowSequenceEntryMappingValue;
Ok((Event::empty_scalar(), mark))
}
_ => {
self.push_state(State::FlowSequenceEntryMappingValue);
self.parse_node(false, false)
}
}
}
fn flow_sequence_entry_mapping_value(&mut self) -> ParseResult {
match *self.peek_token()? {
Token(_, TokenType::Value) => {
self.skip();
self.state = State::FlowSequenceEntryMappingValue;
match *self.peek_token()? {
Token(mark, TokenType::FlowEntry) | Token(mark, TokenType::FlowSequenceEnd) => {
self.state = State::FlowSequenceEntryMappingEnd;
Ok((Event::empty_scalar(), mark))
}
_ => {
self.push_state(State::FlowSequenceEntryMappingEnd);
self.parse_node(false, false)
}
}
}
Token(mark, _) => {
self.state = State::FlowSequenceEntryMappingEnd;
Ok((Event::empty_scalar(), mark))
}
}
}
fn flow_sequence_entry_mapping_end(&mut self) -> ParseResult {
self.state = State::FlowSequenceEntry;
Ok((Event::MappingEnd, self.scanner.mark()))
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,692 @@
use crate::content::yaml::vendored::parser::*;
use crate::content::yaml::vendored::scanner::{Marker, ScanError, TScalarStyle, TokenType};
use std::collections::BTreeMap;
use std::f64;
use std::mem;
use std::ops::Index;
use std::string;
use std::vec;
/// A YAML node is stored as this `Yaml` enumeration, which provides an easy way to
/// access your YAML document.
#[derive(Clone, PartialEq, PartialOrd, Debug, Eq, Ord, Hash)]
pub enum Yaml {
/// Float types are stored as [`String`] and parsed on demand.
/// Note that [`f64'] does NOT implement [`Eq'] trait and can NOT be stored in [`BTreeMap`].
Real(string::String),
/// YAML int is stored as i64.
Integer(i64),
/// YAML scalar.
String(string::String),
/// YAML bool, e.g. `true` or `false`.
Boolean(bool),
/// YAML array, can be accessed as a `Vec`.
Array(self::Array),
/// YAML hash, can be accessed as a sorted vector of key/value pairs.
///
/// Insertion order will match the order of insertion into the map.
Hash(self::Hash),
/// YAML null, e.g. `null` or `~`.
Null,
/// Accessing a nonexistent node via the Index trait returns `BadValue`. This
/// simplifies error handling in the calling code. Invalid type conversion also
/// returns `BadValue`.
BadValue,
}
pub type Array = Vec<Yaml>;
pub type Hash = Vec<(Yaml, Yaml)>;
// parse f64 as Core schema
// See: https://github.com/chyh1990/yaml-rust/issues/51
fn parse_f64(v: &str) -> Option<f64> {
match v {
".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY),
"-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY),
".nan" | "NaN" | ".NAN" => Some(f64::NAN),
_ => v.parse::<f64>().ok(),
}
}
pub struct YamlLoader {
docs: Vec<Yaml>,
// states
// (current node, anchor_id) tuple
doc_stack: Vec<(Yaml, usize)>,
key_stack: Vec<Yaml>,
anchor_map: BTreeMap<usize, Yaml>,
}
impl MarkedEventReceiver for YamlLoader {
fn on_event(&mut self, ev: Event, _: Marker) {
// println!("EV {:?}", ev);
match ev {
Event::DocumentStart => {
// do nothing
}
Event::DocumentEnd => {
match self.doc_stack.len() {
// empty document
0 => self.docs.push(Yaml::BadValue),
1 => self.docs.push(self.doc_stack.pop().unwrap().0),
_ => unreachable!(),
}
}
Event::SequenceStart(aid) => {
self.doc_stack.push((Yaml::Array(Vec::new()), aid));
}
Event::SequenceEnd => {
let node = self.doc_stack.pop().unwrap();
self.insert_new_node(node);
}
Event::MappingStart(aid) => {
self.doc_stack.push((Yaml::Hash(Hash::new()), aid));
self.key_stack.push(Yaml::BadValue);
}
Event::MappingEnd => {
self.key_stack.pop().unwrap();
let node = self.doc_stack.pop().unwrap();
self.insert_new_node(node);
}
Event::Scalar(v, style, aid, tag) => {
let node = if style != TScalarStyle::Plain {
Yaml::String(v)
} else if let Some(TokenType::Tag(ref handle, ref suffix)) = tag {
// XXX tag:yaml.org,2002:
if handle == "!!" {
match suffix.as_ref() {
"bool" => {
// "true" or "false"
match v.parse::<bool>() {
Err(_) => Yaml::BadValue,
Ok(v) => Yaml::Boolean(v),
}
}
"int" => match v.parse::<i64>() {
Err(_) => Yaml::BadValue,
Ok(v) => Yaml::Integer(v),
},
"float" => match parse_f64(&v) {
Some(_) => Yaml::Real(v),
None => Yaml::BadValue,
},
"null" => match v.as_ref() {
"~" | "null" => Yaml::Null,
_ => Yaml::BadValue,
},
_ => Yaml::String(v),
}
} else {
Yaml::String(v)
}
} else {
// Datatype is not specified, or unrecognized
Yaml::from_str(&v)
};
self.insert_new_node((node, aid));
}
_ => { /* ignore */ }
}
// println!("DOC {:?}", self.doc_stack);
}
}
impl YamlLoader {
fn insert_new_node(&mut self, node: (Yaml, usize)) {
// valid anchor id starts from 1
if node.1 > 0 {
self.anchor_map.insert(node.1, node.0.clone());
}
if self.doc_stack.is_empty() {
self.doc_stack.push(node);
} else {
let parent = self.doc_stack.last_mut().unwrap();
match *parent {
(Yaml::Array(ref mut v), _) => v.push(node.0),
(Yaml::Hash(ref mut h), _) => {
let cur_key = self.key_stack.last_mut().unwrap();
// current node is a key
if cur_key.is_badvalue() {
*cur_key = node.0;
// current node is a value
} else {
let mut newkey = Yaml::BadValue;
mem::swap(&mut newkey, cur_key);
h.push((newkey, node.0));
}
}
_ => unreachable!(),
}
}
}
pub fn load_from_str(source: &str) -> Result<Vec<Yaml>, ScanError> {
let mut loader = YamlLoader {
docs: Vec::new(),
doc_stack: Vec::new(),
key_stack: Vec::new(),
anchor_map: BTreeMap::new(),
};
let mut parser = Parser::new(source.chars());
parser.load(&mut loader, true)?;
Ok(loader.docs)
}
}
macro_rules! define_as (
($name:ident, $t:ident, $yt:ident) => (
pub fn $name(&self) -> Option<$t> {
match *self {
Yaml::$yt(v) => Some(v),
_ => None
}
}
);
);
macro_rules! define_as_ref (
($name:ident, $t:ty, $yt:ident) => (
pub fn $name(&self) -> Option<$t> {
match *self {
Yaml::$yt(ref v) => Some(v),
_ => None
}
}
);
);
macro_rules! define_into (
($name:ident, $t:ty, $yt:ident) => (
pub fn $name(self) -> Option<$t> {
match self {
Yaml::$yt(v) => Some(v),
_ => None
}
}
);
);
impl Yaml {
define_as!(as_bool, bool, Boolean);
define_as!(as_i64, i64, Integer);
define_as_ref!(as_str, &str, String);
define_as_ref!(as_hash, &Hash, Hash);
define_as_ref!(as_vec, &Array, Array);
define_into!(into_bool, bool, Boolean);
define_into!(into_i64, i64, Integer);
define_into!(into_string, String, String);
define_into!(into_hash, Hash, Hash);
define_into!(into_vec, Array, Array);
pub fn is_null(&self) -> bool {
matches!(*self, Yaml::Null)
}
pub fn is_badvalue(&self) -> bool {
matches!(*self, Yaml::BadValue)
}
pub fn is_array(&self) -> bool {
matches!(*self, Yaml::Array(_))
}
pub fn as_f64(&self) -> Option<f64> {
match *self {
Yaml::Real(ref v) => parse_f64(v),
_ => None,
}
}
pub fn into_f64(self) -> Option<f64> {
match self {
Yaml::Real(ref v) => parse_f64(v),
_ => None,
}
}
}
impl Yaml {
// Not implementing FromStr because there is no possibility of Error.
// This function falls back to Yaml::String if nothing else matches.
pub fn from_str(v: &str) -> Yaml {
if let Some(rest) = v.strip_prefix("0x") {
if let Ok(i) = i64::from_str_radix(rest, 16) {
return Yaml::Integer(i);
}
}
if let Some(rest) = v.strip_prefix("0o") {
if let Ok(i) = i64::from_str_radix(rest, 8) {
return Yaml::Integer(i);
}
}
if let Some(rest) = v.strip_prefix('+') {
if let Ok(i) = rest.parse::<i64>() {
return Yaml::Integer(i);
}
}
match v {
"~" | "null" => Yaml::Null,
"true" => Yaml::Boolean(true),
"false" => Yaml::Boolean(false),
_ if v.parse::<i64>().is_ok() => Yaml::Integer(v.parse::<i64>().unwrap()),
// try parsing as f64
_ if parse_f64(v).is_some() => Yaml::Real(v.to_owned()),
_ => Yaml::String(v.to_owned()),
}
}
}
static BAD_VALUE: Yaml = Yaml::BadValue;
impl<'a> Index<&'a str> for Yaml {
type Output = Yaml;
fn index(&self, idx: &'a str) -> &Yaml {
let key = Yaml::String(idx.to_owned());
match self.as_hash() {
Some(h) => h.iter().find(|x| x.0 == key).map_or(&BAD_VALUE, |x| &x.1),
None => &BAD_VALUE,
}
}
}
impl Index<usize> for Yaml {
type Output = Yaml;
fn index(&self, idx: usize) -> &Yaml {
if let Some(v) = self.as_vec() {
v.get(idx).unwrap_or(&BAD_VALUE)
} else if let Some(v) = self.as_hash() {
let key = Yaml::Integer(idx as i64);
v.iter().find(|x| x.0 == key).map_or(&BAD_VALUE, |x| &x.1)
} else {
&BAD_VALUE
}
}
}
impl IntoIterator for Yaml {
type Item = Yaml;
type IntoIter = YamlIter;
fn into_iter(self) -> Self::IntoIter {
YamlIter {
yaml: self.into_vec().unwrap_or_default().into_iter(),
}
}
}
pub struct YamlIter {
yaml: vec::IntoIter<Yaml>,
}
impl Iterator for YamlIter {
type Item = Yaml;
fn next(&mut self) -> Option<Yaml> {
self.yaml.next()
}
}
#[cfg(test)]
mod test {
use crate::content::yaml::vendored::yaml::*;
use std::f64;
#[test]
fn test_coerce() {
let s = "---
a: 1
b: 2.2
c: [1, 2]
";
let out = YamlLoader::load_from_str(s).unwrap();
let doc = &out[0];
assert_eq!(doc["a"].as_i64().unwrap(), 1i64);
assert_eq!(doc["b"].as_f64().unwrap(), 2.2f64);
assert_eq!(doc["c"][1].as_i64().unwrap(), 2i64);
assert!(doc["d"][0].is_badvalue());
}
#[test]
fn test_empty_doc() {
let s: String = "".to_owned();
YamlLoader::load_from_str(&s).unwrap();
let s: String = "---".to_owned();
assert_eq!(YamlLoader::load_from_str(&s).unwrap()[0], Yaml::Null);
}
#[test]
fn test_parser() {
let s: String = "
# comment
a0 bb: val
a1:
b1: 4
b2: d
a2: 4 # i'm comment
a3: [1, 2, 3]
a4:
- - a1
- a2
- 2
a5: 'single_quoted'
a6: \"double_quoted\"
a7: 你好
"
.to_owned();
let out = YamlLoader::load_from_str(&s).unwrap();
let doc = &out[0];
assert_eq!(doc["a7"].as_str().unwrap(), "你好");
}
#[test]
fn test_multi_doc() {
let s = "
'a scalar'
---
'a scalar'
---
'a scalar'
";
let out = YamlLoader::load_from_str(s).unwrap();
assert_eq!(out.len(), 3);
}
#[test]
fn test_bad_anchor() {
let s = "
a1: &DEFAULT
b1: 4
b2: *DEFAULT
";
let out = YamlLoader::load_from_str(s).unwrap();
let doc = &out[0];
assert_eq!(doc["a1"]["b2"], Yaml::BadValue);
}
#[test]
fn test_github_27() {
// https://github.com/chyh1990/yaml-rust/issues/27
let s = "&a";
let out = YamlLoader::load_from_str(s).unwrap();
let doc = &out[0];
assert_eq!(doc.as_str().unwrap(), "");
}
#[test]
fn test_plain_datatype() {
let s = "
- 'string'
- \"string\"
- string
- 123
- -321
- 1.23
- -1e4
- ~
- null
- true
- false
- !!str 0
- !!int 100
- !!float 2
- !!null ~
- !!bool true
- !!bool false
- 0xFF
# bad values
- !!int string
- !!float string
- !!bool null
- !!null val
- 0o77
- [ 0xF, 0xF ]
- +12345
- [ true, false ]
";
let out = YamlLoader::load_from_str(s).unwrap();
let doc = &out[0];
assert_eq!(doc[0].as_str().unwrap(), "string");
assert_eq!(doc[1].as_str().unwrap(), "string");
assert_eq!(doc[2].as_str().unwrap(), "string");
assert_eq!(doc[3].as_i64().unwrap(), 123);
assert_eq!(doc[4].as_i64().unwrap(), -321);
assert_eq!(doc[5].as_f64().unwrap(), 1.23);
assert_eq!(doc[6].as_f64().unwrap(), -1e4);
assert!(doc[7].is_null());
assert!(doc[8].is_null());
assert!(doc[9].as_bool().unwrap());
assert!(!doc[10].as_bool().unwrap());
assert_eq!(doc[11].as_str().unwrap(), "0");
assert_eq!(doc[12].as_i64().unwrap(), 100);
assert_eq!(doc[13].as_f64().unwrap(), 2.0);
assert!(doc[14].is_null());
assert!(doc[15].as_bool().unwrap());
assert!(!doc[16].as_bool().unwrap());
assert_eq!(doc[17].as_i64().unwrap(), 255);
assert!(doc[18].is_badvalue());
assert!(doc[19].is_badvalue());
assert!(doc[20].is_badvalue());
assert!(doc[21].is_badvalue());
assert_eq!(doc[22].as_i64().unwrap(), 63);
assert_eq!(doc[23][0].as_i64().unwrap(), 15);
assert_eq!(doc[23][1].as_i64().unwrap(), 15);
assert_eq!(doc[24].as_i64().unwrap(), 12345);
assert!(doc[25][0].as_bool().unwrap());
assert!(!doc[25][1].as_bool().unwrap());
}
#[test]
fn test_bad_hyphen() {
// See: https://github.com/chyh1990/yaml-rust/issues/23
let s = "{-";
assert!(YamlLoader::load_from_str(s).is_err());
}
#[test]
fn test_issue_65() {
// See: https://github.com/chyh1990/yaml-rust/issues/65
let b = "\n\"ll\\\"ll\\\r\n\"ll\\\"ll\\\r\r\r\rU\r\r\rU";
assert!(YamlLoader::load_from_str(b).is_err());
}
#[test]
fn test_bad_docstart() {
assert!(YamlLoader::load_from_str("---This used to cause an infinite loop").is_ok());
assert_eq!(
YamlLoader::load_from_str("----"),
Ok(vec![Yaml::String(String::from("----"))])
);
assert_eq!(
YamlLoader::load_from_str("--- #here goes a comment"),
Ok(vec![Yaml::Null])
);
assert_eq!(
YamlLoader::load_from_str("---- #here goes a comment"),
Ok(vec![Yaml::String(String::from("----"))])
);
}
#[test]
fn test_plain_datatype_with_into_methods() {
let s = "
- 'string'
- \"string\"
- string
- 123
- -321
- 1.23
- -1e4
- true
- false
- !!str 0
- !!int 100
- !!float 2
- !!bool true
- !!bool false
- 0xFF
- 0o77
- +12345
- -.INF
- .NAN
- !!float .INF
";
let mut out = YamlLoader::load_from_str(s).unwrap().into_iter();
let mut doc = out.next().unwrap().into_iter();
assert_eq!(doc.next().unwrap().into_string().unwrap(), "string");
assert_eq!(doc.next().unwrap().into_string().unwrap(), "string");
assert_eq!(doc.next().unwrap().into_string().unwrap(), "string");
assert_eq!(doc.next().unwrap().into_i64().unwrap(), 123);
assert_eq!(doc.next().unwrap().into_i64().unwrap(), -321);
assert_eq!(doc.next().unwrap().into_f64().unwrap(), 1.23);
assert_eq!(doc.next().unwrap().into_f64().unwrap(), -1e4);
assert!(doc.next().unwrap().into_bool().unwrap());
assert!(!doc.next().unwrap().into_bool().unwrap());
assert_eq!(doc.next().unwrap().into_string().unwrap(), "0");
assert_eq!(doc.next().unwrap().into_i64().unwrap(), 100);
assert_eq!(doc.next().unwrap().into_f64().unwrap(), 2.0);
assert!(doc.next().unwrap().into_bool().unwrap());
assert!(!doc.next().unwrap().into_bool().unwrap());
assert_eq!(doc.next().unwrap().into_i64().unwrap(), 255);
assert_eq!(doc.next().unwrap().into_i64().unwrap(), 63);
assert_eq!(doc.next().unwrap().into_i64().unwrap(), 12345);
assert_eq!(doc.next().unwrap().into_f64().unwrap(), f64::NEG_INFINITY);
assert!(doc.next().unwrap().into_f64().is_some());
assert_eq!(doc.next().unwrap().into_f64().unwrap(), f64::INFINITY);
}
#[test]
fn test_hash_order() {
let s = "---
b: ~
a: ~
c: ~
";
let out = YamlLoader::load_from_str(s).unwrap();
let first = out.into_iter().next().unwrap();
let mut iter = first.into_hash().unwrap().into_iter();
assert_eq!(
Some((Yaml::String("b".to_owned()), Yaml::Null)),
iter.next()
);
assert_eq!(
Some((Yaml::String("a".to_owned()), Yaml::Null)),
iter.next()
);
assert_eq!(
Some((Yaml::String("c".to_owned()), Yaml::Null)),
iter.next()
);
assert_eq!(None, iter.next());
}
#[test]
fn test_integer_key() {
let s = "
0:
important: true
1:
important: false
";
let out = YamlLoader::load_from_str(s).unwrap();
let first = out.into_iter().next().unwrap();
assert!(first[0]["important"].as_bool().unwrap());
}
#[test]
fn test_indentation_equality() {
let four_spaces = YamlLoader::load_from_str(
r#"
hash:
with:
indentations
"#,
)
.unwrap()
.into_iter()
.next()
.unwrap();
let two_spaces = YamlLoader::load_from_str(
r#"
hash:
with:
indentations
"#,
)
.unwrap()
.into_iter()
.next()
.unwrap();
let one_space = YamlLoader::load_from_str(
r#"
hash:
with:
indentations
"#,
)
.unwrap()
.into_iter()
.next()
.unwrap();
let mixed_spaces = YamlLoader::load_from_str(
r#"
hash:
with:
indentations
"#,
)
.unwrap()
.into_iter()
.next()
.unwrap();
assert_eq!(four_spaces, two_spaces);
assert_eq!(two_spaces, one_space);
assert_eq!(four_spaces, mixed_spaces);
}
#[test]
fn test_two_space_indentations() {
// https://github.com/kbknapp/clap-rs/issues/965
let s = r#"
subcommands:
- server:
about: server related commands
subcommands2:
- server:
about: server related commands
subcommands3:
- server:
about: server related commands
"#;
let out = YamlLoader::load_from_str(s).unwrap();
let doc = &out.into_iter().next().unwrap();
println!("{doc:#?}");
assert_eq!(doc["subcommands"][0]["server"], Yaml::Null);
assert!(doc["subcommands2"][0]["server"].as_hash().is_some());
assert!(doc["subcommands3"][0]["server"].as_hash().is_some());
}
#[test]
fn test_recursion_depth_check_objects() {
let s = "{a:".repeat(10_000) + &"}".repeat(10_000);
assert!(YamlLoader::load_from_str(&s).is_err());
}
#[test]
fn test_recursion_depth_check_arrays() {
let s = "[".repeat(10_000) + &"]".repeat(10_000);
assert!(YamlLoader::load_from_str(&s).is_err());
}
}
+686
View File
@@ -0,0 +1,686 @@
use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::{env, fmt, fs};
use crate::utils::is_ci;
use crate::{
content::{yaml, Content},
elog,
};
use once_cell::sync::Lazy;
static WORKSPACES: Lazy<Mutex<BTreeMap<String, Arc<PathBuf>>>> =
Lazy::new(|| Mutex::new(BTreeMap::new()));
static TOOL_CONFIGS: Lazy<Mutex<BTreeMap<PathBuf, Arc<ToolConfig>>>> =
Lazy::new(|| Mutex::new(BTreeMap::new()));
pub fn get_tool_config(workspace_dir: &Path) -> Arc<ToolConfig> {
TOOL_CONFIGS
.lock()
.unwrap()
.entry(workspace_dir.to_path_buf())
.or_insert_with(|| {
ToolConfig::from_workspace(workspace_dir)
.unwrap_or_else(|e| panic!("Error building config from {workspace_dir:?}: {e}"))
.into()
})
.clone()
}
/// The test runner to use.
#[cfg(feature = "_cargo_insta_internal")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum TestRunner {
Auto,
CargoTest,
Nextest,
}
#[cfg(feature = "_cargo_insta_internal")]
impl TestRunner {
/// Fall back to `cargo test` if `cargo nextest` isn't installed and
/// `test_runner_fallback` is true
pub fn resolve_fallback(&self, test_runner_fallback: bool) -> &TestRunner {
use crate::utils::get_cargo;
if self == &TestRunner::Nextest
&& test_runner_fallback
&& std::process::Command::new(get_cargo())
.arg("nextest")
.arg("--version")
.output()
.map(|output| !output.status.success())
.unwrap_or(true)
{
&TestRunner::Auto
} else {
self
}
}
}
/// Controls how information is supposed to be displayed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OutputBehavior {
/// Diff only
Diff,
/// Short summary
Summary,
/// The most minimal output
Minimal,
/// No output at all
Nothing,
}
/// Unreferenced snapshots flag
#[cfg(feature = "_cargo_insta_internal")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum UnreferencedSnapshots {
Auto,
Reject,
Delete,
Warn,
Ignore,
}
/// Snapshot update flag
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SnapshotUpdate {
Always,
Auto,
Unseen,
New,
No,
Force,
}
#[derive(Debug)]
pub enum Error {
Deserialize(crate::content::Error),
Env(&'static str),
#[allow(unused)]
Config(&'static str),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Deserialize(_) => write!(f, "failed to deserialize tool config"),
Error::Env(var) => write!(f, "invalid value for env var '{var}'"),
Error::Config(var) => write!(f, "invalid value for config '{var}'"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Deserialize(ref err) => Some(err),
_ => None,
}
}
}
/// Represents a tool configuration.
#[derive(Debug, Clone)]
pub struct ToolConfig {
force_pass: bool,
require_full_match: bool,
output: OutputBehavior,
snapshot_update: SnapshotUpdate,
#[cfg(feature = "glob")]
glob_fail_fast: bool,
#[cfg(feature = "_cargo_insta_internal")]
test_runner_fallback: bool,
#[cfg(feature = "_cargo_insta_internal")]
test_runner: TestRunner,
#[cfg(feature = "_cargo_insta_internal")]
test_unreferenced: UnreferencedSnapshots,
#[cfg(feature = "_cargo_insta_internal")]
auto_review: bool,
#[cfg(feature = "_cargo_insta_internal")]
auto_accept_unseen: bool,
#[cfg(feature = "_cargo_insta_internal")]
review_include_ignored: bool,
#[cfg(feature = "_cargo_insta_internal")]
review_include_hidden: bool,
#[cfg(feature = "_cargo_insta_internal")]
review_warn_undiscovered: bool,
#[cfg(feature = "_cargo_insta_internal")]
disable_nextest_doctest: bool,
}
impl ToolConfig {
/// Loads the tool config from a cargo workspace.
pub fn from_workspace(workspace_dir: &Path) -> Result<ToolConfig, Error> {
let mut cfg = None;
for choice in &[".config/insta.yaml", "insta.yaml", ".insta.yaml"] {
let path = workspace_dir.join(choice);
match fs::read_to_string(&path) {
Ok(s) => {
cfg = Some(yaml::parse_str(&s, &path).map_err(Error::Deserialize)?);
break;
}
// ideally we would not swallow all errors here but unfortunately there are
// some cases where we cannot detect the error properly.
// Eg we can see NotADirectory here as kind, but on stable rust it cannot
// be matched on.
Err(_) => continue,
}
}
let cfg = cfg.unwrap_or_else(|| Content::Map(Default::default()));
// Support for the deprecated environment variables. This is
// implemented in a way that cargo-insta can support older and newer
// insta versions alike. Versions of `cargo-insta` <= 1.39 will set
// `INSTA_FORCE_UPDATE_SNAPSHOTS` & `INSTA_FORCE_UPDATE`.
//
// If `INSTA_FORCE_UPDATE_SNAPSHOTS` is the only env var present we emit
// a deprecation warning, later to be expanded to `INSTA_FORCE_UPDATE`.
//
// Another approach would be to pass the version of `cargo-insta` in a
// `INSTA_CARGO_INSTA_VERSION` env var, and then raise a warning unless
// running under cargo-insta <= 1.39. Though it would require adding a
// `semver` dependency to this crate or doing the version comparison
// ourselves (a tractable task...).
let force_update_old_env_vars = if let Ok("1") = env::var("INSTA_FORCE_UPDATE").as_deref() {
// Don't raise a warning yet, because recent versions of
// `cargo-insta` use this, so that it's compatible with older
// versions of `insta`.
//
// elog!("INSTA_FORCE_UPDATE is deprecated, use
// INSTA_UPDATE=force");
true
} else if let Ok("1") = env::var("INSTA_FORCE_UPDATE_SNAPSHOTS").as_deref() {
// Warn on an old envvar.
//
// There's some possibility that we're running from within an fairly
// old version of `cargo-insta` (before we added an
// `INSTA_CARGO_INSTA` env var, so we can't pick that up.) So offer
// a caveat in that case.
elog!("INSTA_FORCE_UPDATE_SNAPSHOTS is deprecated, use INSTA_UPDATE=force. (If running from `cargo insta`, no action is required; upgrading `cargo-insta` will silence this warning.)");
true
} else {
false
};
if force_update_old_env_vars {
env::set_var("INSTA_UPDATE", "force");
}
Ok(ToolConfig {
require_full_match: match env::var("INSTA_REQUIRE_FULL_MATCH").as_deref() {
Err(_) | Ok("") => resolve(&cfg, &["behavior", "require_full_match"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
Ok("0") => false,
Ok("1") => true,
_ => return Err(Error::Env("INSTA_REQUIRE_FULL_MATCH")),
},
force_pass: match env::var("INSTA_FORCE_PASS").as_deref() {
Err(_) | Ok("") => resolve(&cfg, &["behavior", "force_pass"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
Ok("0") => false,
Ok("1") => true,
_ => return Err(Error::Env("INSTA_FORCE_PASS")),
},
output: {
let env_var = env::var("INSTA_OUTPUT");
let val = match env_var.as_deref() {
Err(_) | Ok("") => resolve(&cfg, &["behavior", "output"])
.and_then(|x| x.as_str())
.unwrap_or("diff"),
Ok(val) => val,
};
match val {
"diff" => OutputBehavior::Diff,
"summary" => OutputBehavior::Summary,
"minimal" => OutputBehavior::Minimal,
"none" => OutputBehavior::Nothing,
_ => return Err(Error::Env("INSTA_OUTPUT")),
}
},
snapshot_update: {
let env_var = env::var("INSTA_UPDATE");
let val = match env_var.as_deref() {
Err(_) | Ok("") => resolve(&cfg, &["behavior", "update"])
.and_then(|x| x.as_str())
// Legacy support for the old force update config
.or(resolve(&cfg, &["behavior", "force_update"]).and_then(|x| {
elog!("`force_update: true` is deprecated in insta config files, use `update: force`");
match x.as_bool() {
Some(true) => Some("force"),
_ => None,
}
}))
.unwrap_or("auto"),
Ok(val) => val,
};
match val {
"auto" => SnapshotUpdate::Auto,
"always" | "1" => SnapshotUpdate::Always,
"new" => SnapshotUpdate::New,
"unseen" => SnapshotUpdate::Unseen,
"no" => SnapshotUpdate::No,
"force" => SnapshotUpdate::Force,
_ => return Err(Error::Env("INSTA_UPDATE")),
}
},
#[cfg(feature = "glob")]
glob_fail_fast: match env::var("INSTA_GLOB_FAIL_FAST").as_deref() {
Err(_) | Ok("") => resolve(&cfg, &["behavior", "glob_fail_fast"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
Ok("1") => true,
Ok("0") => false,
_ => return Err(Error::Env("INSTA_GLOB_FAIL_FAST")),
},
#[cfg(feature = "_cargo_insta_internal")]
test_runner: {
let env_var = env::var("INSTA_TEST_RUNNER");
match env_var.as_deref() {
Err(_) | Ok("") => resolve(&cfg, &["test", "runner"])
.and_then(|x| x.as_str())
.unwrap_or("auto"),
Ok(val) => val,
}
.parse::<TestRunner>()
.map_err(|_| Error::Env("INSTA_TEST_RUNNER"))?
},
#[cfg(feature = "_cargo_insta_internal")]
test_runner_fallback: match env::var("INSTA_TEST_RUNNER_FALLBACK").as_deref() {
Err(_) | Ok("") => resolve(&cfg, &["test", "runner_fallback"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
Ok("1") => true,
Ok("0") => false,
_ => return Err(Error::Env("INSTA_TEST_RUNNER_FALLBACK")),
},
#[cfg(feature = "_cargo_insta_internal")]
test_unreferenced: {
resolve(&cfg, &["test", "unreferenced"])
.and_then(|x| x.as_str())
.unwrap_or("ignore")
.parse::<UnreferencedSnapshots>()
.map_err(|_| Error::Config("unreferenced"))?
},
#[cfg(feature = "_cargo_insta_internal")]
auto_review: resolve(&cfg, &["test", "auto_review"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
#[cfg(feature = "_cargo_insta_internal")]
auto_accept_unseen: resolve(&cfg, &["test", "auto_accept_unseen"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
#[cfg(feature = "_cargo_insta_internal")]
review_include_hidden: resolve(&cfg, &["review", "include_hidden"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
#[cfg(feature = "_cargo_insta_internal")]
review_include_ignored: resolve(&cfg, &["review", "include_ignored"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
#[cfg(feature = "_cargo_insta_internal")]
review_warn_undiscovered: resolve(&cfg, &["review", "warn_undiscovered"])
.and_then(|x| x.as_bool())
.unwrap_or(true),
#[cfg(feature = "_cargo_insta_internal")]
disable_nextest_doctest: resolve(&cfg, &["test", "disable_nextest_doctest"])
.and_then(|x| x.as_bool())
.unwrap_or(false),
})
}
// TODO: Do we want all these methods, vs. just allowing access to the fields?
/// Should we fail if metadata doesn't match?
pub fn require_full_match(&self) -> bool {
self.require_full_match
}
/// Is insta instructed to fail in tests?
pub fn force_pass(&self) -> bool {
self.force_pass
}
/// Returns the intended output behavior for insta.
pub fn output_behavior(&self) -> OutputBehavior {
self.output
}
/// Returns the intended snapshot update behavior.
pub fn snapshot_update(&self) -> SnapshotUpdate {
self.snapshot_update
}
/// Returns whether the glob should fail fast, as snapshot failures within the glob macro will appear only at the end of execution unless `glob_fail_fast` is set.
#[cfg(feature = "glob")]
pub fn glob_fail_fast(&self) -> bool {
self.glob_fail_fast
}
}
#[cfg(feature = "_cargo_insta_internal")]
impl ToolConfig {
/// Returns the intended test runner
pub fn test_runner(&self) -> TestRunner {
self.test_runner
}
/// Whether to fallback to `cargo test` if the test runner isn't available
pub fn test_runner_fallback(&self) -> bool {
self.test_runner_fallback
}
pub fn test_unreferenced(&self) -> UnreferencedSnapshots {
self.test_unreferenced
}
/// Returns the auto review flag.
pub fn auto_review(&self) -> bool {
self.auto_review
}
/// Returns the auto accept unseen flag.
pub fn auto_accept_unseen(&self) -> bool {
self.auto_accept_unseen
}
pub fn review_include_hidden(&self) -> bool {
self.review_include_hidden
}
pub fn review_include_ignored(&self) -> bool {
self.review_include_ignored
}
pub fn review_warn_undiscovered(&self) -> bool {
self.review_warn_undiscovered
}
pub fn disable_nextest_doctest(&self) -> bool {
self.disable_nextest_doctest
}
}
/// How snapshots are supposed to be updated
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SnapshotUpdateBehavior {
/// Snapshots are updated in-place
InPlace,
/// Snapshots are placed in a new file with a .new suffix
NewFile,
/// Snapshots are not updated at all.
NoUpdate,
}
/// Returns the intended snapshot update behavior.
pub fn snapshot_update_behavior(tool_config: &ToolConfig, unseen: bool) -> SnapshotUpdateBehavior {
match tool_config.snapshot_update() {
SnapshotUpdate::Always => SnapshotUpdateBehavior::InPlace,
SnapshotUpdate::Auto => {
if is_ci() {
SnapshotUpdateBehavior::NoUpdate
} else {
SnapshotUpdateBehavior::NewFile
}
}
SnapshotUpdate::Unseen => {
if unseen {
SnapshotUpdateBehavior::NewFile
} else {
SnapshotUpdateBehavior::InPlace
}
}
SnapshotUpdate::New => SnapshotUpdateBehavior::NewFile,
SnapshotUpdate::No => SnapshotUpdateBehavior::NoUpdate,
SnapshotUpdate::Force => SnapshotUpdateBehavior::InPlace,
}
}
pub enum Workspace {
DetectWithCargo(&'static str),
UseAsIs(&'static str),
}
/// Returns the cargo workspace path for a crate manifest, like
/// `/Users/janedoe/projects/insta` when passed
/// `/Users/janedoe/projects/insta/insta/Cargo.toml`.
///
/// If `INSTA_WORKSPACE_ROOT` environment variable is set at runtime, use the value as-is.
/// If `INSTA_WORKSPACE_ROOT` environment variable is set at compile time, use the value as-is.
/// If `INSTA_WORKSPACE_ROOT` environment variable is not set, use `cargo metadata` to find the workspace root.
pub fn get_cargo_workspace(workspace: Workspace) -> Arc<PathBuf> {
// This is useful where CARGO_MANIFEST_DIR at compilation points to some
// transient location. This can easily happen when building the test in one
// directory but running it in another.
if let Ok(workspace_root) = env::var("INSTA_WORKSPACE_ROOT") {
return PathBuf::from(workspace_root).into();
}
// Distinguish if we need to run `cargo metadata`` or if we can return the workspace
// as is.
// This is useful if INSTA_WORKSPACE_ROOT was set at compile time, not pointing to
// the cargo manifest directory
let manifest_dir = match workspace {
Workspace::UseAsIs(workspace_root) => return PathBuf::from(workspace_root).into(),
Workspace::DetectWithCargo(manifest_dir) => manifest_dir,
};
WORKSPACES
.lock()
// we really do not care about poisoning here.
.unwrap()
.entry(manifest_dir.to_string())
.or_insert_with(|| {
get_cargo_workspace_from_metadata(manifest_dir).unwrap_or_else(|e| {
eprintln!("cargo metadata failed in {manifest_dir}: {e}");
eprintln!("will use manifest directory as fallback");
Arc::new(PathBuf::from(manifest_dir))
})
})
.clone()
}
fn get_cargo_workspace_from_metadata(
manifest_dir: &str,
) -> Result<Arc<PathBuf>, Box<dyn std::error::Error>> {
let output =
std::process::Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()))
.args(["metadata", "--format-version=1", "--no-deps"])
.current_dir(manifest_dir)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("command failed with {}: {stderr}", output.status).into());
}
let stdout =
std::str::from_utf8(&output.stdout).map_err(|e| format!("invalid UTF-8 in output: {e}"))?;
let docs = crate::content::yaml::vendored::yaml::YamlLoader::load_from_str(stdout)
.map_err(|e| format!("failed to parse YAML: {e}"))?;
let metadata = docs.into_iter().next().ok_or("no content found in YAML")?;
let workspace_root = metadata["workspace_root"]
.clone()
.into_string()
.ok_or("couldn't find 'workspace_root' in metadata")?;
Ok(Arc::new(workspace_root.into()))
}
#[test]
fn test_get_cargo_workspace_manifest_dir() {
let workspace = get_cargo_workspace(Workspace::DetectWithCargo(env!("CARGO_MANIFEST_DIR")));
// The absolute path of the workspace should be a valid directory
// In worktrees or other setups, the path might not end with "insta"
// but should still be a parent of the manifest directory
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
assert!(manifest_dir.starts_with(&*workspace));
}
#[test]
fn test_get_cargo_workspace_insta_workspace() {
let workspace = get_cargo_workspace(Workspace::UseAsIs("/tmp/insta_workspace_root"));
// The absolute path of the workspace, like `/tmp/insta_workspace_root`
assert!(workspace.ends_with("insta_workspace_root"));
}
#[cfg(feature = "_cargo_insta_internal")]
impl std::str::FromStr for TestRunner {
type Err = ();
fn from_str(value: &str) -> Result<TestRunner, ()> {
match value {
"auto" => Ok(TestRunner::Auto),
"cargo-test" => Ok(TestRunner::CargoTest),
"nextest" => Ok(TestRunner::Nextest),
_ => Err(()),
}
}
}
#[cfg(feature = "_cargo_insta_internal")]
impl std::str::FromStr for UnreferencedSnapshots {
type Err = ();
fn from_str(value: &str) -> Result<UnreferencedSnapshots, ()> {
match value {
"auto" => Ok(UnreferencedSnapshots::Auto),
"reject" | "error" => Ok(UnreferencedSnapshots::Reject),
"delete" => Ok(UnreferencedSnapshots::Delete),
"warn" => Ok(UnreferencedSnapshots::Warn),
"ignore" => Ok(UnreferencedSnapshots::Ignore),
_ => Err(()),
}
}
}
/// Memoizes a snapshot file in the reference file, as part of removing unreferenced snapshots.
pub fn memoize_snapshot_file(snapshot_file: &Path) {
if let Ok(path) = env::var("INSTA_SNAPSHOT_REFERENCES_FILE") {
let mut f = fs::OpenOptions::new()
.append(true)
.create(true)
.open(path)
.unwrap();
f.write_all(format!("{}\n", snapshot_file.display()).as_bytes())
.unwrap();
}
}
/// Appends a warning to the warnings file for cargo-insta to display after tests.
/// Best-effort: does nothing if `INSTA_WARNINGS_FILE` is not set or IO fails.
pub fn memoize_warning(message: &str) {
if let Ok(path) = env::var("INSTA_WARNINGS_FILE") {
if let Ok(mut f) = fs::OpenOptions::new().append(true).create(true).open(path) {
let _ = writeln!(f, "{}", message);
}
}
}
/// Returns the pending directory if `INSTA_PENDING_DIR` is set and non-empty.
pub fn get_pending_dir() -> Option<PathBuf> {
env::var("INSTA_PENDING_DIR")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
}
/// Tries to strip a prefix from a path, with optional normalization fallback.
///
/// First attempts direct `strip_prefix` (preserves symlinks, works for Bazel).
/// If that fails, tries normalizing both paths with `canonicalize()` and
/// retrying (handles Windows path format differences like `\\?\` prefix).
fn strip_prefix_with_fallback<'a>(
path: &'a Path,
prefix: &Path,
) -> Result<std::borrow::Cow<'a, Path>, std::path::StripPrefixError> {
// Try direct strip_prefix first. This preserves symlinks which is
// essential for Bazel's execroot setup where paths are symlinks.
if let Ok(relative) = path.strip_prefix(prefix) {
return Ok(std::borrow::Cow::Borrowed(relative));
}
// Fallback: normalize both paths with canonicalize() and try again.
// This handles Windows path format differences (e.g., `\\?\` prefix,
// 8.3 short names). On Unix, canonicalize() follows symlinks, so we
// only reach here if direct strip failed (likely not a symlink case).
if let (Ok(normalized_path), Ok(normalized_prefix)) =
(path.canonicalize(), prefix.canonicalize())
{
if let Ok(relative) = normalized_path.strip_prefix(&normalized_prefix) {
return Ok(std::borrow::Cow::Owned(relative.to_path_buf()));
}
}
// Return the original error
path.strip_prefix(prefix).map(std::borrow::Cow::Borrowed)
}
/// Computes the path for a pending snapshot file.
///
/// If `INSTA_PENDING_DIR` is set, returns a path within that directory
/// preserving the relative structure from the workspace. Otherwise returns
/// the original path unchanged.
///
/// # Panics
///
/// Panics if `INSTA_PENDING_DIR` is set but the snapshot path is outside the
/// workspace (e.g., external test paths like `../tests/lib.rs`). This is because
/// such paths would escape the pending directory.
pub fn pending_snapshot_path(workspace: &Path, original_path: &Path) -> PathBuf {
match get_pending_dir() {
Some(pending_dir) => {
// Compute relative path from workspace to original_path.
// Try direct strip first (for Bazel), fall back to normalized (for Windows).
let relative =
strip_prefix_with_fallback(original_path, workspace).unwrap_or_else(|_| {
panic!(
"INSTA_PENDING_DIR is set but snapshot path {:?} is outside \
workspace {:?}. External test paths (e.g., path = \"../tests/lib.rs\" \
in Cargo.toml) are not compatible with INSTA_PENDING_DIR because \
the relative path would escape the pending directory.",
original_path, workspace
);
});
// Check if the relative path contains ".." which would escape
// the pending directory. This happens with external test paths
// like `path = "../tests/lib.rs"` in Cargo.toml.
if relative
.components()
.any(|c| c == std::path::Component::ParentDir)
{
panic!(
"INSTA_PENDING_DIR is set but snapshot path {:?} would escape \
the pending directory (relative path contains \"..\"). \
External test paths (e.g., path = \"../tests/lib.rs\" in Cargo.toml) \
are not compatible with INSTA_PENDING_DIR.",
original_path
);
}
pending_dir.join(relative)
}
None => original_path.to_path_buf(),
}
}
fn resolve<'a>(value: &'a Content, path: &[&str]) -> Option<&'a Content> {
path.iter()
.try_fold(value, |node, segment| match node.resolve_inner() {
Content::Map(fields) => fields
.iter()
.find(|x| x.0.as_str() == Some(segment))
.map(|x| &x.1),
Content::Struct(_, fields) | Content::StructVariant(_, _, _, fields) => {
fields.iter().find(|x| x.0 == *segment).map(|x| &x.1)
}
_ => None,
})
}
+127
View File
@@ -0,0 +1,127 @@
use std::borrow::Cow;
use std::iter::FromIterator;
use std::iter::IntoIterator;
use regex::Regex;
/// Represents stored filters.
#[derive(Debug, Default, Clone)]
#[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
pub struct Filters {
rules: Vec<(Regex, String)>,
}
impl<'a, I> From<I> for Filters
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
fn from(value: I) -> Self {
Self::from_iter(value)
}
}
impl<'a> FromIterator<(&'a str, &'a str)> for Filters {
fn from_iter<I: IntoIterator<Item = (&'a str, &'a str)>>(iter: I) -> Self {
let mut rv = Filters::default();
for (regex, replacement) in iter {
rv.add(regex, replacement);
}
rv
}
}
impl Filters {
/// Adds a simple regex with a replacement.
pub(crate) fn add<S: Into<String>>(&mut self, regex: &str, replacement: S) {
self.rules.push((
Regex::new(regex).expect("invalid regex for snapshot filter rule"),
replacement.into(),
));
}
/// Clears all filters.
pub(crate) fn clear(&mut self) {
self.rules.clear();
}
/// Applies all filters to the given snapshot.
pub(crate) fn apply_to<'s>(&self, s: &'s str) -> Cow<'s, str> {
let mut rv = Cow::Borrowed(s);
for (regex, replacement) in &self.rules {
match regex.replace_all(&rv, replacement) {
Cow::Borrowed(_) => continue,
Cow::Owned(value) => rv = Cow::Owned(value),
};
}
rv
}
}
/// Strips all ANSI escape sequences from the given string.
pub(crate) fn strip_ansi_escape_codes(s: &str) -> Cow<'_, str> {
if s.contains('\x1b') {
Cow::Owned(strip_ansi_escapes::strip_str(s))
} else {
Cow::Borrowed(s)
}
}
#[test]
fn test_filters() {
let mut filters = Filters::default();
filters.add("\\bhello\\b", "[NAME]");
filters.add("(a)", "[$1]");
assert_eq!(
filters.apply_to("hellohello hello abc"),
"hellohello [NAME] [a]bc"
);
}
#[test]
fn test_static_str_array_conversion() {
let arr: [(&'static str, &'static str); 2] = [("a1", "b1"), ("a2", "b2")];
let _ = Filters::from_iter(arr);
}
#[test]
fn test_vec_str_conversion() {
let vec: Vec<(&str, &str)> = Vec::from([("a1", "b1"), ("a2", "b2")]);
let _ = Filters::from(vec);
}
#[test]
fn test_strip_ansi_escape_codes_basic() {
assert_eq!(strip_ansi_escape_codes("\x1b[31mhello\x1b[0m"), "hello");
}
#[test]
fn test_strip_ansi_escape_codes_no_codes() {
let plain = "hello world";
let result = strip_ansi_escape_codes(plain);
assert_eq!(result, "hello world");
// When there are no escape codes, the result should borrow (not allocate)
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn test_strip_ansi_escape_codes_multiple() {
assert_eq!(
strip_ansi_escape_codes("\x1b[1m\x1b[31mERROR\x1b[0m: something \x1b[32mfailed\x1b[0m"),
"ERROR: something failed"
);
}
#[test]
fn test_strip_ansi_escape_codes_256_color() {
assert_eq!(strip_ansi_escape_codes("\x1b[38;5;196mred\x1b[0m"), "red");
}
#[test]
fn test_strip_ansi_escape_codes_rgb() {
assert_eq!(
strip_ansi_escape_codes("\x1b[38;2;255;0;0mred\x1b[0m"),
"red"
);
}
+163
View File
@@ -0,0 +1,163 @@
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use globset::{GlobBuilder, GlobMatcher};
use once_cell::sync::Lazy;
use walkdir::WalkDir;
use crate::env::get_tool_config;
use crate::settings::Settings;
use crate::utils::style;
pub(crate) struct GlobCollector {
pub(crate) fail_fast: bool,
pub(crate) failed: usize,
pub(crate) show_insta_hint: bool,
}
/// the glob stack holds failure count and an indication if `cargo insta review`
/// should be run.
pub(crate) static GLOB_STACK: Lazy<Mutex<Vec<GlobCollector>>> = Lazy::new(Mutex::default);
static GLOB_FILTER: Lazy<Vec<GlobMatcher>> = Lazy::new(|| {
env::var("INSTA_GLOB_FILTER")
.unwrap_or_default()
.split(';')
.filter(|x| !x.is_empty())
.filter_map(|filter| {
GlobBuilder::new(filter)
.case_insensitive(true)
.build()
.ok()
.map(|x| x.compile_matcher())
})
.collect()
});
pub fn glob_exec<F: FnMut(&Path)>(workspace_dir: &Path, base: &Path, pattern: &str, mut f: F) {
// Check if the pattern contains parent directory traversal (../)
if pattern.contains("../") || pattern.starts_with("..") {
panic!("Parent directory traversal is not supported in glob patterns. Use the three-argument form of glob! with an explicit base directory instead.");
}
// If settings.allow_empty_glob() == true and `base` doesn't exist, skip
// everything. This is necessary as `base` is user-controlled via `glob!/3`
// and may not exist.
let mut settings = Settings::clone_current();
if settings.allow_empty_glob() && !base.exists() {
return;
}
let glob = GlobBuilder::new(pattern)
.case_insensitive(true)
.literal_separator(true)
.build()
.unwrap()
.compile_matcher();
let walker = WalkDir::new(base).follow_links(true);
let mut glob_found_matches = false;
GLOB_STACK.lock().unwrap().push(GlobCollector {
failed: 0,
show_insta_hint: false,
fail_fast: get_tool_config(workspace_dir).glob_fail_fast(),
});
// step 1: collect all matching files
let mut all_matching_files = vec![];
let mut filtered_files = vec![];
for file in walker {
let file = file.unwrap();
let path = file.path();
let stripped_path = path.strip_prefix(base).unwrap_or(path);
if !glob.is_match(stripped_path) {
continue;
}
glob_found_matches = true;
all_matching_files.push(path.to_path_buf());
// if there is a glob filter, skip if it does not match this path
if !GLOB_FILTER.is_empty() && !GLOB_FILTER.iter().any(|x| x.is_match(stripped_path)) {
eprintln!("Skipping {} due to glob filter", stripped_path.display());
continue;
}
filtered_files.push(path.to_path_buf());
}
// step 2: sort, determine common prefix and run assertions
all_matching_files.sort();
filtered_files.sort();
// Use the common prefix from ALL matching files, not just filtered ones
// This preserves the original snapshot naming when filtering
let common_prefix = find_common_prefix(&all_matching_files);
let matching_files = filtered_files;
for path in &matching_files {
settings.set_input_file(path);
// if there is a common prefix, use that stirp down the input file. That way we
// can ensure that a glob like inputs/*/*.txt with a/file.txt and b/file.txt
// does not create two identical snapshot suffixes. Instead of file.txt for both
// it would end up as a/file.txt and b/file.txt.
let snapshot_suffix = if let Some(prefix) = common_prefix {
path.strip_prefix(prefix).unwrap().as_os_str()
} else {
path.file_name().unwrap()
};
settings.set_snapshot_suffix(snapshot_suffix.to_str().unwrap());
settings.bind(|| {
f(path);
});
}
let top = GLOB_STACK.lock().unwrap().pop().unwrap();
if !glob_found_matches && !settings.allow_empty_glob() {
panic!("the glob! macro did not match any files.");
}
if top.failed > 0 {
if top.show_insta_hint {
println!(
"{hint}",
hint = style("To update snapshots run `cargo insta review`").dim(),
);
}
if top.failed > 1 {
println!(
"{hint}",
hint = style("To enable fast failing for glob! export INSTA_GLOB_FAIL_FAST=1 as environment variable.").dim()
);
}
panic!(
"glob! resulted in {} snapshot assertion failure{}",
top.failed,
if top.failed == 1 { "" } else { "s" },
);
}
}
fn find_common_prefix(sorted_paths: &[PathBuf]) -> Option<&Path> {
let first = sorted_paths.first()?;
let last = sorted_paths.last()?;
let prefix_len = first
.components()
.zip(last.components())
.take_while(|(a, b)| a == b)
.count();
if prefix_len == 0 {
None
} else {
let mut prefix = first.components();
for _ in 0..first.components().count() - prefix_len {
prefix.next_back();
}
Some(prefix.as_path())
}
}
+396
View File
@@ -0,0 +1,396 @@
#![warn(clippy::doc_markdown)]
#![warn(clippy::needless_raw_strings)]
#![warn(rustdoc::all)]
//! <div align="center">
//! <img src="https://github.com/mitsuhiko/insta/blob/master/assets/logo.png?raw=true" width="250" height="250">
//! <p><strong>insta: a snapshot testing library for Rust</strong></p>
//! </div>
//!
//! # 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]);
//! }
//! ```
//!
//! Where are the snapshots stored? Right next to your test in a folder
//! called `snapshots` as individual [`.snap` files](https://insta.rs/docs/snapshot-files/).
//!
//! Got curious?
//!
//! * [Read the introduction](https://insta.rs/docs/quickstart/)
//! * [Read the main documentation](https://insta.rs/docs/) which does not just
//! cover the API of the crate but also many of the details of how it works.
//! * There is a screencast that shows the entire workflow: [watch the insta
//! introduction screencast](https://www.youtube.com/watch?v=rCHrMqE4JOY&feature=youtu.be).
//!
//! # Writing Tests
//!
//! ```
//! use insta::assert_debug_snapshot;
//!
//! # #[allow(clippy::test_attr_in_doctest)]
//! #[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 satisfied
//! move the new files over. To simplify this workflow you can use
//! `cargo insta review` (requires
//! [`cargo-insta`](https://crates.io/crates/cargo-insta)) which will let you
//! interactively review them:
//!
//! ```text
//! $ cargo test
//! $ cargo insta review
//! ```
//!
//! # Use Without `cargo-insta`
//!
//! Note that `cargo-insta` is entirely optional. You can also just use insta
//! directly from `cargo test` and control it via the `INSTA_UPDATE` environment
//! variable — see [Updating snapshots](#updating-snapshots) for details.
//!
//! You can for instance first run the tests and not write any new snapshots, and
//! if you like them run the tests again and update them:
//!
//! ```text
//! INSTA_UPDATE=no cargo test
//! INSTA_UPDATE=always cargo test
//! ```
//!
//! # Assertion Macros
//!
//! This crate exports multiple macros for snapshot testing:
//!
//! - [`assert_snapshot!`] for comparing basic snapshots of
//! [`Display`](std::fmt::Display) outputs, often strings.
//! - [`assert_debug_snapshot!`] for comparing [`Debug`] outputs of values.
//!
//! The following macros require the use of [`serde::Serialize`]:
//!
#![cfg_attr(
feature = "csv",
doc = "- [`assert_csv_snapshot!`] for comparing CSV serialized output. (requires the `csv` feature)"
)]
#![cfg_attr(
feature = "toml",
doc = "- [`assert_toml_snapshot!`] for comparing TOML serialized output. (requires the `toml` feature)"
)]
#![cfg_attr(
feature = "yaml",
doc = "- [`assert_yaml_snapshot!`] for comparing YAML serialized output. (requires the `yaml` feature)"
)]
#![cfg_attr(
feature = "ron",
doc = "- [`assert_ron_snapshot!`] for comparing RON serialized output. (requires the `ron` feature)"
)]
#![cfg_attr(
feature = "json",
doc = "- [`assert_json_snapshot!`] for comparing JSON serialized output. (requires the `json` feature)"
)]
#![cfg_attr(
feature = "json",
doc = "- [`assert_compact_json_snapshot!`] for comparing JSON serialized output while preferring single-line formatting. (requires the `json` feature)"
)]
//!
//! For macros that work with [`serde`] this crate also permits redacting of
//! partial values. See [redactions in the
//! documentation](https://insta.rs/docs/redactions/) for more information.
//!
//! # Updating snapshots
//!
//! During test runs snapshots will be updated according to the `INSTA_UPDATE`
//! environment variable. The default is `auto` which will write snapshots for
//! any failing tests into `.snap.new` files (if no CI is detected) so that
//! [`cargo-insta`](https://crates.io/crates/cargo-insta) can pick them up for
//! review. Normally you don't have to change this variable.
//!
//! `INSTA_UPDATE` modes:
//!
//! - `auto`: the default. `no` for CI environments or `new` otherwise
//! - `new`: writes snapshots for any failing tests into `.snap.new` files,
//! pending review
//! - `always`: writes snapshots for any failing tests into `.snap` files,
//! bypassing review
//! - `unseen`: `always` for previously unseen snapshots or `new` for existing
//! snapshots
//! - `no`: does not write to snapshot files at all; just runs tests
//! - `force`: forcibly updates snapshot files, even if assertions pass
//!
//! When `new`, `auto` or `unseen` is used, the
//! [`cargo-insta`](https://crates.io/crates/cargo-insta) command can be used to
//! review the snapshots conveniently:
//!
//! ```text
//! $ 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 [read the cargo insta
//! docs](https://insta.rs/docs/cli/).
//!
//! # 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. On review, `cargo-insta` will update the string with the
//! new value.
//!
//! Example:
//!
//! ```no_run
//! # use insta::assert_snapshot;
//! assert_snapshot!(2 + 2, @"");
//! ```
//!
//! Like with normal snapshots, an initial test failure will write the proposed
//! value into a draft file (note that inline snapshots use `.pending-snap`
//! files rather than `.snap.new` files). Running `cargo insta review` will
//! review the proposed changes and update the source files on acceptance
//! automatically.
//!
//! # Features
//!
//! The following features exist:
//!
//! * `csv`: enables CSV support (via [`serde`])
//! * `json`: enables JSON support (via [`serde`])
//! * `ron`: enables RON support (via [`serde`])
//! * `toml`: enables TOML support (via [`serde`])
//! * `yaml`: enables YAML support (via [`serde`])
//! * `redactions`: enables support for redactions
//! * `filters`: enables support for filters
//! * `glob`: enables support for globbing ([`glob!`])
//! * `colors`: enables color output (enabled by default)
//!
//! For legacy reasons the `json` and `yaml` features are enabled by default in
//! limited capacity. You will receive a deprecation warning if you are not
//! opting into them but for now the macros will continue to function.
//!
//! Enabling any of the [`serde`] based formats enables the hidden `serde` feature
//! which gates some [`serde`] specific APIs such as [`Settings::set_info`].
//!
//! # Dependencies
//!
//! [`insta`] tries to be light in dependencies but this is tricky to accomplish
//! given what it tries to do.
//! By default, it currently depends on [`serde`] for
//! the [`assert_toml_snapshot!`] and [`assert_yaml_snapshot!`] macros. In the
//! future this default dependencies will be removed. To already benefit from
//! this optimization you can disable the default features and manually opt into
//! what you want.
//!
//! # Settings
//!
//! There are some settings that can be changed on a per-thread (and thus
//! per-test) basis. For more information see [Settings].
//!
//! Additionally, Insta will load a YAML config file with settings that change
//! the behavior of insta between runs. It's loaded from any of the following
//! locations: `.config/insta.yaml`, `insta.yaml` and `.insta.yaml` from the
//! workspace root. The following config options exist:
//!
//! ```yaml
//! behavior:
//! # also set by INSTA_REQUIRE_FULL_MATCH
//! require_full_match: true/false
//! # also set by INSTA_FORCE_PASS
//! force_pass: true/false
//! # also set by INSTA_OUTPUT
//! output: "diff" | "summary" | "minimal" | "none"
//! # also set by INSTA_UPDATE
//! update: "auto" | "new" | "always" | "no" | "unseen" | "force"
//! # also set by INSTA_GLOB_FAIL_FAST
//! glob_fail_fast: true/false
//!
//! # these are used by cargo insta test
//! test:
//! # also set by INSTA_TEST_RUNNER
//! # cargo-nextest binary path can be explicitly set by INSTA_CARGO_NEXTEST_BIN
//! runner: "auto" | "cargo-test" | "nextest"
//! # whether to fallback to `cargo-test` if `nextest` is not available,
//! # also set by INSTA_TEST_RUNNER_FALLBACK, default false
//! runner_fallback: true/false
//! # disable running doctests separately when using nextest
//! disable_nextest_doctest: true/false
//! # automatically assume --review was passed to cargo insta test
//! auto_review: true/false
//! # automatically assume --accept-unseen was passed to cargo insta test
//! auto_accept_unseen: true/false
//!
//! # these are used by cargo insta review
//! review:
//! # also look for snapshots in ignored folders
//! include_ignored: true / false
//! # also look for snapshots in hidden folders
//! include_hidden: true / false
//! # show a warning if undiscovered (ignored or hidden) snapshots are found.
//! # defaults to true but creates a performance hit.
//! warn_undiscovered: true / false
//! ```
//!
//! # External Diff Tools
//!
//! By default, insta displays diffs inline in unified format. You can configure
//! an external diff tool via the `INSTA_DIFF_TOOL` environment variable. When set,
//! insta writes the old and new snapshot contents to temporary files and invokes
//! your diff tool with those files as arguments.
//!
//! ```bash
//! # Use delta for syntax-highlighted diffs
//! export INSTA_DIFF_TOOL=delta
//!
//! # With arguments
//! export INSTA_DIFF_TOOL="delta --side-by-side"
//!
//! # Or any other diff tool
//! export INSTA_DIFF_TOOL=difftastic
//! ```
//!
//! This is a user-level setting (not project-level) since diff tool preference
//! varies by developer. The tool is invoked as `<tool> [args...] <old_file> <new_file>`.
//! If the tool fails to run, insta falls back to the built-in diff.
//!
//! # Optional: Faster Runs
//!
//! Insta benefits from being compiled in release mode, even as dev dependency.
//! It will compile slightly slower once, but use less memory, have faster diffs
//! and just generally be more fun to use. To achieve that, opt [`insta`] and
//! [`similar`] (the diffing library) into higher optimization in your
//! `Cargo.toml`:
//!
//! ```yaml
//! [profile.dev.package.insta]
//! opt-level = 3
//!
//! [profile.dev.package.similar]
//! opt-level = 3
//! ```
//!
//! You can also disable the default features of [`insta`] which will cut down on
//! the compile time a bit by removing some quality of life features.
//!
//! [`insta`]: https://docs.rs/insta
#![cfg_attr(docsrs, feature(doc_cfg))]
#[macro_use]
mod macros;
pub mod comparator;
mod content;
mod env;
#[doc(hidden)]
pub mod output;
mod runtime;
#[cfg(feature = "serde")]
mod serialization;
mod settings;
mod snapshot;
mod utils;
#[cfg(feature = "redactions")]
mod redaction;
#[cfg(feature = "filters")]
mod filters;
#[cfg(feature = "glob")]
mod glob;
#[cfg(test)]
mod test;
pub use crate::comparator::{Comparator, DefaultComparator};
pub use crate::settings::Settings;
pub use crate::snapshot::{MetaData, Snapshot, TextSnapshotKind};
/// Exposes some library internals.
///
/// You're unlikely to want to work with these objects but they
/// are exposed for documentation primarily.
///
/// This module does not follow the same stability guarantees as the rest of the crate and is not
/// guaranteed to be compatible between minor versions.
pub mod internals {
pub use crate::content::Content;
#[cfg(feature = "filters")]
pub use crate::filters::Filters;
pub use crate::runtime::AutoName;
pub use crate::settings::SettingsBindDropGuard;
pub use crate::snapshot::{MetaData, SnapshotContents, TextSnapshotContents};
#[cfg(feature = "redactions")]
pub use crate::{
redaction::{ContentPath, Redaction},
settings::Redactions,
};
}
// exported for cargo-insta only
#[doc(hidden)]
#[cfg(feature = "_cargo_insta_internal")]
pub mod _cargo_insta_support {
pub use crate::{
content::Error as ContentError,
env::{
get_pending_dir, Error as ToolConfigError, OutputBehavior, SnapshotUpdate, TestRunner,
ToolConfig, UnreferencedSnapshots,
},
output::SnapshotPrinter,
snapshot::PendingInlineSnapshot,
snapshot::SnapshotContents,
snapshot::TextSnapshotContents,
utils::get_cargo,
utils::is_ci,
utils::path_to_storage,
};
}
// useful for redactions
#[cfg(feature = "redactions")]
pub use crate::redaction::{dynamic_redaction, rounded_redaction, sorted_redaction};
// these are here to make the macros work
#[doc(hidden)]
pub mod _macro_support {
pub use crate::content::Content;
pub use crate::env::{get_cargo_workspace, Workspace};
pub use crate::runtime::{
assert_snapshot, with_allow_duplicates, AutoName, BinarySnapshotValue, InlineValue,
SnapshotValue,
};
pub use core::{file, line, module_path};
pub use std::{any, env, format, option_env, path, vec};
#[cfg(feature = "serde")]
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,
};
}
+614
View File
@@ -0,0 +1,614 @@
/// Utility macro to return the name of the current function.
#[doc(hidden)]
#[macro_export]
macro_rules! _function_name {
() => {{
fn f() {}
fn type_name_of_val<T>(_: T) -> &'static str {
$crate::_macro_support::any::type_name::<T>()
}
let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
while let Some(rest) = name.strip_suffix("::{{closure}}") {
name = rest;
}
name
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _get_workspace_root {
() => {{
use $crate::_macro_support::{env, option_env};
// Note the `env!("CARGO_MANIFEST_DIR")` needs to be in the macro (in
// contrast to a function in insta) because the macro needs to capture
// the value in the caller library, an exclusive property of macros.
// By default the `CARGO_MANIFEST_DIR` environment variable is used as the workspace root.
// If the `INSTA_WORKSPACE_ROOT` environment variable is set at compile time it will override the default.
// This can be useful to avoid including local paths in the binary.
const WORKSPACE_ROOT: $crate::_macro_support::Workspace = if let Some(root) = option_env!("INSTA_WORKSPACE_ROOT") {
$crate::_macro_support::Workspace::UseAsIs(root)
} else {
$crate::_macro_support::Workspace::DetectWithCargo(env!("CARGO_MANIFEST_DIR"))
};
$crate::_macro_support::get_cargo_workspace(WORKSPACE_ROOT)
}};
}
/// Asserts a [`serde::Serialize`] snapshot in CSV format.
///
/// **Feature:** `csv` (disabled by default)
///
/// This works exactly like [`assert_yaml_snapshot!`](crate::assert_yaml_snapshot!)
/// but serializes in [CSV](https://github.com/burntsushi/rust-csv) format instead of
/// YAML.
///
/// Example:
///
/// ```no_run
/// insta::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 }` or `match .. { selector => replacement }`.
/// For more information about redactions refer to the [redactions feature in
/// the guide](https://insta.rs/docs/redactions/).
///
/// The snapshot name is optional but can be provided as first argument.
#[cfg(feature = "csv")]
#[cfg_attr(docsrs, doc(cfg(feature = "csv")))]
#[macro_export]
macro_rules! assert_csv_snapshot {
($($arg:tt)*) => {
$crate::_assert_serialized_snapshot!(format=Csv, $($arg)*);
};
}
/// Asserts a [`serde::Serialize`] snapshot in TOML format.
///
/// **Feature:** `toml` (disabled by default)
///
/// This works exactly like [`assert_yaml_snapshot!`](crate::assert_yaml_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
/// insta::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 }` or `match .. { selector => replacement }`.
/// For more information about redactions refer to the [redactions feature in
/// the guide](https://insta.rs/docs/redactions/).
///
/// The snapshot name is optional but can be provided as first argument.
#[cfg(feature = "toml")]
#[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
#[macro_export]
macro_rules! assert_toml_snapshot {
($($arg:tt)*) => {
$crate::_assert_serialized_snapshot!(format=Toml, $($arg)*);
};
}
/// Asserts a [`serde::Serialize`] snapshot in YAML format.
///
/// **Feature:** `yaml`
///
/// 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!`](crate::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 comparisons.
///
/// Example:
///
/// ```no_run
/// # use insta::*;
/// assert_yaml_snapshot!(vec![1, 2, 3]);
/// ```
///
/// Unlike the [`assert_debug_snapshot!`](crate::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 }` or `match .. { selector => replacement }`.
/// For more information about redactions refer to the [redactions feature in
/// the guide](https://insta.rs/docs/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 (`@""`).
///
/// The snapshot name is optional but can be provided as first argument.
#[cfg(feature = "yaml")]
#[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
#[macro_export]
macro_rules! assert_yaml_snapshot {
($($arg:tt)*) => {
$crate::_assert_serialized_snapshot!(format=Yaml, $($arg)*);
};
}
/// Asserts a [`serde::Serialize`] snapshot in RON format.
///
/// **Feature:** `ron` (disabled by default)
///
/// This works exactly like [`assert_yaml_snapshot!`](crate::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 comparisons.
///
/// 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 }` or `match .. { selector => replacement }`.
/// For more information about redactions refer to the [redactions feature in
/// the guide](https://insta.rs/docs/redactions/).
///
/// The snapshot name is optional but can be provided as first argument.
#[cfg(feature = "ron")]
#[cfg_attr(docsrs, doc(cfg(feature = "ron")))]
#[macro_export]
macro_rules! assert_ron_snapshot {
($($arg:tt)*) => {
$crate::_assert_serialized_snapshot!(format=Ron, $($arg)*);
};
}
/// Asserts a [`serde::Serialize`] snapshot in JSON format.
///
/// **Feature:** `json`
///
/// This works exactly like [`assert_yaml_snapshot!`](crate::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 }` or `match .. { selector => replacement }`.
/// For more information about redactions refer to the [redactions feature in
/// the guide](https://insta.rs/docs/redactions/).
///
/// The snapshot name is optional but can be provided as first argument.
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
#[macro_export]
macro_rules! assert_json_snapshot {
($($arg:tt)*) => {
$crate::_assert_serialized_snapshot!(format=Json, $($arg)*);
};
}
/// Asserts a [`serde::Serialize`] snapshot in compact JSON format.
///
/// **Feature:** `json`
///
/// This works exactly like [`assert_json_snapshot!`](crate::assert_json_snapshot!) but serializes into a single
/// line for as long as the output is less than 120 characters. This can be useful
/// in cases where you are working with small result outputs but comes at the cost
/// of slightly worse diffing behavior.
///
/// Example:
///
/// ```no_run
/// # use insta::*;
/// assert_compact_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 }` or `match .. { selector => replacement }`.
/// For more information about redactions refer to the [redactions feature in
/// the guide](https://insta.rs/docs/redactions/).
///
/// The snapshot name is optional but can be provided as first argument.
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
#[macro_export]
macro_rules! assert_compact_json_snapshot {
($($arg:tt)*) => {
$crate::_assert_serialized_snapshot!(format=JsonCompact, $($arg)*);
};
}
// This macro handles optional trailing commas.
#[doc(hidden)]
#[macro_export]
macro_rules! _assert_serialized_snapshot {
// If there are redaction expressions, capture the redactions expressions
// and pass to `_assert_snapshot_base`
(format=$format:ident, $value:expr, $(match ..)? {$($k:expr => $v:expr),* $(,)?} $($arg:tt)*) => {{
let transform = |value| {
$crate::_prepare_snapshot_for_redaction!(value, {$($k => $v),*}, $format)
};
$crate::_assert_snapshot_base!(transform=transform, $value $($arg)*);
}};
// If there's a name, redaction expressions, and debug_expr, capture and pass all to `_assert_snapshot_base`
(format=$format:ident, $name:expr, $value:expr, $(match ..)? {$($k:expr => $v:expr),* $(,)?}, $debug_expr:expr $(,)?) => {{
let transform = |value| {
$crate::_prepare_snapshot_for_redaction!(value, {$($k => $v),*}, $format)
};
$crate::_assert_snapshot_base!(transform=transform, $name, $value, $debug_expr);
}};
// If there's a name and redaction expressions, capture and pass to `_assert_snapshot_base`
(format=$format:ident, $name:expr, $value:expr, $(match ..)? {$($k:expr => $v:expr),* $(,)?} $(,)?) => {{
let transform = |value| {
$crate::_prepare_snapshot_for_redaction!(value, {$($k => $v),*}, $format)
};
$crate::_assert_snapshot_base!(transform=transform, $name, $value);
}};
// Capture serialization function and pass to `_assert_snapshot_base`
//
(format=$format:ident, $($arg:tt)*) => {{
let transform = |value| {$crate::_macro_support::serialize_value(
&value,
$crate::_macro_support::SerializationFormat::$format,
)};
$crate::_assert_snapshot_base!(transform=transform, $($arg)*);
}};
}
#[cfg(feature = "redactions")]
#[doc(hidden)]
#[macro_export]
macro_rules! _prepare_snapshot_for_redaction {
($value:expr, {$($k:expr => $v:expr),*}, $format:ident) => {
{
let vec = $crate::_macro_support::vec![
$((
$crate::_macro_support::Selector::parse($k).unwrap(),
$crate::_macro_support::Redaction::from($v)
),)*
];
$crate::_macro_support::serialize_value_redacted(
&$value,
&vec,
$crate::_macro_support::SerializationFormat::$format,
)
}
}
}
#[cfg(not(feature = "redactions"))]
#[doc(hidden)]
#[macro_export]
macro_rules! _prepare_snapshot_for_redaction {
($value:expr, {$($k:expr => $v:expr),*}, $format:ident) => {
compile_error!(
"insta was compiled without redactions support. Enable the `redactions` feature."
)
};
}
/// Asserts a [`Debug`] snapshot.
///
/// The value needs to implement the [`Debug`] trait. This is useful for
/// simple values that do not implement the [`serde::Serialize`] trait, but does not
/// permit redactions.
///
/// Debug is called with `"{:#?}"`, which means this uses pretty-print.
#[macro_export]
macro_rules! assert_debug_snapshot {
($($arg:tt)*) => {
$crate::_assert_snapshot_base!(transform=|v| $crate::_macro_support::format!("{:#?}", v), $($arg)*)
};
}
/// Asserts a [`Debug`] snapshot in compact format.
///
/// The value needs to implement the [`Debug`] trait. This is useful for
/// simple values that do not implement the [`serde::Serialize`] trait, but does not
/// permit redactions.
///
/// Debug is called with `"{:?}"`, which means this does not use pretty-print.
#[macro_export]
macro_rules! assert_compact_debug_snapshot {
($($arg:tt)*) => {
$crate::_assert_snapshot_base!(transform=|v| $crate::_macro_support::format!("{:?}", v), $($arg)*)
};
}
// A helper macro which takes a closure as `transform`, and runs the closure on
// the value. This allows us to implement other macros with a small wrapper. All
// snapshot macros eventually call this macro.
//
// This macro handles optional trailing commas.
#[doc(hidden)]
#[macro_export]
macro_rules! _assert_snapshot_base {
// If there's an inline literal value, wrap the literal in a
// `ReferenceValue::Inline`, call self.
(transform=$transform:expr, $($arg:expr),*, @$snapshot:literal $(,)?) => {
$crate::_assert_snapshot_base!(
transform = $transform,
#[allow(clippy::needless_raw_string_hashes)]
$crate::_macro_support::InlineValue($snapshot),
$($arg),*
)
};
// If there's no debug_expr, use the stringified value, call self.
(transform=$transform:expr, $name:expr, $value:expr $(,)?) => {
$crate::_assert_snapshot_base!(transform = $transform, $name, $value, stringify!($value))
};
// If there's no name (and necessarily no debug expr), auto generate the
// name, call self.
(transform=$transform:expr, $value:expr $(,)?) => {
$crate::_assert_snapshot_base!(
transform = $transform,
$crate::_macro_support::AutoName,
$value
)
};
// The main macro body — every call to this macro should end up here.
(transform=$transform:expr, $name:expr, $value:expr, $debug_expr:expr $(,)?) => {
$crate::_macro_support::assert_snapshot(
(
$name,
#[allow(clippy::redundant_closure_call)]
$transform(&$value).as_str(),
).into(),
$crate::_get_workspace_root!().as_path(),
$crate::_function_name!(),
$crate::_macro_support::module_path!(),
$crate::_macro_support::file!(),
$crate::_macro_support::line!(),
$debug_expr,
)
.unwrap()
};
}
/// (Experimental)
/// Asserts a binary snapshot in the form of a [`Vec<u8>`].
///
/// The contents get stored in a separate file next to the metadata file. The extension for this
/// file must be passed as part of the name. For an implicit snapshot name just an extension can be
/// passed starting with a `.`.
///
/// This feature is considered experimental: we may make incompatible changes for the next couple
/// of versions after 1.41.
///
/// Examples:
///
/// ```no_run
/// // implicit name:
/// insta::assert_binary_snapshot!(".txt", b"abcd".to_vec());
///
/// // named:
/// insta::assert_binary_snapshot!("my_snapshot.bin", [0, 1, 2, 3].to_vec());
/// ```
#[macro_export]
macro_rules! assert_binary_snapshot {
($name_and_extension:expr, $value:expr $(,)?) => {
$crate::assert_binary_snapshot!($name_and_extension, $value, stringify!($value));
};
($name_and_extension:expr, $value:expr, $debug_expr:expr $(,)?) => {
$crate::_macro_support::assert_snapshot(
$crate::_macro_support::BinarySnapshotValue {
name_and_extension: $name_and_extension,
content: $value,
}
.into(),
$crate::_get_workspace_root!().as_path(),
$crate::_function_name!(),
$crate::_macro_support::module_path!(),
$crate::_macro_support::file!(),
$crate::_macro_support::line!(),
$debug_expr,
)
.unwrap()
};
}
/// Asserts a [`Display`](std::fmt::Display) snapshot.
///
/// This is now deprecated, replaced by the more generic [`assert_snapshot!`](crate::assert_snapshot!)
#[macro_export]
#[deprecated = "use assert_snapshot!() instead"]
macro_rules! assert_display_snapshot {
($($arg:tt)*) => {
$crate::assert_snapshot!($($arg)*)
};
}
/// Asserts a [`String`] snapshot.
///
/// This is the simplest of all assertion methods.
/// It accepts any value that implements [`Display`](std::fmt::Display).
///
/// ```no_run
/// # use insta::*;
/// // implicitly named
/// assert_snapshot!("reference value to snapshot");
/// // named
/// assert_snapshot!("snapshot_name", "reference value to snapshot");
/// // inline
/// assert_snapshot!("reference value", @"reference value");
/// ```
///
/// Optionally a third argument can be given as an expression to be stringified
/// as the debug expression. For more information on this, check out
/// <https://insta.rs/docs/snapshot-types/>.
#[macro_export]
macro_rules! assert_snapshot {
($($arg:tt)*) => {
$crate::_assert_snapshot_base!(transform=|v| $crate::_macro_support::format!("{}", v), $($arg)*)
};
}
/// Settings configuration macro.
///
/// This macro lets you bind some [`Settings`](crate::Settings) temporarily. The first argument
/// takes key value pairs that should be set, and the second is the block to
/// execute. All settings can be set (`sort_maps => value` maps to `set_sort_maps(value)`).
/// The exception are redactions, which can only be set to a vector this way.
///
/// This example:
///
/// ```rust
/// insta::with_settings!({sort_maps => true}, {
/// // run snapshot test here
/// });
/// ```
///
/// Is equivalent to the following:
///
/// ```rust
/// # use insta::Settings;
/// let mut settings = Settings::clone_current();
/// settings.set_sort_maps(true);
/// settings.bind(|| {
/// // run snapshot test here
/// });
/// ```
///
/// Note: before insta 0.17, this macro used
/// [`Settings::new`](crate::Settings::new) which meant that original settings
/// were always reset rather than extended.
#[macro_export]
macro_rules! with_settings {
({$($k:ident => $v:expr),*$(,)?}, $body:block) => {{
let mut settings = $crate::Settings::clone_current();
$(
settings._private_inner_mut().$k($v);
)*
settings.bind(|| $body)
}}
}
/// Executes a closure for all input files matching a glob.
///
/// The closure is passed the path to the file. You can use [`std::fs::read_to_string`]
/// or similar functions to load the file and process it.
///
/// ```
/// # use insta::{assert_snapshot, glob, Settings};
/// # let mut settings = Settings::clone_current();
/// # settings.set_allow_empty_glob(true);
/// # let _dropguard = settings.bind_to_scope();
/// use std::fs;
///
/// glob!("inputs/*.txt", |path| {
/// let input = fs::read_to_string(path).unwrap();
/// assert_snapshot!(input.to_uppercase());
/// });
/// ```
///
/// The `INSTA_GLOB_FILTER` environment variable can be set to only execute certain files.
/// The format of the filter is a semicolon separated filter. For instance by setting
/// `INSTA_GLOB_FILTER` to `foo-*txt;bar-*.txt` only files starting with `foo-` or `bar-`
/// end ending in `.txt` will be executed. When using `cargo-insta` the `--glob-filter`
/// option can be used instead.
///
/// Another effect of the globbing system is that snapshot failures within the glob macro
/// are deferred until the end of of it. In other words this means that each snapshot
/// assertion within the `glob!` block are reported. It can be disabled by setting
/// `INSTA_GLOB_FAIL_FAST` environment variable to `1`.
///
/// Note: Parent directory traversal patterns (e.g., "../**/*.rs") are not supported in the
/// two-argument form of this macro currently. If you need to access parent
/// directories, use the three-argument version of this macro instead.
///
/// A three-argument version of this macro allows specifying a base directory
/// for the glob to start in. This allows globbing in arbitrary directories,
/// including parent directories:
///
/// ```
/// # use insta::{assert_snapshot, glob, Settings};
/// # let mut settings = Settings::clone_current();
/// # settings.set_allow_empty_glob(true);
/// # let _dropguard = settings.bind_to_scope();
/// use std::fs;
///
/// glob!("../test_data", "inputs/*.txt", |path| {
/// let input = fs::read_to_string(path).unwrap();
/// assert_snapshot!(input.to_uppercase());
/// });
/// ```
#[cfg(feature = "glob")]
#[cfg_attr(docsrs, doc(cfg(feature = "glob")))]
#[macro_export]
macro_rules! glob {
// TODO: I think we could remove the three-argument version of this macro
// and just support a pattern such as
// `glob!("../test_data/inputs/*.txt"...`.
($base_path:expr, $glob:expr, $closure:expr) => {{
use $crate::_macro_support::path::Path;
let base = $crate::_get_workspace_root!()
.join(Path::new(file!()).parent().unwrap())
.join($base_path)
.to_path_buf();
// we try to canonicalize but on some platforms (eg: wasm) that might not work, so
// we instead silently fall back.
let base = base.canonicalize().unwrap_or_else(|_| base);
$crate::_macro_support::glob_exec(
$crate::_get_workspace_root!().as_path(),
&base,
$glob,
$closure,
);
}};
($glob:expr, $closure:expr) => {{
$crate::glob!(".", $glob, $closure)
}};
}
/// Utility macro to permit a multi-snapshot run where all snapshots match.
///
/// Within this block, insta will allow an assertion to be run more than once
/// (even inline) without generating another snapshot. Instead it will assert
/// that snapshot expressions visited more than once are matching.
///
/// ```rust
/// insta::allow_duplicates! {
/// for x in (0..10).step_by(2) {
/// let is_even = x % 2 == 0;
/// insta::assert_debug_snapshot!(is_even, @"true");
/// }
/// }
/// ```
///
/// The first snapshot assertion will be used as a gold master and every further
/// assertion will be checked against it. If they don't match the assertion will
/// fail.
#[macro_export]
macro_rules! allow_duplicates {
($($x:tt)*) => {
$crate::_macro_support::with_allow_duplicates(|| {
$($x)*
})
}
}
+543
View File
@@ -0,0 +1,543 @@
use std::borrow::Cow;
use std::process::Command;
use std::{env, path::Path, time::Duration};
use similar::{Algorithm, ChangeTag, TextDiff};
use crate::content::yaml;
use crate::snapshot::{MetaData, Snapshot, SnapshotContents};
use crate::utils::{format_rust_expression, style, term_width};
/// Snapshot printer utility.
pub struct SnapshotPrinter<'a> {
workspace_root: &'a Path,
old_snapshot: Option<&'a Snapshot>,
new_snapshot: &'a Snapshot,
old_snapshot_hint: &'a str,
new_snapshot_hint: &'a str,
show_info: bool,
show_diff: bool,
title: Option<&'a str>,
line: Option<u32>,
snapshot_file: Option<&'a Path>,
}
impl<'a> SnapshotPrinter<'a> {
pub fn new(
workspace_root: &'a Path,
old_snapshot: Option<&'a Snapshot>,
new_snapshot: &'a Snapshot,
) -> SnapshotPrinter<'a> {
SnapshotPrinter {
workspace_root,
old_snapshot,
new_snapshot,
old_snapshot_hint: "old snapshot",
new_snapshot_hint: "new results",
show_info: false,
show_diff: false,
title: None,
line: None,
snapshot_file: None,
}
}
pub fn set_snapshot_hints(&mut self, old: &'a str, new: &'a str) {
self.old_snapshot_hint = old;
self.new_snapshot_hint = new;
}
pub fn set_show_info(&mut self, yes: bool) {
self.show_info = yes;
}
pub fn set_show_diff(&mut self, yes: bool) {
self.show_diff = yes;
}
pub fn set_title(&mut self, title: Option<&'a str>) {
self.title = title;
}
pub fn set_line(&mut self, line: Option<u32>) {
self.line = line;
}
pub fn set_snapshot_file(&mut self, file: Option<&'a Path>) {
self.snapshot_file = file;
}
pub fn print(&self) {
if let Some(title) = self.title {
let width = term_width();
println!(
"{title:━^width$}",
title = style(format!(" {title} ")).bold(),
width = width
);
}
self.print_snapshot_diff();
}
fn print_snapshot_diff(&self) {
self.print_snapshot_summary();
if self.show_diff {
self.print_changeset();
} else {
self.print_snapshot();
}
}
fn print_snapshot_summary(&self) {
print_snapshot_summary(
self.workspace_root,
self.new_snapshot,
self.snapshot_file,
self.line,
);
}
fn print_info(&self) {
print_info(self.new_snapshot.metadata());
}
fn print_snapshot(&self) {
print_line(term_width());
let width = term_width();
if self.show_info {
self.print_info();
}
println!("Snapshot Contents:");
match self.new_snapshot.contents() {
SnapshotContents::Text(new_contents) => {
let new_contents = new_contents.to_string();
println!("──────┬{:─^1$}", "", width.saturating_sub(7));
for (idx, line) in new_contents.lines().enumerate() {
println!("{:>5}{}", style(idx + 1).cyan().dim().bold(), line);
}
println!("──────┴{:─^1$}", "", width.saturating_sub(7));
}
SnapshotContents::Binary(_) => {
println!(
"{}",
encode_file_link_escape(
&self
.new_snapshot
.build_binary_path(
self.snapshot_file.unwrap().with_extension("snap.new")
)
.unwrap()
)
);
}
}
}
fn print_changeset(&self) {
let width = term_width();
print_line(width);
if self.show_info {
self.print_info();
}
if let Some(old_snapshot) = self.old_snapshot {
if old_snapshot.contents().is_binary() {
println!(
"{}",
style(format_args!(
"-{}: {}",
self.old_snapshot_hint,
encode_file_link_escape(
&old_snapshot
.build_binary_path(self.snapshot_file.unwrap())
.unwrap()
),
))
.red()
);
}
}
if self.new_snapshot.contents().is_binary() {
println!(
"{}",
style(format_args!(
"+{}: {}",
self.new_snapshot_hint,
encode_file_link_escape(
&self
.new_snapshot
.build_binary_path(
self.snapshot_file.unwrap().with_extension("snap.new")
)
.unwrap()
),
))
.green()
);
}
if let Some((old, new)) = match (
self.old_snapshot.as_ref().map(|o| o.contents()),
self.new_snapshot.contents(),
) {
(Some(SnapshotContents::Binary(_)) | None, SnapshotContents::Text(new)) => {
Some((None, Some(new.to_string())))
}
(Some(SnapshotContents::Text(old)), SnapshotContents::Binary { .. }) => {
Some((Some(old.to_string()), None))
}
(Some(SnapshotContents::Text(old)), SnapshotContents::Text(new)) => {
Some((Some(old.to_string()), Some(new.to_string())))
}
_ => None,
} {
let old_text = old.as_deref().unwrap_or("");
let new_text = new.as_deref().unwrap_or("");
// Check for external diff tool
if let Ok(tool) = env::var("INSTA_DIFF_TOOL") {
if !tool.is_empty()
&& invoke_external_diff_tool(&tool, old_text, new_text, self.snapshot_file)
{
println!(); // Add spacing after external tool output
return;
}
}
let newlines_matter = newlines_matter(old_text, new_text);
let diff = TextDiff::configure()
.algorithm(Algorithm::Patience)
.timeout(Duration::from_millis(500))
.diff_lines(old_text, new_text);
if old.is_some() {
println!(
"{}",
style(format_args!("-{}", self.old_snapshot_hint)).red()
);
}
if new.is_some() {
println!(
"{}",
style(format_args!("+{}", self.new_snapshot_hint)).green()
);
}
println!("────────────┬{:─^1$}", "", width.saturating_sub(13));
// This is to make sure that binary and text snapshots are never reported as being
// equal (that would otherwise happen if the text snapshot is an empty string).
let mut has_changes = old.is_none() || new.is_none();
for (idx, group) in diff.grouped_ops(4).iter().enumerate() {
if idx > 0 {
println!("┈┈┈┈┈┈┈┈┈┈┈┈┼{:┈^1$}", "", width.saturating_sub(13));
}
for op in group {
for change in diff.iter_inline_changes(op) {
match change.tag() {
ChangeTag::Insert => {
has_changes = true;
print!(
"{:>5} {:>5}{}",
"",
style(change.new_index().unwrap() + 1).cyan().dim().bold(),
style("+").green(),
);
for &(emphasized, change) in change.values() {
let change = render_invisible(change, newlines_matter);
if emphasized {
print!("{}", style(change).green().underlined());
} else {
print!("{}", style(change).green());
}
}
}
ChangeTag::Delete => {
has_changes = true;
print!(
"{:>5} {:>5}{}",
style(change.old_index().unwrap() + 1).cyan().dim(),
"",
style("-").red(),
);
for &(emphasized, change) in change.values() {
let change = render_invisible(change, newlines_matter);
if emphasized {
print!("{}", style(change).red().underlined());
} else {
print!("{}", style(change).red());
}
}
}
ChangeTag::Equal => {
print!(
"{:>5} {:>5}",
style(change.old_index().unwrap() + 1).cyan().dim(),
style(change.new_index().unwrap() + 1).cyan().dim().bold(),
);
for &(_, change) in change.values() {
let change = render_invisible(change, newlines_matter);
print!("{}", style(change).dim());
}
}
}
if change.missing_newline() {
println!();
}
}
}
}
if !has_changes {
println!(
"{:>5} {:>5}{}",
"",
style("-").dim(),
style(" snapshots are matching").cyan(),
);
}
println!("────────────┴{:─^1$}", "", width.saturating_sub(13));
}
}
}
/// Prints the summary of a snapshot
pub fn print_snapshot_summary(
workspace_root: &Path,
snapshot: &Snapshot,
snapshot_file: Option<&Path>,
line: Option<u32>,
) {
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("<inline>").dim());
}
if let Some(ref value) = snapshot.metadata().get_relative_source(workspace_root) {
println!(
"Source: {}{}",
style(value.display()).cyan(),
line.or(
// default to old assertion line from snapshot.
snapshot.metadata().assertion_line()
)
.map(|line| format!(":{}", style(line).bold()))
.unwrap_or_default()
);
}
if let Some(ref value) = snapshot.metadata().input_file() {
println!("Input file: {}", style(value).cyan());
}
}
fn print_line(width: usize) {
println!("{:─^1$}", "", width);
}
fn trailing_newline(s: &str) -> &str {
if s.ends_with("\r\n") {
"\r\n"
} else if s.ends_with('\r') {
"\r"
} else if s.ends_with('\n') {
"\n"
} else {
""
}
}
fn detect_newlines(s: &str) -> (bool, bool, bool) {
let mut last_char = None;
let mut detected_crlf = false;
let mut detected_cr = false;
let mut detected_lf = false;
for c in s.chars() {
if c == '\n' {
if last_char.take() == Some('\r') {
detected_crlf = true;
} else {
detected_lf = true;
}
}
if last_char == Some('\r') {
detected_cr = true;
}
last_char = Some(c);
}
if last_char == Some('\r') {
detected_cr = true;
}
(detected_cr, detected_crlf, detected_lf)
}
fn newlines_matter(left: &str, right: &str) -> bool {
if trailing_newline(left) != trailing_newline(right) {
return true;
}
let (cr1, crlf1, lf1) = detect_newlines(left);
let (cr2, crlf2, lf2) = detect_newlines(right);
!matches!(
(cr1 || cr2, crlf1 || crlf2, lf1 || lf2),
(false, false, false) | (true, false, false) | (false, true, false) | (false, false, true)
)
}
fn render_invisible(s: &str, newlines_matter: bool) -> Cow<'_, str> {
if newlines_matter || s.find(&['\x1b', '\x07', '\x08', '\x7f'][..]).is_some() {
Cow::Owned(
s.replace('\r', "\r")
.replace('\n', "\n")
.replace("\r\n", "␍␊\r\n")
.replace('\x07', "")
.replace('\x08', "")
.replace('\x1b', "")
.replace('\x7f', ""),
)
} else {
Cow::Borrowed(s)
}
}
fn print_info(metadata: &MetaData) {
let width = term_width();
if let Some(expr) = metadata.expression() {
println!("Expression: {}", style(format_rust_expression(expr)));
print_line(width);
}
if let Some(descr) = metadata.description() {
println!("{descr}");
print_line(width);
}
if let Some(info) = metadata.private_info() {
let out = yaml::to_string(info);
// TODO: does the yaml output always start with '---'?
println!("{}", out.trim().strip_prefix("---").unwrap().trim_start());
print_line(width);
}
}
/// Encodes a path as an OSC-8 escape sequence. This makes it a clickable link in supported
/// terminal emulators.
fn encode_file_link_escape(path: &Path) -> String {
assert!(path.is_absolute());
format!(
"\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\",
path.display(),
path.display()
)
}
/// Invokes an external diff tool with the old and new snapshot contents.
///
/// Returns `true` if the external tool was successfully invoked, `false` if it failed
/// (in which case the caller should fall back to the built-in diff).
///
/// This function is public for testing purposes.
#[doc(hidden)]
pub fn invoke_external_diff_tool(
tool: &str,
old_content: &str,
new_content: &str,
snapshot_file: Option<&Path>,
) -> bool {
let dir = match tempfile::tempdir() {
Ok(dir) => dir,
Err(err) => {
eprintln!("warning: failed to create temp dir for diff tool: {err}");
return false;
}
};
// Use snapshot file stem for naming (helps diff tools with syntax detection).
// Fall back to generic name - these are ephemeral temp files anyway.
let base_name = snapshot_file
.and_then(|p| p.file_stem())
.and_then(|s| s.to_str())
.unwrap_or("snapshot");
let old_path = dir.path().join(format!("{base_name}.old.snap"));
let new_path = dir.path().join(format!("{base_name}.new.snap"));
// Write old content
if let Err(err) = std::fs::write(&old_path, old_content) {
eprintln!("warning: failed to write old snapshot to temp file: {err}");
return false;
}
// Write new content
if let Err(err) = std::fs::write(&new_path, new_content) {
eprintln!("warning: failed to write new snapshot to temp file: {err}");
return false;
}
// Invoke the diff tool from the temp directory so paths are relative/clean.
// We capture stdout/stderr and print them ourselves so the output goes through
// the same capture mechanism as the built-in diff (important for cargo test).
let old_filename = old_path.file_name().unwrap();
let new_filename = new_path.file_name().unwrap();
// Split tool string to support arguments (e.g., "delta --side-by-side")
let mut parts = tool.split_whitespace();
let cmd = match parts.next() {
Some(cmd) => cmd,
None => return false,
};
let mut command = Command::new(cmd);
command.args(parts);
command.current_dir(dir.path());
command.arg(old_filename);
command.arg(new_filename);
match command.output() {
Ok(output) => {
// Print captured output through normal channels so it gets captured
// by cargo test when appropriate (just like the built-in diff)
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
// Non-zero exit is normal for diff tools when files differ
true
}
Err(err) => {
eprintln!("warning: failed to invoke diff tool `{tool}`: {err}");
false
}
}
// Temp dir is cleaned up when `dir` goes out of scope
}
#[test]
fn test_invisible() {
assert_eq!(
render_invisible("\r\n\x1b\r\x07\x08\x7f\n", true),
"␍␊\r\n␛␍\r␇␈␡␊\n"
);
}
+547
View File
@@ -0,0 +1,547 @@
use pest::Parser;
use pest_derive::Parser;
use std::borrow::Cow;
use std::fmt;
use crate::content::Content;
#[derive(Debug)]
pub struct SelectorParseError(Box<pest::error::Error<Rule>>);
impl SelectorParseError {
/// Return the column of where the error occurred.
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)]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub struct ContentPath<'a>(&'a [PathItem]);
impl fmt::Display for ContentPath<'_> {
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, "<content>")?;
}
}
PathItem::Field(name) => write!(f, "{name}")?,
PathItem::Index(idx, _) => write!(f, "{idx}")?,
}
}
Ok(())
}
}
/// Replaces a value with another one.
///
/// Represents a redaction.
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub enum Redaction {
/// Static redaction with new content.
Static(Content),
/// Redaction with new content.
Dynamic(Box<dyn Fn(Content, ContentPath<'_>) -> 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<u8>);
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]"
/// }));
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn dynamic_redaction<I, F>(func: F) -> Redaction
where
I: Into<Content>,
F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static,
{
Redaction::Dynamic(Box::new(move |c, p| func(c, p).into()))
}
/// Creates a dynamic redaction that sorts the value at the selector.
///
/// This is useful to force something like a set or map to be ordered to make
/// it deterministic. This is necessary as insta's serialization support is
/// based on [`serde`] which does not have native set support. As a result vectors
/// (which need to retain order) and sets (which should be given a stable order)
/// look the same.
///
/// ```rust
/// # use insta::{Settings, sorted_redaction};
/// # let mut settings = Settings::new();
/// settings.add_redaction(".flags", sorted_redaction());
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn sorted_redaction() -> Redaction {
fn sort(mut value: Content, _path: ContentPath) -> Content {
match value.resolve_inner_mut() {
Content::Seq(ref mut val) => {
val.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
Content::Map(ref mut val) => {
val.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
Content::Struct(_, ref mut fields)
| Content::StructVariant(_, _, _, ref mut fields) => {
fields.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
_ => {}
}
value
}
dynamic_redaction(sort)
}
/// Creates a redaction that rounds floating point numbers to a given
/// number of decimal places.
///
/// ```rust
/// # use insta::{Settings, rounded_redaction};
/// # let mut settings = Settings::new();
/// settings.add_redaction(".sum", rounded_redaction(2));
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn rounded_redaction(decimals: usize) -> Redaction {
dynamic_redaction(move |value: Content, _path: ContentPath| -> Content {
let f = match value.resolve_inner() {
Content::F32(f) => *f as f64,
Content::F64(f) => *f,
_ => return value,
};
let x = 10f64.powf(decimals as f64);
Content::F64((f * x).round() / x)
})
}
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<u64> {
match *self {
PathItem::Content(ref content) => content.as_u64(),
PathItem::Field(_) => None,
PathItem::Index(idx, _) => Some(idx),
}
}
fn range_check(&self, start: Option<i64>, end: Option<i64>) -> 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, Eq)]
pub enum Segment<'a> {
DeepWildcard,
Wildcard,
Key(Cow<'a, str>),
Index(u64),
Range(Option<i64>, Option<i64>),
}
#[derive(Debug, Clone)]
pub struct Selector<'a> {
selectors: Vec<Vec<Segment<'a>>>,
}
impl<'a> Selector<'a> {
pub fn parse(selector: &'a str) -> Result<Selector<'a>, SelectorParseError> {
let pair = SelectParser::parse(Rule::selectors, selector)
.map_err(Box::new)
.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(Box::new(
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<Content>,
redaction: &Redaction,
path: &mut Vec<PathItem>,
) -> Vec<Content> {
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<PathItem>,
) -> 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<PathItem>,
) -> 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::Field("$key"));
let new_key = self.redact_impl(key.clone(), redaction, path);
path.pop();
path.push(PathItem::Content(key));
let new_value = self.redact_impl(value, redaction, path);
path.pop();
(new_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() {
use similar_asserts::assert_eq;
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);
}
+978
View File
@@ -0,0 +1,978 @@
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::str;
use std::sync::{Arc, Mutex};
use std::{borrow::Cow, env};
use crate::settings::Settings;
use crate::snapshot::{
MetaData, PendingInlineSnapshot, Snapshot, SnapshotContents, SnapshotKind, TextSnapshotContents,
};
use crate::utils::{path_to_storage, style};
use crate::{env::get_tool_config, output::SnapshotPrinter};
use crate::{
env::{
memoize_snapshot_file, pending_snapshot_path, snapshot_update_behavior, OutputBehavior,
SnapshotUpdateBehavior, ToolConfig,
},
snapshot::TextSnapshotKind,
};
use once_cell::sync::Lazy;
static TEST_NAME_COUNTERS: Lazy<Mutex<BTreeMap<String, usize>>> =
Lazy::new(|| Mutex::new(BTreeMap::new()));
static TEST_NAME_CLASH_DETECTION: Lazy<Mutex<BTreeMap<String, bool>>> =
Lazy::new(|| Mutex::new(BTreeMap::new()));
static INLINE_DUPLICATES: Lazy<Mutex<BTreeSet<String>>> = Lazy::new(|| Mutex::new(BTreeSet::new()));
thread_local! {
static RECORDED_DUPLICATES: RefCell<Vec<BTreeMap<String, Snapshot>>> = RefCell::default()
}
// Writes to stderr and also to a warnings file (if INSTA_WARNINGS_FILE is set).
// The warnings file allows cargo-insta to display warnings after tests complete,
// since test runners like nextest suppress output from passing tests by default.
#[macro_export]
macro_rules! elog {
($($arg:tt)*) => {{
use std::io::Write as _;
let msg = format!($($arg)*);
let _ = writeln!(std::io::stderr(), "{}", msg);
$crate::env::memoize_warning(&msg);
}};
}
#[cfg(feature = "glob")]
macro_rules! print_or_panic {
($fail_fast:expr, $($tokens:tt)*) => {{
if (!$fail_fast) {
eprintln!($($tokens)*);
eprintln!();
} else {
panic!($($tokens)*);
}
}}
}
/// 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;
pub struct InlineValue<'a>(pub &'a str);
/// The name of a snapshot, from which the path is derived.
type SnapshotName<'a> = Option<Cow<'a, str>>;
pub struct BinarySnapshotValue<'a> {
pub name_and_extension: &'a str,
pub content: Vec<u8>,
}
pub enum SnapshotValue<'a> {
/// A text snapshot that gets stored along with the metadata in the same file.
FileText {
name: SnapshotName<'a>,
/// The new generated value to compare against any previously approved content.
content: &'a str,
},
/// An inline snapshot.
InlineText {
/// The reference content from the macro invocation that will be compared against.
reference_content: &'a str,
/// The new generated value to compare against any previously approved content.
content: &'a str,
},
/// A binary snapshot that gets stored as a separate file next to the metadata file.
Binary {
name: SnapshotName<'a>,
/// The new generated value to compare against any previously approved content.
content: Vec<u8>,
/// The extension of the separate file.
extension: &'a str,
},
}
impl<'a> From<(AutoName, &'a str)> for SnapshotValue<'a> {
fn from((_, content): (AutoName, &'a str)) -> Self {
SnapshotValue::FileText {
name: None,
content,
}
}
}
impl<'a> From<(Option<String>, &'a str)> for SnapshotValue<'a> {
fn from((name, content): (Option<String>, &'a str)) -> Self {
SnapshotValue::FileText {
name: name.map(Cow::Owned),
content,
}
}
}
impl<'a> From<(String, &'a str)> for SnapshotValue<'a> {
fn from((name, content): (String, &'a str)) -> Self {
SnapshotValue::FileText {
name: Some(Cow::Owned(name)),
content,
}
}
}
impl<'a> From<(Option<&'a str>, &'a str)> for SnapshotValue<'a> {
fn from((name, content): (Option<&'a str>, &'a str)) -> Self {
SnapshotValue::FileText {
name: name.map(Cow::Borrowed),
content,
}
}
}
impl<'a> From<(&'a str, &'a str)> for SnapshotValue<'a> {
fn from((name, content): (&'a str, &'a str)) -> Self {
SnapshotValue::FileText {
name: Some(Cow::Borrowed(name)),
content,
}
}
}
impl<'a> From<(InlineValue<'a>, &'a str)> for SnapshotValue<'a> {
fn from((InlineValue(reference_content), content): (InlineValue<'a>, &'a str)) -> Self {
SnapshotValue::InlineText {
reference_content,
content,
}
}
}
impl<'a> From<BinarySnapshotValue<'a>> for SnapshotValue<'a> {
fn from(
BinarySnapshotValue {
name_and_extension,
content,
}: BinarySnapshotValue<'a>,
) -> Self {
let (name, extension) = name_and_extension.split_once('.').unwrap_or_else(|| {
panic!("\"{name_and_extension}\" does not match the format \"name.extension\"",)
});
let name = if name.is_empty() {
None
} else {
Some(Cow::Borrowed(name))
};
SnapshotValue::Binary {
name,
extension,
content,
}
}
}
fn is_doctest(function_name: &str) -> bool {
function_name.starts_with("rust_out::main::_doctest")
}
fn detect_snapshot_name(function_name: &str, module_path: &str) -> Result<String, &'static str> {
// clean test name first
let name = function_name.rsplit("::").next().unwrap();
let (name, test_prefixed) = if let Some(stripped) = name.strip_prefix("test_") {
(stripped, true)
} else {
(name, false)
};
// 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);
// because fn foo and fn test_foo end up with the same snapshot name we
// make sure we detect this here and raise an error.
let mut name_clash_detection = TEST_NAME_CLASH_DETECTION
.lock()
.unwrap_or_else(|x| x.into_inner());
match name_clash_detection.get(&key) {
None => {
name_clash_detection.insert(key.clone(), test_prefixed);
}
Some(&was_test_prefixed) => {
if was_test_prefixed != test_prefixed {
panic!(
"Insta snapshot name clash detected between '{name}' \
and 'test_{name}' in '{module_path}'. Rename one function."
);
}
}
}
// The rest of the code just deals with duplicates, which we in some
// cases do not want to guard against.
if allow_duplicates() {
return Ok(name.to_string());
}
// 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)
}
/// 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)
})
}
fn get_snapshot_filename(
module_path: &str,
assertion_file: &str,
snapshot_name: &str,
cargo_workspace: &Path,
is_doctest: bool,
) -> PathBuf {
let root = Path::new(cargo_workspace);
let base = Path::new(assertion_file);
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() {
if is_doctest {
write!(
&mut f,
"doctest_{}__",
base.file_name()
.unwrap()
.to_string_lossy()
.replace('.', "_")
)
.unwrap();
} else {
write!(&mut f, "{}__", module_path.replace("::", "__")).unwrap();
}
}
write!(
&mut f,
"{}.snap",
snapshot_name.replace(&['/', '\\'][..], "__")
)
.unwrap();
f
})
})
}
/// The context around a snapshot, such as the reference value, location, etc.
/// (but not including the generated value). Responsible for saving the
/// snapshot.
#[derive(Debug)]
struct SnapshotAssertionContext<'a> {
tool_config: Arc<ToolConfig>,
workspace: &'a Path,
module_path: &'a str,
snapshot_name: Option<Cow<'a, str>>,
snapshot_file: Option<PathBuf>,
duplication_key: Option<String>,
old_snapshot: Option<Snapshot>,
pending_snapshots_path: Option<PathBuf>,
assertion_file: &'a str,
assertion_line: u32,
is_doctest: bool,
snapshot_kind: SnapshotKind,
}
impl<'a> SnapshotAssertionContext<'a> {
fn prepare(
new_snapshot_value: &SnapshotValue<'a>,
workspace: &'a Path,
function_name: &'a str,
module_path: &'a str,
assertion_file: &'a str,
assertion_line: u32,
) -> Result<SnapshotAssertionContext<'a>, Box<dyn Error>> {
let tool_config = get_tool_config(workspace);
let snapshot_name;
let mut duplication_key = None;
let mut snapshot_file = None;
let mut old_snapshot = None;
let mut pending_snapshots_path = None;
let is_doctest = is_doctest(function_name);
match new_snapshot_value {
SnapshotValue::FileText { name, .. } | SnapshotValue::Binary { name, .. } => {
let name = match &name {
Some(name) => add_suffix_to_snapshot_name(name.clone()),
None => {
if is_doctest {
panic!("Cannot determine reliable names for snapshot in doctests. Please use explicit names instead.");
}
detect_snapshot_name(function_name, module_path)
.unwrap()
.into()
}
};
if allow_duplicates() {
duplication_key = Some(format!("named:{module_path}|{name}"));
}
let file = get_snapshot_filename(
module_path,
assertion_file,
&name,
workspace,
is_doctest,
);
if fs::metadata(&file).is_ok() {
match Snapshot::from_file(&file) {
Ok(snapshot) => {
old_snapshot = Some(snapshot);
}
Err(err) => {
// If we can't parse the snapshot (e.g., invalid YAML,
// merge conflicts, truncated file), log a warning and
// proceed. The test will generate a new pending snapshot.
elog!(
"{}: Failed to parse snapshot file; \
a new snapshot will be generated: {}\n Error: {}",
style("warning").yellow().bold(),
file.display(),
err
);
}
}
}
snapshot_name = Some(name);
snapshot_file = Some(file);
}
SnapshotValue::InlineText {
reference_content: contents,
..
} => {
if allow_duplicates() {
duplication_key = Some(format!(
"inline:{function_name}|{assertion_file}|{assertion_line}"
));
} else {
prevent_inline_duplicate(function_name, assertion_file, assertion_line);
}
snapshot_name = detect_snapshot_name(function_name, module_path)
.ok()
.map(Cow::Owned);
let mut pending_file = workspace.join(assertion_file);
pending_file.set_file_name(format!(
".{}.pending-snap",
pending_file
.file_name()
.expect("no filename")
.to_str()
.expect("non unicode filename")
));
pending_snapshots_path = Some(pending_file);
old_snapshot = Some(Snapshot::from_components(
module_path.replace("::", "__"),
None,
MetaData::default(),
SnapshotContents::Text(TextSnapshotContents::from_inline_literal(contents)),
));
}
};
let snapshot_type = match new_snapshot_value {
SnapshotValue::FileText { .. } | SnapshotValue::InlineText { .. } => SnapshotKind::Text,
&SnapshotValue::Binary { extension, .. } => SnapshotKind::Binary {
extension: extension.to_string(),
},
};
Ok(SnapshotAssertionContext {
tool_config,
workspace,
module_path,
snapshot_name,
snapshot_file,
old_snapshot,
pending_snapshots_path,
assertion_file,
assertion_line,
duplication_key,
is_doctest,
snapshot_kind: snapshot_type,
})
}
/// Given a path returns the local path within the workspace.
pub fn localize_path(&self, p: &Path) -> Option<PathBuf> {
let workspace = self.workspace.canonicalize().ok()?;
let p = self.workspace.join(p).canonicalize().ok()?;
p.strip_prefix(&workspace).ok().map(|x| x.to_path_buf())
}
/// Creates the new snapshot from input values.
pub fn new_snapshot(&self, contents: SnapshotContents, expr: &str) -> Snapshot {
assert_eq!(
contents.is_binary(),
matches!(self.snapshot_kind, SnapshotKind::Binary { .. })
);
Snapshot::from_components(
self.module_path.replace("::", "__"),
self.snapshot_name.as_ref().map(|x| x.to_string()),
Settings::with(|settings| MetaData {
source: {
let source_path = Path::new(self.assertion_file);
// We need to compute a relative path from the workspace to the source file.
// This is necessary for workspace setups where the project is not a direct
// child of the workspace root (e.g., when workspace and project are siblings).
// We canonicalize paths first to properly handle symlinks.
let canonicalized_base = self.workspace.canonicalize().ok();
let canonicalized_path = source_path.canonicalize().ok();
let relative = if let (Some(base), Some(path)) =
(canonicalized_base, canonicalized_path)
{
path_relative_from(&path, &base)
.unwrap_or_else(|| source_path.to_path_buf())
} else {
// If canonicalization fails, try with original paths
path_relative_from(source_path, self.workspace)
.unwrap_or_else(|| source_path.to_path_buf())
};
Some(path_to_storage(&relative))
},
assertion_line: Some(self.assertion_line),
description: settings.description().map(Into::into),
expression: if settings.omit_expression() {
None
} else {
Some(expr.to_string())
},
info: settings.info().map(ToOwned::to_owned),
input_file: settings
.input_file()
.and_then(|x| self.localize_path(x))
.map(|x| path_to_storage(&x)),
snapshot_kind: self.snapshot_kind.clone(),
}),
contents,
)
}
/// Cleanup logic for passing snapshots.
pub fn cleanup_passing(&self) -> Result<(), Box<dyn Error>> {
// let's just make sure there are no more pending files lingering
// around.
if let Some(ref snapshot_file) = self.snapshot_file {
let target_path = pending_snapshot_path(self.workspace, snapshot_file);
let new_file = target_path.with_extension("snap.new");
fs::remove_file(new_file).ok();
}
// and add a null pending snapshot to a pending snapshot file if needed
if let Some(ref pending_snapshots) = self.pending_snapshots_path {
let target_path = pending_snapshot_path(self.workspace, pending_snapshots);
if fs::metadata(&target_path).is_ok() {
PendingInlineSnapshot::new(None, None, self.assertion_line).save(&target_path)?;
}
}
Ok(())
}
/// Removes any old .snap.new.* files that belonged to previous pending snapshots. This should
/// only ever remove maximum one file because we do this every time before we create a new
/// pending snapshot.
pub fn cleanup_previous_pending_binary_snapshots(&self) -> Result<(), Box<dyn Error>> {
if let Some(ref path) = self.snapshot_file {
// Use pending directory if set
let target_path = pending_snapshot_path(self.workspace, path);
// The file name to compare against has to be valid utf-8 as it is generated by this crate
// out of utf-8 strings.
let file_name_prefix = format!(
"{}.new.",
target_path.file_name().unwrap().to_str().unwrap()
);
let read_dir = target_path.parent().unwrap().read_dir();
match read_dir {
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
_ => (),
}
// We have to loop over where whole directory here because there is no filesystem API
// for getting files by prefix.
for entry in read_dir? {
let entry = entry?;
let entry_file_name = entry.file_name();
// We'll just skip over files with non-utf-8 names. The assumption being that those
// would not have been generated by this crate.
if entry_file_name
.to_str()
.map(|f| f.starts_with(&file_name_prefix))
.unwrap_or(false)
{
std::fs::remove_file(entry.path())?;
}
}
}
Ok(())
}
/// Writes the changes of the snapshot back.
pub fn update_snapshot(
&self,
new_snapshot: Snapshot,
) -> Result<SnapshotUpdateBehavior, Box<dyn Error>> {
// TODO: this seems to be making `unseen` be true when there is an
// existing snapshot file; which seems wrong??
let unseen = self
.snapshot_file
.as_ref()
.map_or(false, |x| fs::metadata(x).is_ok());
let should_print = self.tool_config.output_behavior() != OutputBehavior::Nothing;
let snapshot_update = snapshot_update_behavior(&self.tool_config, unseen);
// If snapshot_update is `InPlace` and we have an inline snapshot, then
// use `NewFile`, since we can't use `InPlace` for inline. `cargo-insta`
// then accepts all snapshots at the end of the test.
let snapshot_update =
// TODO: could match on the snapshot kind instead of whether snapshot_file is None
if snapshot_update == SnapshotUpdateBehavior::InPlace && self.snapshot_file.is_none() {
SnapshotUpdateBehavior::NewFile
} else {
snapshot_update
};
match snapshot_update {
SnapshotUpdateBehavior::InPlace => {
if let Some(ref snapshot_file) = self.snapshot_file {
new_snapshot.save(snapshot_file)?;
if should_print {
elog!(
"{} {}",
style("updated snapshot").green(),
style(snapshot_file.display()).cyan().underlined(),
);
}
} else {
// Checked self.snapshot_file.is_none() above
unreachable!()
}
}
SnapshotUpdateBehavior::NewFile => {
if let Some(ref snapshot_file) = self.snapshot_file {
// File snapshot - use pending directory if set
let target_path = pending_snapshot_path(self.workspace, snapshot_file);
let new_path = new_snapshot.save_new(&target_path)?;
if should_print {
elog!(
"{} {}",
style("stored new snapshot").green(),
style(new_path.display()).cyan().underlined(),
);
}
} else if self.is_doctest {
if should_print {
elog!(
"{}",
style("warning: cannot update inline snapshots in doctests")
.red()
.bold(),
);
}
} else {
// Inline snapshot - use pending directory if set
let pending_path = self.pending_snapshots_path.as_ref().unwrap();
let target_path = pending_snapshot_path(self.workspace, pending_path);
PendingInlineSnapshot::new(
Some(new_snapshot),
self.old_snapshot.clone(),
self.assertion_line,
)
.save(&target_path)?;
}
}
SnapshotUpdateBehavior::NoUpdate => {}
}
Ok(snapshot_update)
}
/// This prints the information about the snapshot
fn print_snapshot_info(&self, new_snapshot: &Snapshot) {
let mut printer =
SnapshotPrinter::new(self.workspace, self.old_snapshot.as_ref(), new_snapshot);
printer.set_line(Some(self.assertion_line));
printer.set_snapshot_file(self.snapshot_file.as_deref());
printer.set_title(Some("Snapshot Summary"));
printer.set_show_info(true);
match self.tool_config.output_behavior() {
OutputBehavior::Summary => {
printer.print();
}
OutputBehavior::Diff => {
printer.set_show_diff(true);
printer.print();
}
_ => {}
}
}
/// Finalizes the assertion when the snapshot comparison fails, potentially
/// panicking to fail the test
fn finalize(&self, update_result: SnapshotUpdateBehavior) {
// if we are in glob mode, we want to adjust the finalization
// so that we do not show the hints immediately.
let fail_fast = {
#[cfg(feature = "glob")]
{
if let Some(top) = crate::glob::GLOB_STACK.lock().unwrap().last() {
top.fail_fast
} else {
true
}
}
#[cfg(not(feature = "glob"))]
{
true
}
};
if fail_fast
&& update_result == SnapshotUpdateBehavior::NewFile
&& self.tool_config.output_behavior() != OutputBehavior::Nothing
&& !self.is_doctest
{
println!(
"{hint}",
hint = style("To update snapshots run `cargo insta review`").dim(),
);
}
if update_result != SnapshotUpdateBehavior::InPlace && !self.tool_config.force_pass() {
if fail_fast && self.tool_config.output_behavior() != OutputBehavior::Nothing {
let msg = if env::var("INSTA_CARGO_INSTA") == Ok("1".to_string()) {
"Stopped on the first failure."
} else {
"Stopped on the first failure. Run `cargo insta test` to run all snapshots."
};
println!("{hint}", hint = style(msg).dim(),);
}
// if we are in glob mode, count the failures and print the
// errors instead of panicking. The glob will then panic at
// the end.
#[cfg(feature = "glob")]
{
let mut stack = crate::glob::GLOB_STACK.lock().unwrap();
if let Some(glob_collector) = stack.last_mut() {
glob_collector.failed += 1;
if update_result == SnapshotUpdateBehavior::NewFile
&& self.tool_config.output_behavior() != OutputBehavior::Nothing
{
glob_collector.show_insta_hint = true;
}
print_or_panic!(
fail_fast,
"snapshot assertion from glob for '{}' failed in line {}",
self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
self.assertion_line
);
return;
}
}
panic!(
"snapshot assertion for '{}' failed in line {}",
self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
self.assertion_line
);
}
}
}
/// Computes a relative path from `base` to `path`, returning a path with `../` components
/// if necessary.
///
/// This function is vendored from the old Rust standard library implementation
/// (pre-1.0, removed in RFC 474) and is distributed under the same terms as the
/// Rust project (MIT/Apache-2.0 dual license).
///
/// Unlike `Path::strip_prefix`, this function can handle cases where `path` is not
/// a descendant of `base`, making it suitable for finding relative paths between
/// arbitrary directories (e.g., between sibling directories in a workspace).
fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
use std::path::Component;
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Some(PathBuf::from(path))
} else {
None
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => {}
(Some(a), Some(_b)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Some(comps.iter().map(|c| c.as_os_str()).collect())
}
}
fn prevent_inline_duplicate(function_name: &str, assertion_file: &str, assertion_line: u32) {
let key = format!("{function_name}|{assertion_file}|{assertion_line}");
let mut set = INLINE_DUPLICATES.lock().unwrap();
if set.contains(&key) {
// drop the lock so we don't poison it
drop(set);
panic!(
"Insta does not allow inline snapshot assertions in loops. \
Wrap your assertions in allow_duplicates! to change this."
);
}
set.insert(key);
}
fn record_snapshot_duplicate(
results: &mut BTreeMap<String, Snapshot>,
snapshot: &Snapshot,
ctx: &SnapshotAssertionContext,
) {
let key = ctx.duplication_key.as_deref().unwrap();
if let Some(prev_snapshot) = results.get(key) {
if prev_snapshot.contents() != snapshot.contents() {
println!("Snapshots in allow-duplicates block do not match.");
let mut printer = SnapshotPrinter::new(ctx.workspace, Some(prev_snapshot), snapshot);
printer.set_line(Some(ctx.assertion_line));
printer.set_snapshot_file(ctx.snapshot_file.as_deref());
printer.set_title(Some("Differences in Block"));
printer.set_snapshot_hints("previous assertion", "current assertion");
if ctx.tool_config.output_behavior() == OutputBehavior::Diff {
printer.set_show_diff(true);
}
printer.print();
panic!(
"snapshot assertion for '{}' failed in line {}. Result \
does not match previous snapshot in allow-duplicates block.",
ctx.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
ctx.assertion_line
);
}
} else {
results.insert(key.to_string(), snapshot.clone());
}
}
/// Do we allow recording of duplicates?
fn allow_duplicates() -> bool {
RECORDED_DUPLICATES.with(|x| !x.borrow().is_empty())
}
/// Helper function to support perfect duplicate detection.
pub fn with_allow_duplicates<R, F>(f: F) -> R
where
F: FnOnce() -> R,
{
RECORDED_DUPLICATES.with(|x| x.borrow_mut().push(BTreeMap::new()));
let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
RECORDED_DUPLICATES.with(|x| x.borrow_mut().pop().unwrap());
match rv {
Ok(rv) => rv,
Err(payload) => std::panic::resume_unwind(payload),
}
}
/// This function is invoked from the macros to run the main assertion logic.
///
/// This will create the assertion context, run the main logic to assert
/// on snapshots and write changes to the pending snapshot files. It will
/// also print the necessary bits of information to the output and fail the
/// assertion with a panic if needed.
#[allow(clippy::too_many_arguments)]
pub fn assert_snapshot(
snapshot_value: SnapshotValue<'_>,
workspace: &Path,
function_name: &str,
module_path: &str,
assertion_file: &str,
assertion_line: u32,
expr: &str,
) -> Result<(), Box<dyn Error>> {
let ctx = SnapshotAssertionContext::prepare(
&snapshot_value,
workspace,
function_name,
module_path,
assertion_file,
assertion_line,
)?;
ctx.cleanup_previous_pending_binary_snapshots()?;
let content = match snapshot_value {
SnapshotValue::FileText { content, .. } | SnapshotValue::InlineText { content, .. } => {
// strip ANSI escape codes if enabled
#[cfg(feature = "filters")]
let content = Settings::with(|settings| {
if settings.strip_ansi_escape_codes() {
crate::filters::strip_ansi_escape_codes(content)
} else {
std::borrow::Cow::Borrowed(content)
}
});
// apply filters if they are available
#[cfg(feature = "filters")]
let content = Settings::with(|settings| settings.filters().apply_to(&content));
let kind = match ctx.snapshot_file {
Some(_) => TextSnapshotKind::File,
None => TextSnapshotKind::Inline,
};
TextSnapshotContents::new(content.into(), kind).into()
}
SnapshotValue::Binary {
content, extension, ..
} => {
assert!(
extension != "new",
"'.new' is not allowed as a file extension"
);
assert!(
!extension.starts_with("new."),
"file extensions starting with 'new.' are not allowed",
);
SnapshotContents::Binary(Some(Rc::new(content)))
}
};
let new_snapshot = ctx.new_snapshot(content, expr);
// memoize the snapshot file if requested, as part of potentially removing unreferenced snapshots
if let Some(ref snapshot_file) = ctx.snapshot_file {
memoize_snapshot_file(snapshot_file);
}
// If we allow assertion with duplicates, we record the duplicate now. This will
// in itself fail the assertion if the previous visit of the same assertion macro
// did not yield the same result.
RECORDED_DUPLICATES.with(|x| {
if let Some(results) = x.borrow_mut().last_mut() {
record_snapshot_duplicate(results, &new_snapshot, &ctx);
}
});
let pass = Settings::with(|settings| {
ctx.old_snapshot
.as_ref()
.map(|x| {
if ctx.tool_config.require_full_match() {
settings.comparator().matches_fully(x, &new_snapshot)
} else {
settings.comparator().matches(x, &new_snapshot)
}
})
.unwrap_or(false)
});
if pass {
ctx.cleanup_passing()?;
if matches!(
ctx.tool_config.snapshot_update(),
crate::env::SnapshotUpdate::Force
) {
ctx.update_snapshot(new_snapshot)?;
}
// otherwise print information and update snapshots.
} else {
ctx.print_snapshot_info(&new_snapshot);
let update_result = ctx.update_snapshot(new_snapshot)?;
ctx.finalize(update_result);
}
Ok(())
}
#[allow(rustdoc::private_doc_tests)]
/// Test snapshots in doctests.
///
/// ```
/// // this is only working on newer rust versions
/// extern crate rustc_version;
/// use rustc_version::{Version, version};
/// if version().unwrap() > Version::parse("1.72.0").unwrap() {
/// insta::assert_debug_snapshot!("named", vec![1, 2, 3, 4, 5]);
/// }
/// ```
///
/// ```should_panic
/// insta::assert_debug_snapshot!(vec![1, 2, 3, 4, 5]);
/// ```
///
/// ```
/// let some_string = "Coucou je suis un joli bug";
/// insta::assert_snapshot!(some_string, @"Coucou je suis un joli bug");
/// ```
///
/// ```
/// let some_string = "Coucou je suis un joli bug";
/// insta::assert_snapshot!(some_string, @"Coucou je suis un joli bug");
/// ```
const _DOCTEST1: bool = false;
+20
View File
@@ -0,0 +1,20 @@
WHITESPACE = _{ 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 }
+264
View File
@@ -0,0 +1,264 @@
use serde::{de::value::Error as ValueError, Serialize};
#[cfg(feature = "ron")]
use std::borrow::Cow;
#[cfg(feature = "toml")]
use {
core::str::FromStr,
toml_edit::{visit_mut::*, Array, Item, Table, Value},
toml_writer::ToTomlValue,
};
use crate::{
content::{json, yaml, Content, ContentSerializer},
settings::Settings,
};
pub enum SerializationFormat {
#[cfg(feature = "csv")]
Csv,
#[cfg(feature = "ron")]
Ron,
#[cfg(feature = "toml")]
Toml,
Yaml,
Json,
JsonCompact,
}
#[derive(Debug)]
pub enum SnapshotLocation {
Inline,
File,
}
pub fn serialize_content(mut content: Content, format: SerializationFormat) -> String {
content = Settings::with(|settings| {
if settings.sort_maps() {
content.sort_maps();
}
#[cfg(feature = "redactions")]
{
content = settings.apply_redactions(content);
}
content
});
match format {
SerializationFormat::Yaml => yaml::to_string(&content)[4..].to_string(),
SerializationFormat::Json => json::to_string_pretty(&content),
SerializationFormat::JsonCompact => json::to_string_compact(&content),
#[cfg(feature = "csv")]
SerializationFormat::Csv => {
let mut buf = Vec::with_capacity(128);
{
let mut writer = csv::Writer::from_writer(&mut buf);
// if the top-level content we're serializing is a vector we
// want to serialize it multiple times once for each item.
if let Some(content_slice) = content.as_slice() {
for content in content_slice {
writer.serialize(content).unwrap();
}
} else {
writer.serialize(&content).unwrap();
}
writer.flush().unwrap();
}
if buf.ends_with(b"\n") {
buf.truncate(buf.len() - 1);
}
String::from_utf8(buf).unwrap()
}
#[cfg(feature = "ron")]
SerializationFormat::Ron => {
let mut buf = String::new();
let mut config = ron::ser::PrettyConfig::new();
config.new_line = Cow::Borrowed("\n");
config.indentor = Cow::Borrowed(" ");
config.struct_names = true;
let mut serializer = ron::ser::Serializer::with_options(
&mut buf,
Some(config),
&ron::options::Options::default(),
)
.unwrap();
content.serialize(&mut serializer).unwrap();
buf
}
#[cfg(feature = "toml")]
SerializationFormat::Toml => {
struct Pretty {
in_value: bool,
}
impl VisitMut for Pretty {
fn visit_item_mut(&mut self, node: &mut Item) {
let decor = if let Item::Value(Value::InlineTable(t)) = node {
Some(t.decor().clone())
} else {
None
};
if !self.in_value {
let other = std::mem::take(node);
let other = match other.into_table().map(Item::Table) {
Ok(i) => i,
Err(i) => i,
};
let other = match other.into_array_of_tables().map(Item::ArrayOfTables) {
Ok(i) => i,
Err(i) => i,
};
*node = other;
}
if let Item::Table(table) = node {
if let Some(decor) = decor {
*table.decor_mut() = decor;
}
for (_key, value) in table.iter_mut() {
if let Item::Value(Value::InlineTable(inner)) = value {
inner.decor_mut().set_prefix("");
}
}
}
visit_item_mut(self, node);
}
fn visit_table_mut(&mut self, node: &mut Table) {
if !node.is_empty() {
node.set_implicit(true);
}
visit_table_mut(self, node);
}
fn visit_value_mut(&mut self, node: &mut Value) {
if let Value::String(f) = node {
let builder = toml_writer::TomlStringBuilder::new(f.value().as_str());
let formatted = builder
.as_literal()
.or_else(|| builder.as_ml_literal())
.unwrap_or_else(|| builder.as_default())
.to_toml_value();
if let Ok(value) = Value::from_str(&formatted) {
*node = value;
}
}
node.decor_mut().clear();
let old_in_value = self.in_value;
self.in_value = true;
visit_value_mut(self, node);
self.in_value = old_in_value;
}
fn visit_array_mut(&mut self, node: &mut Array) {
visit_array_mut(self, node);
if (0..=1).contains(&node.len()) {
node.set_trailing("");
node.set_trailing_comma(false);
} else {
for item in node.iter_mut() {
item.decor_mut().set_prefix("\n ");
}
node.set_trailing("\n");
node.set_trailing_comma(true);
}
}
}
let mut dm = toml_edit::ser::to_document(&content).unwrap_or_else(|e| {
panic!(
"TOML serialization failed: {e}. \
Note: TOML requires the top-level value to be a struct or map. \
Use assert_json_snapshot! or assert_yaml_snapshot! for other types.",
)
});
let mut visitor = Pretty { in_value: false };
visitor.visit_document_mut(&mut dm);
let mut rv = dm.to_string();
if rv.ends_with('\n') {
rv.truncate(rv.len() - 1);
}
rv
}
}
}
pub fn serialize_value<S: Serialize>(s: &S, format: SerializationFormat) -> String {
let serializer = ContentSerializer::<ValueError>::new();
let content = Serialize::serialize(s, serializer).unwrap();
serialize_content(content, format)
}
#[cfg(feature = "redactions")]
pub fn serialize_value_redacted<S: Serialize>(
s: &S,
redactions: &[(crate::redaction::Selector, crate::redaction::Redaction)],
format: SerializationFormat,
) -> String {
let serializer = ContentSerializer::<ValueError>::new();
let mut content = Serialize::serialize(s, serializer).unwrap();
for (selector, redaction) in redactions {
content = selector.redact(content, redaction);
}
serialize_content(content, format)
}
#[test]
fn test_yaml_serialization() {
let yaml = serialize_content(
Content::Map(vec![
(
Content::from("env"),
Content::Seq(vec![
Content::from("ENVIRONMENT"),
Content::from("production"),
]),
),
(
Content::from("cmdline"),
Content::Seq(vec![Content::from("my-tool"), Content::from("run")]),
),
]),
SerializationFormat::Yaml,
);
crate::assert_snapshot!(&yaml, @"
env:
- ENVIRONMENT
- production
cmdline:
- my-tool
- run
");
let inline_yaml = serialize_content(
Content::Map(vec![
(
Content::from("env"),
Content::Seq(vec![
Content::from("ENVIRONMENT"),
Content::from("production"),
]),
),
(
Content::from("cmdline"),
Content::Seq(vec![Content::from("my-tool"), Content::from("run")]),
),
]),
SerializationFormat::Yaml,
);
crate::assert_snapshot!(&inline_yaml, @"
env:
- ENVIRONMENT
- production
cmdline:
- my-tool
- run
");
}
+737
View File
@@ -0,0 +1,737 @@
#[cfg(feature = "serde")]
use serde::{de::value::Error as ValueError, Serialize};
use std::cell::RefCell;
use std::future::Future;
use std::mem;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use crate::comparator::Comparator;
use crate::content::Content;
#[cfg(feature = "serde")]
use crate::content::ContentSerializer;
#[cfg(feature = "filters")]
use crate::filters::Filters;
#[cfg(feature = "redactions")]
use crate::redaction::{dynamic_redaction, sorted_redaction, ContentPath, Redaction, Selector};
thread_local!(static CURRENT_SETTINGS: RefCell<Settings> = RefCell::new(Settings::new()));
/// Represents stored redactions.
#[cfg(feature = "redactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
#[derive(Clone, Default)]
pub struct Redactions(Vec<(Selector<'static>, Arc<Redaction>)>);
#[cfg(feature = "redactions")]
impl<'a> From<Vec<(&'a str, Redaction)>> 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(),
)
}
}
#[cfg(feature = "redactions")]
impl Redactions {
/// Applies all redactions to the given content.
pub(crate) fn apply_to_content(&self, mut content: Content) -> Content {
for (selector, redaction) in self.0.iter() {
content = selector.redact(content, redaction);
}
content
}
}
#[doc(hidden)]
pub struct ActualSettings {
pub sort_maps: bool,
pub snapshot_path: PathBuf,
pub snapshot_suffix: String,
pub input_file: Option<PathBuf>,
pub description: Option<String>,
pub info: Option<Content>,
pub omit_expression: bool,
pub prepend_module_to_snapshot: bool,
pub comparator: Box<dyn Comparator>,
#[cfg(feature = "redactions")]
pub redactions: Redactions,
#[cfg(feature = "filters")]
pub filters: Filters,
#[cfg(feature = "filters")]
pub strip_ansi_escape_codes: bool,
#[cfg(feature = "glob")]
pub allow_empty_glob: bool,
}
impl Clone for ActualSettings {
fn clone(&self) -> Self {
ActualSettings {
sort_maps: self.sort_maps,
snapshot_path: self.snapshot_path.clone(),
snapshot_suffix: self.snapshot_suffix.clone(),
input_file: self.input_file.clone(),
description: self.description.clone(),
info: self.info.clone(),
omit_expression: self.omit_expression,
prepend_module_to_snapshot: self.prepend_module_to_snapshot,
comparator: self.comparator.dyn_clone(),
#[cfg(feature = "redactions")]
redactions: self.redactions.clone(),
#[cfg(feature = "filters")]
filters: self.filters.clone(),
#[cfg(feature = "filters")]
strip_ansi_escape_codes: self.strip_ansi_escape_codes,
#[cfg(feature = "glob")]
allow_empty_glob: self.allow_empty_glob,
}
}
}
impl ActualSettings {
pub fn sort_maps(&mut self, value: bool) {
self.sort_maps = value;
}
pub fn snapshot_path<P: AsRef<Path>>(&mut self, path: P) {
self.snapshot_path = path.as_ref().to_path_buf();
}
pub fn snapshot_suffix<I: Into<String>>(&mut self, suffix: I) {
self.snapshot_suffix = suffix.into();
}
pub fn input_file<P: AsRef<Path>>(&mut self, p: P) {
self.input_file = Some(p.as_ref().to_path_buf());
}
pub fn description<S: Into<String>>(&mut self, value: S) {
self.description = Some(value.into());
}
#[cfg(feature = "serde")]
pub fn info<S: Serialize>(&mut self, s: &S) {
let serializer = ContentSerializer::<ValueError>::new();
let content = Serialize::serialize(s, serializer).unwrap();
// Apply redactions to metadata immediately when set. Unlike snapshot
// content (which is redacted lazily during serialization), metadata is
// redacted eagerly to ensure sensitive data never reaches the stored
// settings. The redacted content is then written to the snapshot file
// as-is without further redaction.
#[cfg(feature = "redactions")]
let content = self.redactions.apply_to_content(content);
self.info = Some(content);
}
pub fn raw_info(&mut self, content: &Content) {
self.info = Some(content.to_owned());
}
pub fn omit_expression(&mut self, value: bool) {
self.omit_expression = value;
}
pub fn prepend_module_to_snapshot(&mut self, value: bool) {
self.prepend_module_to_snapshot = value;
}
pub fn comparator(&mut self, value: Box<dyn Comparator>) {
self.comparator = value;
}
#[cfg(feature = "redactions")]
pub fn redactions<R: Into<Redactions>>(&mut self, r: R) {
self.redactions = r.into();
}
#[cfg(feature = "filters")]
pub fn filters<F: Into<Filters>>(&mut self, f: F) {
self.filters = f.into();
}
#[cfg(feature = "filters")]
pub fn strip_ansi_escape_codes(&mut self, value: bool) {
self.strip_ansi_escape_codes = value;
}
#[cfg(feature = "glob")]
pub fn allow_empty_glob(&mut self, value: bool) {
self.allow_empty_glob = value;
}
}
/// Configures how insta operates at test time.
///
/// Settings are always bound to a thread, and some default settings are always
/// available. These settings can be changed and influence how insta behaves on
/// that thread. They can be either temporarily or permanently changed.
///
/// 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.
///
/// Some of the settings can be changed but shouldn't as it will make it harder
/// for tools like cargo-insta or an editor integration to locate the snapshot
/// files.
///
/// 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(|| {
/// // runs the assertion with the changed settings enabled
/// insta::assert_snapshot!(...);
/// });
/// ```
#[derive(Clone)]
pub struct Settings {
// TODO: consider switching to `Rc` in the next breaking change — `Settings`
// are stored in thread-local storage and never cross thread boundaries, so
// `Arc` is unnecessary. Removing it would also let us drop the `Send + Sync`
// bounds on `Redaction` and `Comparator`.
inner: Arc<ActualSettings>,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
inner: Arc::new(ActualSettings {
sort_maps: false,
snapshot_path: "snapshots".into(),
snapshot_suffix: "".into(),
input_file: None,
description: None,
info: None,
omit_expression: false,
prepend_module_to_snapshot: true,
comparator: Box::new(crate::comparator::DefaultComparator),
#[cfg(feature = "redactions")]
redactions: Redactions::default(),
#[cfg(feature = "filters")]
filters: Filters::default(),
#[cfg(feature = "filters")]
strip_ansi_escape_codes: false,
#[cfg(feature = "glob")]
allow_empty_glob: false,
}),
}
}
}
impl Settings {
/// Returns the default settings.
///
/// It's recommended to use [`Self::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!`](crate::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
}
/// Disables prepending of modules to the snapshot filename.
///
/// By default, the filename of a snapshot is `<module>__<name>.snap`.
/// Setting this flag to `false` changes the snapshot filename to just
/// `<name>.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
}
/// Allows the [`glob!`] macro to succeed if it matches no files.
///
/// By default, the glob macro will fail the test if it does not find
/// any files to prevent accidental typos. This can be disabled when
/// fixtures should be conditional.
///
/// The default value is `false`.
#[cfg(feature = "glob")]
pub fn set_allow_empty_glob(&mut self, value: bool) {
self._private_inner_mut().allow_empty_glob(value);
}
/// Returns the current value for the empty glob setting.
#[cfg(feature = "glob")]
pub fn allow_empty_glob(&self) -> bool {
self.inner.allow_empty_glob
}
/// 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<I: Into<String>>(&mut self, suffix: I) {
self._private_inner_mut().snapshot_suffix(suffix);
}
/// 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
/// allows storing some metadata with a snapshot that refers 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<P: AsRef<Path>>(&mut self, p: P) {
self._private_inner_mut().input_file(p);
}
/// 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()
}
/// Sets the description.
///
/// The description is stored alongside the snapshot and will be displayed
/// in the diff UI. When a snapshot is captured, the Rust expression for that
/// snapshot is always retained. However, sometimes that information is not
/// super useful by itself, particularly when working with loops and generated
/// tests. In that case the `description` can be set as extra information.
///
/// See also [`Self::set_info`].
pub fn set_description<S: Into<String>>(&mut self, value: S) {
self._private_inner_mut().description(value);
}
/// Removes the description.
pub fn remove_description(&mut self) {
self._private_inner_mut().description = None;
}
/// Returns the current description
pub fn description(&self) -> Option<&str> {
self.inner.description.as_deref()
}
/// Sets the info.
///
/// The `info` is similar to `description` but for structured data. This is
/// stored with the snapshot and shown in the review UI. This for instance
/// can be used to show extended information that can make a reviewer better
/// understand what the snapshot is supposed to be testing.
///
/// As an example the input parameters to the function that creates the snapshot
/// can be persisted here.
///
/// **Note:** Redactions configured via [`Self::add_redaction`] are automatically
/// applied to the info metadata when it is set.
///
/// Alternatively you can use [`Self::set_raw_info`] instead.
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
pub fn set_info<S: Serialize>(&mut self, s: &S) {
self._private_inner_mut().info(s);
}
/// Sets the info from a content object.
///
/// This works like [`Self::set_info`] but does not require [`serde`].
///
/// **Note:** Unlike [`Self::set_info`], this method does NOT automatically apply
/// redactions. If you need redactions applied to metadata, use [`Self::set_info`]
/// instead (which requires the `serde` feature).
pub fn set_raw_info(&mut self, content: &Content) {
self._private_inner_mut().raw_info(content);
}
/// Removes the info.
pub fn remove_info(&mut self) {
self._private_inner_mut().info = None;
}
/// Returns the current info
pub(crate) fn info(&self) -> Option<&Content> {
self.inner.info.as_ref()
}
/// Returns the current info
pub fn has_info(&self) -> bool {
self.inner.info.is_some()
}
/// If set to true, does not retain the expression in the snapshot.
pub fn set_omit_expression(&mut self, value: bool) {
self._private_inner_mut().omit_expression(value);
}
/// Retrieves the [`Comparator`] that is currently active.
pub fn comparator(&self) -> &dyn Comparator {
self.inner.comparator.as_ref()
}
/// Sets the currently active [`Comparator`] to `value`.
pub fn set_comparator(&mut self, value: Box<dyn Comparator>) {
self._private_inner_mut().comparator = value;
}
/// Returns true if expressions are omitted from snapshots.
pub fn omit_expression(&self) -> bool {
self.inner.omit_expression
}
/// 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!`](crate::assert_debug_snapshot!).)
#[cfg(feature = "redactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn add_redaction<R: Into<Redaction>>(&mut self, selector: &str, replacement: R) {
self.add_redaction_impl(selector, replacement.into())
}
#[cfg(feature = "redactions")]
fn add_redaction_impl(&mut self, selector: &str, replacement: Redaction) {
self._private_inner_mut().redactions.0.push((
Selector::parse(selector).unwrap().make_static(),
Arc::new(replacement),
));
}
/// 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(selector, dynamic_redaction(...))`;
#[cfg(feature = "redactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn add_dynamic_redaction<I, F>(&mut self, selector: &str, func: F)
where
I: Into<Content>,
F: Fn(Content, ContentPath<'_>) -> I + Send + Sync + 'static,
{
self.add_redaction(selector, dynamic_redaction(func));
}
/// A special redaction that sorts a sequence or map.
///
/// This is a shortcut to `add_redaction(selector, sorted_redaction())`.
#[cfg(feature = "redactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn sort_selector(&mut self, selector: &str) {
self.add_redaction(selector, sorted_redaction());
}
/// Replaces the currently set redactions.
///
/// The default set is empty.
#[cfg(feature = "redactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn set_redactions<R: Into<Redactions>>(&mut self, redactions: R) {
self._private_inner_mut().redactions(redactions);
}
/// Removes all redactions.
#[cfg(feature = "redactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub fn clear_redactions(&mut self) {
self._private_inner_mut().redactions.0.clear();
}
/// Apply redactions to content.
#[cfg(feature = "redactions")]
#[cfg_attr(docsrs, doc(cfg(feature = "redactions")))]
pub(crate) fn apply_redactions(&self, content: Content) -> Content {
self.inner.redactions.apply_to_content(content)
}
/// Adds a new filter.
///
/// Filters are similar to redactions but are applied as regex onto the final snapshot
/// value. This can be used to perform modifications to the snapshot string that would
/// be impossible to do with redactions because for instance the value is just a string.
///
/// The first argument is the [`regex`] pattern to apply, the second is a replacement
/// string. The replacement string has the same functionality as the second argument
/// to [`regex::Regex::replace`].
///
/// This is useful to perform some cleanup procedures on the snapshot for unstable values.
///
/// ```rust
/// # use insta::Settings;
/// # async fn foo() {
/// # let mut settings = Settings::new();
/// settings.add_filter(r"\b[[:xdigit:]]{32}\b", "[UID]");
/// # }
/// ```
#[cfg(feature = "filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
pub fn add_filter<S: Into<String>>(&mut self, regex: &str, replacement: S) {
self._private_inner_mut().filters.add(regex, replacement);
}
/// Replaces the currently set filters.
///
/// The default set is empty.
#[cfg(feature = "filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
pub fn set_filters<F: Into<Filters>>(&mut self, filters: F) {
self._private_inner_mut().filters(filters);
}
/// Removes all filters.
#[cfg(feature = "filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
pub fn clear_filters(&mut self) {
self._private_inner_mut().filters.clear();
}
/// Returns the current filters
#[cfg(feature = "filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
pub(crate) fn filters(&self) -> &Filters {
&self.inner.filters
}
/// Enables or disables stripping of ANSI escape codes from snapshots.
///
/// When enabled, ANSI escape sequences (such as color codes, cursor movement,
/// etc.) are automatically removed from snapshot content before comparison.
/// This is useful when snapshotting output from tools that produce colored
/// terminal output.
///
/// The stripping is applied before any user-defined filters.
///
/// The default value is `false`.
///
/// ```rust
/// # use insta::Settings;
/// let mut settings = Settings::clone_current();
/// settings.set_strip_ansi_escape_codes(true);
/// settings.bind(|| {
/// // ANSI codes are stripped before snapshot comparison
/// insta::assert_snapshot!("\x1b[31mhello\x1b[0m", @"hello");
/// });
/// ```
#[cfg(feature = "filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
pub fn set_strip_ansi_escape_codes(&mut self, value: bool) {
self._private_inner_mut().strip_ansi_escape_codes(value);
}
/// Returns the current value for ANSI escape code stripping.
#[cfg(feature = "filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "filters")))]
pub fn strip_ansi_escape_codes(&self) -> bool {
self.inner.strip_ansi_escape_codes
}
/// Sets the snapshot path.
///
/// If not absolute, it's relative to where the test is in.
///
/// Defaults to `snapshots`.
pub fn set_snapshot_path<P: AsRef<Path>>(&mut self, path: P) {
self._private_inner_mut().snapshot_path(path);
}
/// 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.
///
/// This is an alternative to [`Self::bind_to_scope`]()
/// which does not require holding on to a drop guard. The return value
/// of the closure is passed through.
///
/// ```
/// # use insta::Settings;
/// let mut settings = Settings::clone_current();
/// settings.set_sort_maps(true);
/// settings.bind(|| {
/// // do stuff here
/// });
/// ```
pub fn bind<F: FnOnce() -> R, R>(&self, f: F) -> R {
let _guard = self.bind_to_scope();
f()
}
/// Like [`Self::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<F: Future<Output = T>, T>(&self, future: F) -> impl Future<Output = T> {
struct BindingFuture<F> {
settings: Arc<ActualSettings>,
future: F,
}
impl<F: Future> Future for BindingFuture<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
let inner = self.settings.clone();
// SAFETY: This is okay because `future` is pinned when `self` is.
let future = unsafe { self.map_unchecked_mut(|s| &mut s.future) };
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 {
settings: self.inner.clone(),
future,
}
}
/// Binds the settings to the current thread and resets when the drop
/// guard is released.
///
/// This is the recommended way to temporarily bind settings. It replaces
/// the earlier [`bind_to_scope`](Settings::bind_to_scope), and relies on
/// drop guards. An alternative is [`bind`](Settings::bind), which binds
/// for the duration of the block it wraps.
///
/// ```
/// # use insta::Settings;
/// let mut settings = Settings::clone_current();
/// settings.set_sort_maps(true);
/// let _guard = settings.bind_to_scope();
/// // do stuff here
/// ```
pub fn bind_to_scope(&self) -> SettingsBindDropGuard {
CURRENT_SETTINGS.with(|x| {
let mut x = x.borrow_mut();
let old = mem::replace(&mut x.inner, self.inner.clone());
SettingsBindDropGuard(Some(old), std::marker::PhantomData)
})
}
/// Runs a function with the current settings.
pub(crate) fn with<R, F: FnOnce(&Settings) -> R>(f: F) -> R {
CURRENT_SETTINGS.with(|x| f(&x.borrow()))
}
}
/// Returned from [`Settings::bind_to_scope`]
///
/// This type is not shareable between threads:
///
/// ```compile_fail E0277
/// let mut settings = insta::Settings::clone_current();
/// settings.set_snapshot_suffix("test drop guard");
/// let guard = settings.bind_to_scope();
///
/// std::thread::spawn(move || { let guard = guard; }); // doesn't compile
/// ```
///
/// This is to ensure tests under async runtimes like `tokio` don't show unexpected results
#[must_use = "The guard is immediately dropped so binding has no effect. Use `let _guard = ...` to bind it."]
pub struct SettingsBindDropGuard(
Option<Arc<ActualSettings>>,
/// A ZST that is not [`Send`] but is [`Sync`]
///
/// This is necessary due to the lack of stable [negative impls](https://github.com/rust-lang/rust/issues/68318).
///
/// Required as [`SettingsBindDropGuard`] modifies a thread local variable which would end up
/// with unexpected results if sent to a different thread.
std::marker::PhantomData<std::sync::MutexGuard<'static, ()>>,
);
impl Drop for SettingsBindDropGuard {
fn drop(&mut self) {
CURRENT_SETTINGS.with(|x| {
x.borrow_mut().inner = self.0.take().unwrap();
})
}
}
// Prevent accidental removal of Send/Sync (which is a semver-breaking change).
const _: () = {
fn _assert_send_sync<T: Send + Sync>() {}
fn _assert() {
_assert_send_sync::<Settings>();
#[cfg(feature = "redactions")]
_assert_send_sync::<Redactions>();
}
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
---
source: insta/src/runtime.rs
expression: "vec![1, 2, 3, 4, 5]"
---
[
1,
2,
3,
4,
5,
]
@@ -0,0 +1,5 @@
---
source: insta/src/test.rs
expression: "\"Just a string\""
---
Just a string
+4
View File
@@ -0,0 +1,4 @@
#[test]
fn test_embedded_test() {
assert_snapshot!("embedded", "Just a string");
}
+127
View File
@@ -0,0 +1,127 @@
use std::{
borrow::Cow,
env,
io::Write,
path::Path,
process::{Command, Stdio},
};
/// Are we running in in a CI environment?
pub fn is_ci() -> bool {
match env::var("CI").ok().as_deref() {
Some("false") | Some("0") | Some("") => false,
None => env::var("TF_BUILD").is_ok(),
Some(_) => true,
}
}
#[cfg(feature = "colors")]
pub use console::style;
#[cfg(not(feature = "colors"))]
mod fake_colors {
pub struct FakeStyledObject<D>(D);
macro_rules! style_attr {
($($name:ident)*) => {
$(
#[inline]
pub fn $name(self) -> FakeStyledObject<D> { self }
)*
}
}
impl<D> FakeStyledObject<D> {
style_attr!(red green yellow cyan bold dim underlined);
}
impl<D: std::fmt::Display> std::fmt::Display for FakeStyledObject<D> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
pub fn style<D>(val: D) -> FakeStyledObject<D> {
FakeStyledObject(val)
}
}
#[cfg(not(feature = "colors"))]
pub use self::fake_colors::*;
/// Returns the term width that insta should use.
pub fn term_width() -> usize {
#[cfg(feature = "colors")]
{
console::Term::stdout().size().1 as usize
}
#[cfg(not(feature = "colors"))]
{
74
}
}
/// Converts a path into a string that can be persisted.
pub fn path_to_storage(path: &Path) -> String {
#[cfg(windows)]
{
path.to_str().unwrap().replace('\\', "/")
}
#[cfg(not(windows))]
{
path.to_string_lossy().into()
}
}
/// Tries to format a given rust expression with rustfmt
pub 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 std::str::from_utf8(&output.stdout[start..end])
.unwrap()
.replace("\r\n", "\n")
.into();
}
}
}
Cow::Borrowed(value)
}
#[cfg(feature = "_cargo_insta_internal")]
pub fn get_cargo() -> std::ffi::OsString {
let cargo = env::var_os("CARGO");
let cargo = cargo
.as_deref()
.unwrap_or_else(|| std::ffi::OsStr::new("cargo"));
cargo.to_os_string()
}
#[test]
fn test_format_rust_expression() {
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("😄😄😄😄😄"), @"😄😄😄😄😄")
}
+53
View File
@@ -0,0 +1,53 @@
#![cfg(feature = "glob")]
#[test]
fn test_basic_globbing_parent_dir() {
insta::glob!("../inputs", "*.txt", |path| {
let contents = std::fs::read_to_string(path).unwrap();
insta::assert_json_snapshot!(&contents);
});
}
#[test]
fn test_basic_globbing_nested_parent_dir_base_path() {
insta::glob!("../inputs-nested", "*/*.txt", |path| {
let contents = std::fs::read_to_string(path).unwrap();
insta::assert_snapshot!(&contents);
});
}
#[test]
fn test_basic_globbing_nested_parent_glob() {
insta::glob!("..", "inputs-nested/*/*.txt", |path| {
let contents = std::fs::read_to_string(path).unwrap();
insta::assert_snapshot!(&contents);
});
}
#[test]
fn test_globs_follow_links_parent_dir_base_path() {
insta::glob!("../link-to-inputs", "*.txt", |path| {
let contents = std::fs::read_to_string(path).unwrap();
insta::assert_json_snapshot!(&contents);
});
}
#[test]
fn test_globs_follow_links_parent_dir_glob() {
insta::glob!("..", "link-to-inputs/*.txt", |path| {
let contents = std::fs::read_to_string(path).unwrap();
insta::assert_json_snapshot!(&contents);
});
}
#[test]
fn test_basic_globbing_absolute_dir() {
insta::glob!(
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/inputs"),
"*.txt",
|path| {
let contents = std::fs::read_to_string(path).unwrap();
insta::assert_json_snapshot!(&contents);
}
);
}
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/goodbye.txt
---
"Contents of goodbye"
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/hello.txt
---
"Contents of hello"
@@ -0,0 +1,7 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs-nested/a/file.txt
---
Hello A
@@ -0,0 +1,7 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs-nested/b/file.txt
---
Hello B
@@ -0,0 +1,7 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs-nested/a/file.txt
---
Hello A
@@ -0,0 +1,7 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs-nested/b/file.txt
---
Hello B
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/goodbye.txt
---
"Contents of goodbye"
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/hello.txt
---
"Contents of hello"
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/goodbye.txt
---
"Contents of goodbye"
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/hello.txt
---
"Contents of hello"
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/goodbye.txt
---
"Contents of goodbye"
@@ -0,0 +1,6 @@
---
source: tests/glob_submodule/mod.rs
expression: "&contents"
input_file: tests/inputs/hello.txt
---
"Contents of hello"
@@ -0,0 +1 @@
Hello A
@@ -0,0 +1 @@
Hello B
+1
View File
@@ -0,0 +1 @@
Contents of goodbye
+1
View File
@@ -0,0 +1 @@
Contents of hello
@@ -0,0 +1 @@
Contents of goodbye
@@ -0,0 +1 @@
Contents of hello
@@ -0,0 +1,9 @@
---
source: insta/tests/test_settings.rs
expression: "vec![1, 2, 3]"
---
[
1,
2,
3,
]
@@ -0,0 +1,5 @@
---
source: insta/tests/test_advanced.rs
expression: "&value"
---
1
@@ -0,0 +1,5 @@
---
source: insta/tests/test_advanced.rs
expression: "&value"
---
2
@@ -0,0 +1,5 @@
---
source: insta/tests/test_advanced.rs
expression: "&value"
---
3
@@ -0,0 +1,5 @@
---
source: insta/tests/test_basic.rs
expression: expr
---
name
@@ -0,0 +1,7 @@
---
source: insta/tests/test_basic.rs
expression: "\"foo\\r\\nbar\\r\\nbaz\""
---
foo
bar
baz
@@ -0,0 +1,9 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3]"
---
[
1,
2,
3,
]
@@ -0,0 +1,5 @@
---
source: insta/tests/test_basic.rs
expression: td
---
TestDisplay struct
@@ -0,0 +1,19 @@
---
source: insta/tests/test_basic.rs
expression: m
---
? - 1
- 3
: 4
? - 1
- 4
: 4
? - 2
- 3
: 4
? - 3
- 3
: 4
? - 9
- 3
: 4
@@ -0,0 +1,9 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3]"
---
[
1,
2,
3
]
@@ -0,0 +1,5 @@
---
source: insta/tests/test_basic.rs
expression: "\"aoeu\""
---
aoeu
@@ -0,0 +1,9 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3, 4, 5]"
---
- 1
- 2
- 3
- 4
- 5
@@ -0,0 +1,5 @@
---
source: insta/tests/test_basic.rs
expression: "\"Testing\""
---
Testing
@@ -0,0 +1,7 @@
---
source: insta/tests/test_basic.rs
expression: "\"foo\\r\\nbar\\r\\nbaz\\r\\n\""
---
foo
bar
baz
@@ -0,0 +1,10 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3, 4]"
---
[
1,
2,
3,
4,
]
@@ -0,0 +1,11 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3, 4, 5]"
---
[
1,
2,
3,
4,
5,
]
@@ -0,0 +1,9 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3]"
---
[
1,
2,
3,
]
@@ -0,0 +1,5 @@
---
source: insta/tests/test_basic.rs
expression: "\"whatever\""
---
whatever
@@ -0,0 +1,5 @@
---
source: insta/tests/test_basic.rs
expression: td
---
TestDisplay struct
@@ -0,0 +1,10 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3, 4]"
---
[
1,
2,
3,
4
]
@@ -0,0 +1,11 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3, 4, 5]"
---
[
1,
2,
3,
4,
5
]
@@ -0,0 +1,9 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3]"
---
[
1,
2,
3
]
@@ -0,0 +1,9 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3]"
---
[
1,
2,
3,
]
@@ -0,0 +1,8 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3, 4]"
---
- 1
- 2
- 3
- 4
@@ -0,0 +1,9 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3, 4, 5]"
---
- 1
- 2
- 3
- 4
- 5
@@ -0,0 +1,7 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3]"
---
- 1
- 2
- 3
@@ -0,0 +1,7 @@
---
source: insta/tests/test_basic.rs
expression: "vec![1, 2, 3]"
---
- 1
- 2
- 3
@@ -0,0 +1,6 @@
---
source: insta/tests/test_binary.rs
expression: "b\"test\".to_vec()"
extension: txt
snapshot_kind: binary
---
@@ -0,0 +1 @@
test
@@ -0,0 +1,6 @@
---
source: insta/tests/test_binary.rs
expression: "b\"test\".to_vec()"
extension: tar.gz
snapshot_kind: binary
---
@@ -0,0 +1,6 @@
---
source: insta/tests/test_binary.rs
expression: "b\"null\".to_vec()"
extension: json
snapshot_kind: binary
---
@@ -0,0 +1 @@
null
@@ -0,0 +1,6 @@
---
source: tests/test_glob.rs
expression: "&contents"
input_file: tests/inputs/goodbye.txt
---
"Contents of goodbye"
@@ -0,0 +1,6 @@
---
source: tests/test_glob.rs
expression: "&contents"
input_file: tests/inputs/hello.txt
---
"Contents of hello"
@@ -0,0 +1,7 @@
---
source: tests/test_glob.rs
expression: "&contents"
input_file: tests/inputs-nested/a/file.txt
---
Hello A
@@ -0,0 +1,7 @@
---
source: tests/test_glob.rs
expression: "&contents"
input_file: tests/inputs-nested/b/file.txt
---
Hello B
@@ -0,0 +1,6 @@
---
source: tests/test_glob.rs
expression: "&contents"
input_file: tests/inputs/goodbye.txt
---
"Contents of goodbye"
@@ -0,0 +1,6 @@
---
source: tests/test_glob.rs
expression: "&contents"
input_file: tests/inputs/hello.txt
---
"Contents of hello"
@@ -0,0 +1,5 @@
---
source: insta/tests/test_inline.rs
expression: "\"Testing-thread-2\""
---
Testing-thread-2
@@ -0,0 +1,5 @@
---
source: insta/tests/test_inline.rs
expression: "\"Testing-thread\""
---
Testing-thread
@@ -0,0 +1,16 @@
---
source: insta/tests/test_redaction.rs
expression: "Selector :: parse(\".foo.bar\").unwrap()"
---
Selector {
selectors: [
[
Key(
"foo",
),
Key(
"bar",
),
],
],
}
@@ -0,0 +1,16 @@
---
source: insta/tests/test_redaction.rs
expression: "Selector :: parse(\".foo[\\\"bar\\\"]\").unwrap()"
---
Selector {
selectors: [
[
Key(
"foo",
),
Key(
"bar",
),
],
],
}
@@ -0,0 +1,17 @@
---
source: insta/tests/test_redaction.rs
expression: "Selector :: parse(\".foo.bar.**\").unwrap()"
---
Selector {
selectors: [
[
Key(
"foo",
),
Key(
"bar",
),
DeepWildcard,
],
],
}
@@ -0,0 +1,20 @@
---
source: insta/tests/test_redaction.rs
expression: "Selector :: parse(\".foo.bar[]\").unwrap()"
---
Selector {
selectors: [
[
Key(
"foo",
),
Key(
"bar",
),
Range(
None,
None,
),
],
],
}
@@ -0,0 +1,24 @@
---
source: insta/tests/test_redaction.rs
expression: "Selector :: parse(\".foo.bar[10:20]\").unwrap()"
---
Selector {
selectors: [
[
Key(
"foo",
),
Key(
"bar",
),
Range(
Some(
10,
),
Some(
20,
),
),
],
],
}
@@ -0,0 +1,22 @@
---
source: insta/tests/test_redaction.rs
expression: "Selector :: parse(\".foo.bar[10:]\").unwrap()"
---
Selector {
selectors: [
[
Key(
"foo",
),
Key(
"bar",
),
Range(
Some(
10,
),
None,
),
],
],
}
@@ -0,0 +1,22 @@
---
source: insta/tests/test_redaction.rs
expression: "Selector :: parse(\".foo.bar[:10]\").unwrap()"
---
Selector {
selectors: [
[
Key(
"foo",
),
Key(
"bar",
),
Range(
None,
Some(
10,
),
),
],
],
}

Some files were not shown because too many files have changed in this diff Show More