Skip to content

Macro-less variants of expect and expect_file #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.4.0

* Add non-macro versions of `expect` and `expect_file` ([#32])

# 1.3.0

* Add `data()` getter to Expect ([#31])
Expand All @@ -13,6 +17,7 @@

* No changelog until this point :-(

[#32]: https://github.com/rust-analyzer/expect-test/pull/32
[#31]: https://github.com/rust-analyzer/expect-test/pull/31
[#27]: https://github.com/rust-analyzer/expect-test/pull/27
[#26]: https://github.com/rust-analyzer/expect-test/pull/26
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "expect-test"
version = "1.3.0"
version = "1.4.0"
description = "Minimalistic snapshot testing library"
keywords = ["snapshot", "testing", "expect"]
categories = ["development-tools::testing"]
Expand Down
144 changes: 114 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
//!
//! ## Minimal Supported Rust Version
//!
//! This crate's minimum supported `rustc` version is `1.45.0`. MSRV is updated
//! This crate's minimum supported `rustc` version is `1.46.0`. MSRV is updated
//! conservatively, supporting roughly 10 minor versions of `rustc`. MSRV bump
//! is not considered semver breaking, but will require at least minor version
//! bump.
Expand Down Expand Up @@ -163,7 +163,7 @@ fn update_expect() -> bool {
env::var("UPDATE_EXPECT").is_ok()
}

/// Creates an instance of `Expect` from string literal:
/// Creates an instance of `Expect` from a string literal:
///
/// ```
/// # use expect_test::expect;
Expand All @@ -190,6 +190,61 @@ macro_rules! expect {
[[]] => { $crate::expect![[""]] };
}

/// Creates an instance of `Expect` from a string literal:
///
/// ```
/// # use expect_test::expect;
/// expect("
/// Foo { value: 92 }
/// ");
/// expect(r#"{"Foo": 92}"#);
/// ```
///
/// Leading indentation is stripped.
///
/// This uses the `#[track_caller]` attribute to resolve relative file paths.
/// This [may not work as expected][1] if this is called from a function which als uses `#[track_caller]`,
/// or if the toolchain is [configured to strip caller information][2].
/// In these cases you may use the macro form, [`expect!`].
///
/// [1]: https://github.com/rust-analyzer/expect-test/issues/15#issuecomment-939308821
/// [2]: https://doc.rust-lang.org/beta/unstable-book/compiler-flags/location-detail.html
#[track_caller]
pub fn expect(data: impl ExpectedData) -> Expect {
let location = std::panic::Location::caller();
Expect {
position: Position {
file: location.file(),
line: location.line(),
column: location.column(),
},
data: data.str(),
indent: true,
}
}

pub trait ExpectedData {
fn str(&self) -> &'static str;
}

impl ExpectedData for &'static str {
fn str(&self) -> &'static str {
self
}
}

impl ExpectedData for [&'static str; 1] {
fn str(&self) -> &'static str {
self[0]
}
}

impl ExpectedData for [&'static str; 0] {
fn str(&self) -> &'static str {
""
}
}

/// Creates an instance of `ExpectFile` from relative or absolute path:
///
/// ```
Expand All @@ -204,6 +259,25 @@ macro_rules! expect_file {
}};
}

/// Creates an instance of `ExpectFile` from relative or absolute path:
///
/// ```
/// # use expect_test::expect_file;
/// expect_file("./test_data/bar.html");
/// ```
///
/// This uses the `#[track_caller]` attribute to resolve relative file paths.
/// This [may not work as expected][1] if this is called from a function which als uses `#[track_caller]`,
/// or if the toolchain is [configured to strip caller information][2].
/// In these cases you may use the macro form, [`expect_file!`].
///
/// [1]: https://github.com/rust-analyzer/expect-test/issues/15#issuecomment-939308821
/// [2]: https://doc.rust-lang.org/beta/unstable-book/compiler-flags/location-detail.html
#[track_caller]
pub fn expect_file(path: impl Into<std::path::PathBuf>) -> ExpectFile {
ExpectFile { path: path.into(), position: std::panic::Location::caller().file() }
}

/// Self-updating string literal.
#[derive(Debug)]
pub struct Expect {
Expand Down Expand Up @@ -311,22 +385,18 @@ impl Expect {
let mut line_start = 0;
for (i, line) in lines_with_ends(file).enumerate() {
if i == self.position.line as usize - 1 {
// `column` points to the first character of the macro invocation:
// `column` points to the first character of the macro invocation/function call:
//
// expect![[r#""#]] expect![""]
// ^ ^ ^ ^
// column offset offset
// expect![[r#""#]] expect![""] expect("") expect([""])
// ^ ^ ^ ^ ^ ^ ^ ^
// column offset
//
// Seek past the exclam, then skip any whitespace and
// the macro delimiter to get to our argument.
// we seek until we find the first character of the string literal, if present.
let byte_offset = line
.char_indices()
.skip((self.position.column - 1).try_into().unwrap())
.skip_while(|&(_, c)| c != '!')
.skip(1) // !
.skip_while(|&(_, c)| c.is_whitespace())
.skip(1) // [({
.skip_while(|&(_, c)| c.is_whitespace())
.skip_while(|&(_, c)| !matches!(c, '[' | '(' | '{'))
.skip(1)
.next()
.expect("Failed to parse macro invocation")
.0;
Expand All @@ -346,29 +416,35 @@ impl Expect {
let literal_start = literal_start + (lit_to_eof.len() - lit_to_eof_trimmed.len());

let literal_len =
locate_end(lit_to_eof_trimmed).expect("Couldn't find closing delimiter for `expect!`.");
locate_end(lit_to_eof_trimmed).expect("Couldn't find closing delimiter for `expect`.");
let literal_range = literal_start..literal_start + literal_len;
Location { line_indent, literal_range }
}
}

fn locate_end(arg_start_to_eof: &str) -> Option<usize> {
match arg_start_to_eof.chars().next()? {
c if c.is_whitespace() => panic!("skip whitespace before calling `locate_end`"),

// expect![[]]
'[' => {
let c = arg_start_to_eof.chars().next()?;
if c.is_whitespace() {
panic!("skip whitespace before calling `locate_end`")
}
match c {
// expect![["..."]] | expect!(["..."])
'[' | '(' => {
let end = if c == '[' { ']' } else { ')' };
let str_start_to_eof = arg_start_to_eof[1..].trim_start();
if str_start_to_eof.chars().next() == Some(end) {
return Some(2);
}
let str_len = find_str_lit_len(str_start_to_eof)?;
let str_end_to_eof = &str_start_to_eof[str_len..];
let closing_brace_offset = str_end_to_eof.find(']')?;
let closing_brace_offset = str_end_to_eof.find(end)?;
Some((arg_start_to_eof.len() - str_end_to_eof.len()) + closing_brace_offset + 1)
}

// expect![] | expect!{} | expect!()
']' | '}' | ')' => Some(0),

// expect!["..."] | expect![r#"..."#]
// expect!["..."] | expect![r#"..."#] | expect("...")
_ => find_str_lit_len(arg_start_to_eof),
}
}
Expand Down Expand Up @@ -591,11 +667,7 @@ fn lit_kind_for_patch(patch: &str) -> StrLitKind {
let has_dquote = patch.chars().any(|c| c == '"');
if !has_dquote {
let has_bslash_or_newline = patch.chars().any(|c| matches!(c, '\\' | '\n'));
return if has_bslash_or_newline {
StrLitKind::Raw(1)
} else {
StrLitKind::Normal
};
return if has_bslash_or_newline { StrLitKind::Raw(1) } else { StrLitKind::Normal };
}

// Find the maximum number of hashes that follow a double quote in the string.
Expand Down Expand Up @@ -731,7 +803,14 @@ mod tests {

#[test]
fn test_trivial_assert() {
expect!["5"].assert_eq("5");
expect!["1"].assert_eq("1");
expect!["2"].assert_eq("2");
}

#[test]
fn test_trivial_assert_fn() {
expect("3").assert_eq("3");
expect("4").assert_eq("4");
}

#[test]
Expand All @@ -751,15 +830,15 @@ mod tests {
expect![[r##"[r#"{"foo": 42}"#]"##]].assert_eq(&patch);

let patch = format_patch(Some(0), "hello\nworld\n");
expect![[r##"
expect([r##"
[r#"
hello
world
"#]"##]]
"#]"##])
.assert_eq(&patch);

let patch = format_patch(Some(4), "single line");
expect![[r#""single line""#]].assert_eq(&patch);
expect([r#""single line""#]).assert_eq(&patch);
}

#[test]
Expand Down Expand Up @@ -795,6 +874,11 @@ mod tests {
expect_file!["./lib.rs"].assert_eq(include_str!("./lib.rs"))
}

#[test]
fn test_expect_file_fn() {
expect_file("./lib.rs").assert_eq(include_str!("./lib.rs"))
}

#[test]
fn smoke_test_indent() {
fn check_indented(input: &str, mut expect: Expect) {
Expand Down
2 changes: 1 addition & 1 deletion xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::env;

use xaction::{cargo_toml, cmd, git, push_rustup_toolchain, section, Result};

const MSRV: &str = "1.45.0";
const MSRV: &str = "1.46.0";

fn main() {
if let Err(err) = try_main() {
Expand Down