Skip to content

Commit 47a539c

Browse files
committed
convert relative paths to forward slashes on Windows
A breaking change, but I think bringing Windows in line with Unix is valuable. Also fix up other path-related documentation, and add tests for everything.
1 parent 6481faa commit 47a539c

File tree

6 files changed

+201
-57
lines changed

6 files changed

+201
-57
lines changed

README.md

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ Given:
2121
`datatest-stable` will call the `my_test` function once per matching file in
2222
the directory. Directory traversals are recursive.
2323

24-
`datatest-stable` works with [cargo nextest](https://nexte.st/), and is part of the [nextest-rs
25-
organization](https://github.com/nextest-rs/) on GitHub.
24+
`datatest-stable` works with [cargo nextest](https://nexte.st/), and is part
25+
of the [nextest-rs organization](https://github.com/nextest-rs/) on GitHub.
26+
With nextest, each test case is represented as a separate test, and is run
27+
as a separate process in parallel.
2628

2729
## Usage
2830

@@ -49,8 +51,10 @@ harness = false
4951
* `fn(&P, Vec<u8>) -> datatest_stable::Result<()>` where `P` is `Path` or `Utf8Path`. If the
5052
extra `Vec<u8>` parameter is specified, the contents of the file will be loaded and passed
5153
in as a `Vec<u8>` (erroring out if that failed).
52-
* `root` - The path to the root directory where the input files (fixtures) live. This path is
53-
relative to the root of the crate (the directory where the crate’s `Cargo.toml` is located).
54+
* `root` - The path to the root directory where the input files (fixtures)
55+
live. This path is relative to the root of the crate (the directory where
56+
the crate’s `Cargo.toml` is located). Absolute paths are not supported, and
57+
will panic at runtime.
5458

5559
`root` is an arbitrary expression that implements `Display`, such as `&str`, or a
5660
function call that returns a `Utf8PathBuf`.
@@ -62,11 +66,21 @@ harness = false
6266
`pattern` is an arbitrary expression that implements `Display`, such as
6367
`&str`, or a function call that returns a `String`.
6468

65-
The passed-in `Path` and `Utf8Path` are **absolute** paths to the files to be tested.
66-
6769
The three parameters can be repeated if you have multiple sets of data-driven tests to be run:
6870
`datatest_stable::harness!(testfn1, root1, pattern1, testfn2, root2, pattern2)`.
6971

72+
### Relative paths
73+
74+
The `pattern` argument is tested against the **relative** path of each file,
75+
**excluding** the `root` prefix – not the absolute path.
76+
77+
The `Path` and `Utf8Path` passed into the test functions are **relative** to
78+
the root of the crate, obtained by joining `root` to the relative path of
79+
the file.
80+
81+
For universality, all relative paths use `/` as the path separator,
82+
including on Windows.
83+
7084
### Examples
7185

7286
This is an example test. Use it with `harness = false`.
@@ -93,6 +107,11 @@ datatest_stable::harness!(
93107
);
94108
````
95109

110+
If `path/to/fixtures` contains a file `foo/bar.txt`, then:
111+
112+
* The pattern `r"^.*/*"` will match `foo/bar.txt`.
113+
* `my_test` and `my_test_utf8` will be called with `"path/to/fixtures/foo/bar.txt"`.
114+
96115
### Embedding directories at compile time
97116

98117
With the `include-dir` feature enabled, you can use the
@@ -140,7 +159,9 @@ datatest_stable::harness!(
140159
````
141160

142161
In this case, the passed-in `Path` and `Utf8Path` are **relative** to the
143-
root of the included directory.
162+
root of the included directory. Like elsewhere in `datatest-stable`, these
163+
relative paths always use forward slashes as separators, including on
164+
Windows.
144165

145166
Because the files don’t exist on disk, the test functions must accept their
146167
contents as either a `String` or a `Vec<u8>`. If the argument is not
@@ -170,9 +191,11 @@ datatest_stable::harness!(
170191
);
171192
````
172193

173-
In this case, note that `path` will be absolute if `FIXTURES` is a string,
174-
and relative if `FIXTURES` is a `Dir`. Your test should be prepared to
175-
handle either case.
194+
In this case, note that `path` will be relative to the **crate directory**
195+
(e.g. `tests/files/foo/bar.txt`) if `FIXTURES` is a string, and relative to
196+
the **include directory** (e.g. `foo/bar.txt`) if `FIXTURES` is a
197+
[`Dir`](https://docs.rs/include_dir/0.7.4/include_dir/dir/struct.Dir.html). Your test should be prepared to handle either
198+
case.
176199

177200
## Features
178201

src/data_source.rs

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// Copyright (c) The datatest-stable Contributors
22
// SPDX-License-Identifier: MIT OR Apache-2.0
33

4-
use camino::{Utf8Path, Utf8PathBuf};
4+
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
55

66
#[derive(Debug)]
77
#[doc(hidden)]
88
pub enum DataSource {
9+
// This is relative to the crate root, and stored with forward slashes.
910
Directory(Utf8PathBuf),
1011
#[cfg(feature = "include-dir")]
1112
IncludeDir(std::borrow::Cow<'static, include_dir::Dir<'static>>),
@@ -31,18 +32,23 @@ impl DataSource {
3132
///
3233
/// Used for `--exact` matches.
3334
pub(crate) fn derive_exact(&self, filter: &str, test_name: &str) -> Option<TestEntry> {
34-
let rel_path = filter.strip_prefix(test_name)?.strip_prefix("::")?;
35+
// include_dir 0.7.4 returns paths with forward slashes, including on
36+
// Windows. But that isn't part of the stable API it seems, so we call
37+
// `rel_path_to_forward_slashes` anyway.
38+
let rel_path = rel_path_to_forward_slashes(
39+
filter.strip_prefix(test_name)?.strip_prefix("::")?.as_ref(),
40+
);
3541
match self {
3642
DataSource::Directory(path) => Some(TestEntry {
37-
source: TestSource::Path(path.join(rel_path)),
38-
rel_path: rel_path.into(),
43+
source: TestSource::Path(rel_path_to_forward_slashes(&path.join(&rel_path))),
44+
rel_path,
3945
}),
4046
#[cfg(feature = "include-dir")]
4147
DataSource::IncludeDir(dir) => {
42-
let file = dir.get_file(rel_path)?;
48+
let file = dir.get_file(&rel_path)?;
4349
Some(TestEntry {
4450
source: TestSource::IncludeDir(file),
45-
rel_path: rel_path.into(),
51+
rel_path,
4652
})
4753
}
4854
}
@@ -122,8 +128,15 @@ fn iter_include_dir<'a>(
122128
stack: dir.entries().iter().collect(),
123129
}
124130
.map(|file| {
125-
let rel_path = Utf8PathBuf::try_from(file.path().to_path_buf())
126-
.map_err(|error| error.into_io_error())?;
131+
// include_dir 0.7.4 returns paths with forward slashes, including on
132+
// Windows. But that isn't part of the stable API it seems, so we call
133+
// `rel_path_to_forward_slashes` anyway.
134+
let rel_path = match file.path().try_into() {
135+
Ok(path) => rel_path_to_forward_slashes(path),
136+
Err(error) => {
137+
return Err(error.into_io_error());
138+
}
139+
};
127140
Ok(TestEntry {
128141
source: TestSource::IncludeDir(file),
129142
rel_path,
@@ -139,10 +152,11 @@ pub(crate) struct TestEntry {
139152

140153
impl TestEntry {
141154
pub(crate) fn from_full_path(root: &Utf8Path, path: Utf8PathBuf) -> Self {
142-
let rel_path = path
143-
.strip_prefix(root)
144-
.unwrap_or_else(|_| panic!("failed to strip root '{}' from path '{}'", root, path))
145-
.to_owned();
155+
let path = rel_path_to_forward_slashes(&path);
156+
let rel_path =
157+
rel_path_to_forward_slashes(path.strip_prefix(root).unwrap_or_else(|_| {
158+
panic!("failed to strip root '{}' from path '{}'", root, path)
159+
}));
146160
Self {
147161
source: TestSource::Path(path),
148162
rel_path,
@@ -180,10 +194,18 @@ impl TestEntry {
180194
}
181195
}
182196

183-
/// Returns the path to the test data.
197+
/// Returns the path to match regexes against.
198+
///
199+
/// This is always the relative path to the file from the include directory.
200+
pub(crate) fn match_path(&self) -> &Utf8Path {
201+
&self.rel_path
202+
}
203+
204+
/// Returns the path to the test data, as passed into the test function.
184205
///
185-
/// For directories on disk, this is the absolute path. For `include_dir`
186-
/// sources, this is the path relative to the root of the include directory.
206+
/// For directories on disk, this is the relative path after being joined
207+
/// with the include directory. For `include_dir` sources, this is the path
208+
/// relative to the root of the include directory.
187209
pub(crate) fn test_path(&self) -> &Utf8Path {
188210
match &self.source {
189211
TestSource::Path(path) => path,
@@ -219,9 +241,37 @@ impl TestEntry {
219241
}
220242
}
221243

244+
#[cfg(windows)]
245+
#[track_caller]
246+
fn rel_path_to_forward_slashes(path: &Utf8Path) -> Utf8PathBuf {
247+
assert!(is_truly_relative(path), "path {path} must be relative");
248+
path.as_str().replace('\\', "/").into()
249+
}
250+
251+
#[cfg(not(windows))]
252+
#[track_caller]
253+
fn rel_path_to_forward_slashes(path: &Utf8Path) -> Utf8PathBuf {
254+
assert!(is_truly_relative(path), "path {path} must be relative");
255+
path.to_owned()
256+
}
257+
258+
/// Returns true if this is a path with no root-dir or prefix components.
259+
///
260+
/// On Windows, unlike `path.is_relative()`, this rejects paths like "C:temp"
261+
/// and "\temp".
262+
#[track_caller]
263+
fn is_truly_relative(path: &Utf8Path) -> bool {
264+
path.components().all(|c| match c {
265+
Utf8Component::Normal(_) | Utf8Component::CurDir | Utf8Component::ParentDir => true,
266+
Utf8Component::RootDir | Utf8Component::Prefix(_) => false,
267+
})
268+
}
269+
222270
#[derive(Debug)]
223271
#[doc(hidden)]
224272
pub(crate) enum TestSource {
273+
/// A data source on disk, with the path being the relative path to the file
274+
/// from the crate root.
225275
Path(Utf8PathBuf),
226276
#[cfg(feature = "include-dir")]
227277
IncludeDir(&'static include_dir::File<'static>),
@@ -256,7 +306,14 @@ pub mod data_source_kinds {
256306

257307
impl<T: ToString> AsDirectory for T {
258308
fn resolve_data_source(self) -> DataSource {
259-
DataSource::Directory(self.to_string().into())
309+
let s = self.to_string();
310+
let path = Utf8Path::new(&s);
311+
312+
if !is_truly_relative(path) {
313+
panic!("data source path must be relative: '{}'", s);
314+
}
315+
316+
DataSource::Directory(rel_path_to_forward_slashes(path))
260317
}
261318
}
262319

@@ -290,6 +347,12 @@ pub mod data_source_kinds {
290347
mod tests {
291348
use super::*;
292349

350+
#[test]
351+
#[should_panic = "data source path must be relative: '/absolute/path'"]
352+
fn data_source_absolute_path_panics() {
353+
data_source_kinds::AsDirectory::resolve_data_source("/absolute/path");
354+
}
355+
293356
#[test]
294357
fn missing_test_name() {
295358
assert_eq!(derive_test_path("root".into(), "file", "test_name"), None);

src/lib.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
//! `datatest-stable` will call the `my_test` function once per matching file in
1616
//! the directory. Directory traversals are recursive.
1717
//!
18-
//! `datatest-stable` works with [cargo nextest](https://nexte.st/), and is part of the [nextest-rs
19-
//! organization](https://github.com/nextest-rs/) on GitHub.
18+
//! `datatest-stable` works with [cargo nextest](https://nexte.st/), and is part
19+
//! of the [nextest-rs organization](https://github.com/nextest-rs/) on GitHub.
20+
//! With nextest, each test case is represented as a separate test, and is run
21+
//! as a separate process in parallel.
2022
//!
2123
//! # Usage
2224
//!
@@ -43,8 +45,10 @@
4345
//! extra `Vec<u8>` parameter is specified, the contents of the file will be loaded and passed
4446
//! in as a `Vec<u8>` (erroring out if that failed).
4547
//!
46-
//! * `root` - The path to the root directory where the input files (fixtures) live. This path is
47-
//! relative to the root of the crate (the directory where the crate's `Cargo.toml` is located).
48+
//! * `root` - The path to the root directory where the input files (fixtures)
49+
//! live. This path is relative to the root of the crate (the directory where
50+
//! the crate's `Cargo.toml` is located). Absolute paths are not supported, and
51+
//! will panic at runtime.
4852
//!
4953
//! `root` is an arbitrary expression that implements `Display`, such as `&str`, or a
5054
//! function call that returns a `Utf8PathBuf`.
@@ -56,11 +60,21 @@
5660
//! `pattern` is an arbitrary expression that implements `Display`, such as
5761
//! `&str`, or a function call that returns a `String`.
5862
//!
59-
//! The passed-in `Path` and `Utf8Path` are **absolute** paths to the files to be tested.
60-
//!
6163
//! The three parameters can be repeated if you have multiple sets of data-driven tests to be run:
6264
//! `datatest_stable::harness!(testfn1, root1, pattern1, testfn2, root2, pattern2)`.
6365
//!
66+
//! ## Relative paths
67+
//!
68+
//! The `pattern` argument is tested against the **relative** path of each file,
69+
//! **excluding** the `root` prefix -- not the absolute path.
70+
//!
71+
//! The `Path` and `Utf8Path` passed into the test functions are **relative** to
72+
//! the root of the crate, obtained by joining `root` to the relative path of
73+
//! the file.
74+
//!
75+
//! For universality, all relative paths use `/` as the path separator,
76+
//! including on Windows.
77+
//!
6478
//! ## Examples
6579
//!
6680
//! This is an example test. Use it with `harness = false`.
@@ -87,6 +101,11 @@
87101
//! );
88102
//! ```
89103
//!
104+
//! If `path/to/fixtures` contains a file `foo/bar.txt`, then:
105+
//!
106+
//! * The pattern `r"^.*/*"` will match `foo/bar.txt`.
107+
//! * `my_test` and `my_test_utf8` will be called with `"path/to/fixtures/foo/bar.txt"`.
108+
//!
90109
//! ## Embedding directories at compile time
91110
//!
92111
//! With the `include-dir` feature enabled, you can use the
@@ -136,7 +155,9 @@
136155
//! ```
137156
//!
138157
//! In this case, the passed-in `Path` and `Utf8Path` are **relative** to the
139-
//! root of the included directory.
158+
//! root of the included directory. Like elsewhere in `datatest-stable`, these
159+
//! relative paths always use forward slashes as separators, including on
160+
//! Windows.
140161
//!
141162
//! Because the files don't exist on disk, the test functions must accept their
142163
//! contents as either a `String` or a `Vec<u8>`. If the argument is not
@@ -169,9 +190,11 @@
169190
//! );
170191
//! ```
171192
//!
172-
//! In this case, note that `path` will be absolute if `FIXTURES` is a string,
173-
//! and relative if `FIXTURES` is a `Dir`. Your test should be prepared to
174-
//! handle either case.
193+
//! In this case, note that `path` will be relative to the **crate directory**
194+
//! (e.g. `tests/files/foo/bar.txt`) if `FIXTURES` is a string, and relative to
195+
//! the **include directory** (e.g. `foo/bar.txt`) if `FIXTURES` is a
196+
//! [`Dir`](include_dir::Dir). Your test should be prepared to handle either
197+
//! case.
175198
//!
176199
//! # Features
177200
//!

src/runner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ impl Requirements {
152152
.walk_files()
153153
.filter_map(|entry_res| {
154154
let entry = entry_res.expect("error reading directory");
155-
let path_str = entry.test_path().as_str();
155+
let path_str = entry.match_path().as_str();
156156
if re.is_match(path_str).unwrap_or_else(|error| {
157157
panic!(
158158
"error matching pattern '{}' against path '{}' : {}",

0 commit comments

Comments
 (0)