Skip to content
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
2 changes: 2 additions & 0 deletions crates/libtest-json/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
<!-- next-header -->
## [Unreleased] - ReleaseDate

- Add `DiscoverCase::source`

## [0.0.2] - 2025-09-12

- Make schema `snake_case`
Expand Down
43 changes: 43 additions & 0 deletions crates/libtest-json/event.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,39 @@
}
}
},
"Source": {
"description": "Where a case comes from",
"type": "object",
"properties": {
"path": {
"description": "The file as reported by the compiler (`file!()`) or by the code that built the case",
"type": "string"
},
"start_line": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"start_col": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"end_line": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"end_col": {
"type": "integer",
"format": "uint",
"minimum": 0
}
},
"required": [
"path"
]
},
"RunMode": {
"type": "string",
"enum": [
Expand All @@ -142,6 +175,16 @@
"description": "An identifier that is unique across the entire run",
"type": "string"
},
"source": {
"anyOf": [
{
"$ref": "#/$defs/Source"
},
{
"type": "null"
}
]
},
"mode": {
"$ref": "#/$defs/RunMode"
},
Expand Down
59 changes: 59 additions & 0 deletions crates/libtest-json/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ impl DiscoverStart {
pub struct DiscoverCase {
/// An identifier that is unique across the entire run
pub name: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub source: Option<Source>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "RunMode::is_default")
Expand Down Expand Up @@ -160,6 +165,33 @@ impl DiscoverCase {
buffer.keyval_sep().unwrap();
buffer.value(&self.name).unwrap();

if let Some(source) = &self.source {
buffer.val_sep().unwrap();
buffer.key("source").unwrap();
buffer.keyval_sep().unwrap();
buffer.open_object().unwrap();

buffer.key("path").unwrap();
buffer.keyval_sep().unwrap();
buffer.value(&source.path).unwrap();

if let Some(lines) = &source.lines {
for (key, value) in [
("start_line", lines.start_line),
("start_col", lines.start_col),
("end_line", lines.end_line),
("end_col", lines.end_col),
] {
buffer.val_sep().unwrap();
buffer.key(key).unwrap();
buffer.keyval_sep().unwrap();
buffer.value(value as u64).unwrap();
}
}

buffer.close_object().unwrap();
}

if !self.mode.is_default() {
buffer.val_sep().unwrap();
buffer.key("mode").unwrap();
Expand Down Expand Up @@ -187,6 +219,33 @@ impl DiscoverCase {
}
}

/// Where a case comes from
#[derive(Clone, Debug)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub struct Source {
/// The file as reported by the compiler (`file!()`) or by the code that built the case
pub path: String,
#[cfg_attr(
feature = "serde",
serde(flatten, default, skip_serializing_if = "Option::is_none")
)]
pub lines: Option<LineSpan>,
}

/// 1-based, `end_col` exclusive, columns count characters
#[derive(Clone, Debug)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub struct LineSpan {
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
}

#[derive(Clone, Debug)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
39 changes: 39 additions & 0 deletions crates/libtest-json/tests/roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fn discover_case() {
t(
libtest_json::event::DiscoverCase {
name: "Hello\tworld!".to_owned(),
source: None,
mode: libtest_json::RunMode::Test,
selected: true,
elapsed_s: None,
Expand All @@ -44,6 +45,7 @@ fn discover_case() {
t(
libtest_json::event::DiscoverCase {
name: "Hello\tworld!".to_owned(),
source: None,
mode: libtest_json::RunMode::Bench,
selected: false,
elapsed_s: Some(libtest_json::Elapsed(Default::default())),
Expand All @@ -52,6 +54,43 @@ fn discover_case() {
r#"{"event":"discover_case","name":"Hello\tworld!","mode":"bench","selected":false,"elapsed_s":"0"}"#
]],
);

t(
libtest_json::event::DiscoverCase {
name: "Hello\tworld!".to_owned(),
source: Some(libtest_json::event::Source {
path: "src/lib.rs".to_owned(),
lines: Some(libtest_json::event::LineSpan {
start_line: 11,
start_col: 4,
end_line: 11,
end_col: 14,
}),
}),
mode: libtest_json::RunMode::Test,
selected: true,
elapsed_s: None,
},
str![[
r#"{"event":"discover_case","name":"Hello\tworld!","source":{"path":"src/lib.rs","start_line":11,"start_col":4,"end_line":11,"end_col":14}}"#
]],
);

t(
libtest_json::event::DiscoverCase {
name: "Hello\tworld!".to_owned(),
source: Some(libtest_json::event::Source {
path: "tests/fixtures/a.toml".to_owned(),
lines: None,
}),
mode: libtest_json::RunMode::Test,
selected: true,
elapsed_s: None,
},
str![[
r#"{"event":"discover_case","name":"Hello\tworld!","source":{"path":"tests/fixtures/a.toml"}}"#
]],
);
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/libtest2-harness/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
<!-- next-header -->
## [Unreleased] - ReleaseDate

### Features

- Report `Case::source` in `discover_case`

## [0.0.3] - 2025-09-18

### Fixes
Expand Down
27 changes: 26 additions & 1 deletion crates/libtest2-harness/src/harness.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use libtest_lexarg::OutputFormat;

use crate::{cli, notify, Case, RunError, RunMode, TestContext};
use crate::{cli, notify, Case, RunError, RunMode, Source, TestContext};

pub trait HarnessState: sealed::_HarnessState_is_Sealed {}

Expand Down Expand Up @@ -106,6 +106,7 @@ impl Harness<StateParsed> {
self.state.notifier.notify(
notify::event::DiscoverCase {
name: case.name().to_owned(),
source: case.source().map(notify_source),
mode: RunMode::Test,
selected,
elapsed_s: Some(notify::Elapsed(self.state.start.elapsed())),
Expand Down Expand Up @@ -293,6 +294,30 @@ fn matches_filter(case: &dyn Case, filter: &str, opts: &libtest_lexarg::TestOpts
}
}

fn notify_source(source: &Source) -> notify::event::Source {
match source {
Source::Rust {
source_file,
start_line,
start_col,
end_line,
end_col,
} => notify::event::Source {
path: source_file.to_string_lossy().into_owned(),
lines: Some(notify::event::LineSpan {
start_line: *start_line,
start_col: *start_col,
end_line: *end_line,
end_col: *end_col,
}),
},
Source::Path(path) => notify::event::Source {
path: path.to_string_lossy().into_owned(),
lines: None,
},
}
}

fn run(
start: &std::time::Instant,
opts: &libtest_lexarg::TestOpts,
Expand Down
46 changes: 45 additions & 1 deletion crates/libtest2-proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use std::iter::FromIterator;

use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree};
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};

#[allow(missing_docs)]
#[proc_macro_attribute]
Expand All @@ -18,6 +18,50 @@ pub fn main(attribute: TokenStream, item: TokenStream) -> TokenStream {
generate("main", "libtest2", attribute, item)
}

/// Expands to `(file!(), line!(), column!(), <name length in chars>)` for a test's name
#[doc(hidden)]
#[proc_macro]
pub fn _source(input: TokenStream) -> TokenStream {
let mut input = input.into_iter();
let name = match (input.next(), input.next()) {
(Some(TokenTree::Ident(name)), None) => name,
_ => panic!("expected the test function's name"),
};

// `line!()`/`column!()` report where they are invoked, so every token takes the name's span
// to point at the `fn` name instead of the `#[test]` attribute
let span = name.span();
let punct = |ch, spacing| {
let mut punct = Punct::new(ch, spacing);
punct.set_span(span);
TokenTree::Punct(punct)
};

let mut fields = TokenStream::new();
for builtin in ["file", "line", "column"] {
let mut args = Group::new(Delimiter::Parenthesis, TokenStream::new());
args.set_span(span);
fields.extend([
punct(':', Spacing::Joint),
punct(':', Spacing::Alone),
TokenTree::Ident(Ident::new("core", span)),
punct(':', Spacing::Joint),
punct(':', Spacing::Alone),
TokenTree::Ident(Ident::new(builtin, span)),
punct('!', Spacing::Alone),
TokenTree::Group(args),
punct(',', Spacing::Alone),
]);
}
let mut len = Literal::usize_suffixed(name.to_string().chars().count());
len.set_span(span);
fields.extend([TokenTree::Literal(len)]);

let mut tuple = Group::new(Delimiter::Parenthesis, fields);
tuple.set_span(span);
TokenStream::from_iter([TokenTree::Group(tuple)])
}

/// Generates the equivalent of this Rust code as a `TokenStream`:
///
/// ```nocompile
Expand Down
2 changes: 2 additions & 0 deletions crates/libtest2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
<!-- next-header -->
## [Unreleased] - ReleaseDate

- `#[test]` reports the test's source location

## [0.0.3] - 2025-09-18

## [0.0.2] - 2025-09-12
Expand Down
1 change: 1 addition & 0 deletions crates/libtest2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub mod _private {
pub use distributed_list::DistributedList;
pub use libtest2_harness::Source;
pub use libtest2_harness::TestKind;
pub use libtest2_proc_macro::_source as source;

pub use crate::_main_parse as main_parse;
pub use crate::_parse_ignore as parse_ignore;
Expand Down
12 changes: 11 additions & 1 deletion crates/libtest2/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,17 @@ macro_rules! _test_parse {
Default::default()
}
fn source(&self) -> Option<&$crate::_private::Source> {
None
static SOURCE: ::std::sync::OnceLock<$crate::_private::Source> = ::std::sync::OnceLock::new();
Some(SOURCE.get_or_init(|| {
let (file, line, col, len) = $crate::_private::source!($name);
$crate::_private::Source::Rust {
source_file: ::std::path::PathBuf::from(file),
start_line: line as usize,
start_col: col as usize,
end_line: line as usize,
end_col: col as usize + len,
}
}))
}
fn exclusive(&self, _: &$crate::TestContext) -> bool {
false
Expand Down
Loading